Skip to content

Commit

Permalink
Merge pull request #104 from Fresh-D101/karate
Browse files Browse the repository at this point in the history
swagger-coverage integration for Karate Framework
  • Loading branch information
viclovsky authored Mar 18, 2022
2 parents 6c52c7e + faa9117 commit 4874fef
Show file tree
Hide file tree
Showing 23 changed files with 4,788 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ build

# Allure
allure-results
bin/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The next stage is to compare saved result with generated conditions from current
You can use swagger-coverage with any language and framework. You need to have proxy/filter/interceptor that accumulates data in swagger format.
Swagger-coverage have rest-assured integration from the box.

> There is also a Karate integration, which has its own [manual](/swagger-coverage-karate/README.md).
Add filter dependency:
```xml
<dependency>
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ configure(subprojects) {
dependency("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3")
dependency("com.fasterxml.jackson.core:jackson-annotations:2.12.3")
dependency("org.springframework:spring-web:5.3.7")
dependency("com.intuit.karate:karate-core:1.2.0.RC1")
}
}

Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ rootProject.name = "swagger-coverage"
include("swagger-coverage-commandline")
include("swagger-coverage-rest-assured")
include("swagger-coverage-commons")
include("swagger-coverage-karate")
137 changes: 137 additions & 0 deletions swagger-coverage-karate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# swagger-coverage for Karate
This module provides an integration of swagger-coverage for the [Karate Framework](https://github.com/karatelabs/karate). With the help of a single test runner, the coverage report can thus be generated automatically after the API tests.

## How it Works
Swagger-coverage for Karate uses the [server-side features](https://github.com/karatelabs/karate/tree/master/karate-netty#karate-netty) of the Karate framework to implement a proxy server which intercepts the HTTP calls, extracts the necessary information, and then redirects the call to the original destination. For this, a local mock server is started on a free port before running the tests to host the proxy. The port number is chosen at runtime and can be accessed by calling `karate.properties['proxy.port']`.

This approach has the advantage that the tests do not have to be changed and swagger-coverage can also be integrated into existing Karate projects with minimal effort. In addition, multiple Test Runners can be used in parallel. Since you usually don't want to create a report every time you run the tests (e.g. during active test implementation), you can decide individually when to use the Coverage Test Runner.

## Setup and How to Use
The Swagger Coverage Runner requires at least [Java](https://www.oracle.com/java/technologies/downloads/) 8 and the use of either JUnit 4 or JUnit 5.

The `swagger-coverage-karate` artifact also includes the `swagger-coverage-commons` and the `swagger-coverage-commandline` modules.
To add the dependency to your project when using Maven, add the following to your pom.xml:
```xml
<dependency>
<groupId>com.github.viclovsky</groupId>
<artifactId>swagger-coverage-karate</artifactId>
<version>${latest-swagger-coverage-version}</version>
</dependency>
```

If you use Gradle instead, add this to your build.gradle file:

```gradle
testImplementation "com.github.viclovsky:swagger-coverage-karate:${latest-swagger-coverage-version}"
```


### <a name="coverageDir"></a> Coverage Directory and Naming Conventions
The easiest way to provide the Test Runner with all the necessary information is to have a folder in the project where the required files are located. If these are named correctly, as in the following, it is only necessary to specify the path to this directory.

> The files can also be specifically set in the [options](#options). If not, the coverage directory is always checked for the files seen below.
```
api-test-coverage (can be any path and name)
|
+-- swagger-coverage-config.json
+-- swagger-specification.json/yaml
```

If specified, the directory will function as the working directory for the swagger-coverage tool. Therefore, not only the `swagger-coverage-output` folder is then created in there, where the generated swagger models of the HTTP calls are stored, but also the final HTML coverage report.
If not specified, the needed file paths must specifically be provided in the arguments of the Test Runner and everything else will be generated within the current working directory (the root of the project, most of the time).

### <a name="runner"></a> The SwaggerCoverageRunner
The `SwaggerCoverageRunner` extends the [Karate Runner](https://github.com/karatelabs/karate#junit-4-parallel-execution) class. Therefore, all options which are available there, can also be used with the coverage runner. This enables an easy migration, since it is only neccesary to add the additional builder methods to provide the runner with the needed information for creating the coverage report.

For example:
```java
import java.net.URI;
import com.github.viclovsky.swagger.coverage.karate.SwaggerCoverageRunner;
import com.intuit.karate.Results;
import org.junit.jupiter.api.Test;

public class CoverageReportRunner {

@Test
void testAll(){
Results results = SwaggerCoverageRunner.path("classpath:some/path")
.coverageDir("api-test-coverage")
.swaggerSpec(URI.create("https://petstore3.swagger.io/api/v3/openapi.json"))
.oas3()
.outputJunitXml(true)
.karateEnv("dev")
.parallel(1);
}
}
```

#### <a name="options"></a> Available Options
| Option | Description |
|--------|--------------|
| `.coverageDir(String)` | set the working directory for the swagger-coverage tool. See [Coverage Directory](#coverageDir). |
| `.swaggerSpec(URI)` | specifically set the path to the Swagger/OpenAPI specification. Can also be a URL to a remote spec, useful for when swagger is hosted on the test environment.|
| `.swaggerCoverageConfig(String)` | specifically set the path to the [Config File](https://github.com/viclovsky/swagger-coverage#configuration-options). |
| `.swagger()` | use this when the specification uses the [Swagger/OpenAPI 2.0](https://swagger.io/specification/v2/) format. |
| `.oas3()` | use this when the specification uses the [OpenAPI 3.0](https://swagger.io/specification/) format. |
| `.backupCoverageOutput(boolean)` | backup the `swagger-coverage-output` folder, if one exists from a previous run. Default is set to `false`. |

### Swagger Coverage Options
There are a few options you may want to decide on more flexible instead of setting them once at the start of the tests. For that reason, the `scOptions` object is provided and can be used anywhere in the Karate-context. It holds the following functionalities:

#### `scOptions.setDestUrl(arg)`
This tells the proxy server where to send the HTTP calls to. Most of the time, it is sufficient to set this once in the `karate-config.js`. But if needed, it can be changed before every single call.

> Note that if your destination URL uses HTTPS, take a look at [The Problem with HTTPS and the Workaround](#https).
#### `scOptions.setPathPattern(arg)`
One of the conditions in the coverage report checks, whether a specified param was set in the test. In order for this to happen, the parameter name must be included in the swagger representation of the HTTP call. But unlike query parameters, for example, path parameters in Karate are not specified by their name but only by their value. That's why on the server side you can only get to this parameter name in a certain way. For this the path pattern in the style of `/some/path/{paramName}` is needed. To correctly use the aforementioned condition, this option can be used to specify the needed path pattern.

For example:

```gherkin
Feature: using the 'setPathPattern' option
Background:
* url "http://petstore3.swagger.io"
* eval scOptions.setDestUrl("https://petstore3.swagger.io")
Scenario:
* eval scOptions.setPathPattern("/api/v3/pet/{petId}")
Given path "api", "v3", "pet", 2
When method GET
Then status 200
```

#### `scOptions.ignoreNextCall()`
Sometimes you don't want a call to be included in the coverage report, for example when reusing feature files to create entities etc. This options tells the proxy server to simply send the call to the destination URL without extracting any information and thus ignoring it for the coverage report.

## <a name="https"></a> The Problem with HTTPS and the Workaround
As of now, the karate proxy server does not support HTTPS calls, as it is unable to unpack them (more information can be found in this [thread](https://github.com/karatelabs/karate/issues/640)). Therefore, the requests must be send as HTTP calls when using the [SwaggerCoverageRunner](#runner), and then the proxy can send them to the HTTPS destination. The following example shows, how a dynamic setup can be achieved in the `karate-config.json`:

```javaScript
function fn(){
var env = karate.env;
karate.log('karate.env system property was:', env);
if (!env){
env = 'dev';
}

var uri = "://petstore3.swagger.io";
var protocol = "https";

// always set the destination url to use https
scOptions.setDestUrl(protocol + uri);

// if the port is set, that means the proxy server is used. Then use the http protocol.
if (karate.properties['proxy.port']){
protocol = "http";
}

var baseUrl = protocol + uri;

return {
baseUrl : baseUrl
}
}
```
36 changes: 36 additions & 0 deletions swagger-coverage-karate/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
java
`java-library`
}

description = "Swagger Coverage Karate"

repositories {
mavenCentral()
}

dependencies {
api(project(":swagger-coverage-commons"))
api(project(":swagger-coverage-commandline"))
implementation("io.swagger:swagger-models")
implementation("io.swagger.core.v3:swagger-models")
implementation("com.intuit.karate:karate-core")

//needed for karate runner
implementation("com.linecorp.armeria:armeria:1.14.1")
implementation("io.netty:netty-all:4.1.74.Final")
implementation("org.thymeleaf:thymeleaf:3.0.15.RELEASE")
implementation("io.github.classgraph:classgraph:4.8.108")
implementation("org.antlr:antlr4-runtime:4.9.3")
implementation("org.apache.httpcomponents:httpclient:4.5.13")

testImplementation("junit:junit")
testImplementation("org.hamcrest:hamcrest")
testImplementation("com.github.tomakehurst:wiremock")
}

tasks {
test {
workingDir(buildDir)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.github.viclovsky.swagger.coverage.karate;

import java.util.List;
import java.util.Map;

public class Request {
private String baseUrl;
private String path;
private Map<String, List<String>> requestParams;
private Map<String, List<String>> headerParams;
private Map<String, List<Map<String, String>>> requestParts;
private Map<String, String> pathParams;
private Boolean hasBody;
private String method;
private int statusCode;
private Map<String, List<String>> responseHeaders;

// #region Getter/Setter
public String getBaseUrl() {
return baseUrl;
}

public void setBaseUrl(String uri) {
this.baseUrl = uri;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public Map<String, List<String>> getRequestParams() {
return requestParams;
}

public void setRequestParams(Map<String, List<String>> requestParams) {
this.requestParams = requestParams;
}

public Map<String, List<Map<String, String>>> getRequestParts() {
return requestParts;
}

public void setRequestParts(Map<String, List<Map<String, String>>> requestParts) {
this.requestParts = requestParts;
}

public Map<String, String> getPathParams() {
return pathParams;
}

public void setPathParams(Map<String, String> pathParams) {
this.pathParams = pathParams;
}

public Map<String, List<String>> getHeaderParams() {
return headerParams;
}

public void setHeaderParams(Map<String, List<String>> headerParams) {
this.headerParams = headerParams;
}

public Map<String, List<String>> getResponseHeaders() {
return responseHeaders;
}

public void setResponseHeaders(Map<String, List<String>> responseHeaders) {
this.responseHeaders = responseHeaders;
}

public String getMethod() {
return method;
}

public void setMethod(String method) {
this.method = method;
}

public int getStatusCode() {
return statusCode;
}

public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}

public Boolean hasBody() {
return hasBody;
}

public void setHasBody(Boolean hasBody) {
this.hasBody = hasBody;
}
// #endregion
}
Loading

0 comments on commit 4874fef

Please sign in to comment.