Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/pages/junit5/extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ This feature can be enabled easily by setting ``enableAutoCapture`` to ``true``
// ...
}

Diff
----

You can declare ``@HoverflyDiff`` to run Hoverfly in diff mode (see :ref:`diffing`). You can customize the location of the simulation data as well as the Hoverfly configuration parameters.

.. code-block:: java

@HoverflyDiff(
source = @HoverflySimulate.Source(value = "hoverfly/diff/captured-wrong-simulation-for-diff.json",
type = HoverflySimulate.SourceType.CLASSPATH))
)

Also you can use ``@HoverflyValidate`` at class or method level, to assert automatically that there is no difference between simulated and captured traffic.

Nested tests
------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import io.specto.hoverfly.junit5.api.HoverflyCapture;
import io.specto.hoverfly.junit5.api.HoverflyConfig;
import io.specto.hoverfly.junit5.api.HoverflyCore;
import io.specto.hoverfly.junit5.api.HoverflyDiff;
import io.specto.hoverfly.junit5.api.HoverflySimulate;
import io.specto.hoverfly.junit5.api.HoverflyValidate;
import org.junit.jupiter.api.extension.*;

import java.lang.reflect.AnnotatedElement;
Expand All @@ -27,17 +29,20 @@
* and make use of the Hoverfly API directly.
*
* {@link HoverflyCore} annotation can be used along with HoverflyExtension to set the mode and customize the configurations.
* @see HoverflyCore for more configuration options*
* @see HoverflyCore for more configuration options
*
* {@link HoverflySimulate} annotation can be used to instruct Hoverfly to load simulation from a file located at
* Hoverfly default path (src/test/resources/hoverfly) and file called with fully qualified name of test class, replacing dots (.) and dollar signs ($) to underlines (_).
* @see HoverflySimulate for more configuration options
*
* {@link HoverflyCapture} annotation can be used to config Hoverfly to capture and export simulation to a file located at
* Hoverfly default path (src/test/resources/hoverfly) and file called with fully qualified name of test class, replacing dots (.) and dollar signs ($) to underlines (_).
* @see HoverflyCapture for more configuration options*
* @see HoverflyCapture for more configuration options
*
* {@link HoverflyDiff} annotation can be used along with HoverflyExtension to set the mode to diff and customize the configurations.
* @see HoverflyDiff for more configuration options*
*/
public class HoverflyExtension implements BeforeEachCallback, AfterAllCallback, BeforeAllCallback, ParameterResolver {
public class HoverflyExtension implements AfterEachCallback, BeforeEachCallback, AfterAllCallback, BeforeAllCallback, ParameterResolver {

private Hoverfly hoverfly;
private SimulationSource source = SimulationSource.empty();
Expand Down Expand Up @@ -67,22 +72,16 @@ public void beforeAll(ExtensionContext context) {
HoverflySimulate hoverflySimulate = annotatedElement.getAnnotation(HoverflySimulate.class);
config = hoverflySimulate.config();

String path = getPath(context, hoverflySimulate.source());
HoverflySimulate.SourceType type = hoverflySimulate.source().type();
String path = hoverflySimulate.source().value();

if (path.isEmpty()) {
path = context.getTestClass()
.map(HoverflyExtensionUtils::getFileNameFromTestClass)
.orElseThrow(() -> new IllegalStateException("No test class found."));
}
source = getSimulationSource(path, type);

if(hoverflySimulate.enableAutoCapture()) {
AutoCaptureSource.newInstance(path, type).ifPresent(source -> {
mode = HoverflyMode.CAPTURE;
capturePath = source.getCapturePath();
});
}
source = getSimulationSource(path, type);

} else if (isAnnotated(annotatedElement, HoverflyCore.class)) {
HoverflyCore hoverflyCore = annotatedElement.getAnnotation(HoverflyCore.class);
Expand All @@ -101,6 +100,13 @@ public void beforeAll(ExtensionContext context) {
}

capturePath = getCapturePath(hoverflyCapture.path(), filename);
} else if (isAnnotated(annotatedElement, HoverflyDiff.class)) {
HoverflyDiff hoverflyDiff = annotatedElement.getAnnotation(HoverflyDiff.class);
config = hoverflyDiff.config();
mode = HoverflyMode.DIFF;
String path = getPath(context, hoverflyDiff.source());
HoverflySimulate.SourceType type = hoverflyDiff.source().type();
source = getSimulationSource(path, type);
}

if (!isRunning()) {
Expand All @@ -113,11 +119,23 @@ public void beforeAll(ExtensionContext context) {
}
}

private String getPath(ExtensionContext context, HoverflySimulate.Source source) {
String path = source.value();

if (path.isEmpty()) {
path = context.getTestClass()
.map(HoverflyExtensionUtils::getFileNameFromTestClass)
.orElseThrow(() -> new IllegalStateException("No test class found."));
}
return path;
}

@Override
public void afterAll(ExtensionContext context) {
if (isRunning()) {

try {
verifyHoverflyValidate(context);
if (this.capturePath != null) {
this.hoverfly.exportSimulation(this.capturePath);
}
Expand All @@ -144,4 +162,20 @@ private boolean isRunning() {
return this.hoverfly != null;
}

@Override
public void afterEach(ExtensionContext context) {
if (isRunning()) {
verifyHoverflyValidate(context);
}
}

private void verifyHoverflyValidate(ExtensionContext context) {
AnnotatedElement annotatedElement =
context.getElement().orElseThrow(() -> new IllegalStateException("No test class found."));

if (isAnnotated(annotatedElement, HoverflyValidate.class)) {
final HoverflyValidate hoverflyValidate = annotatedElement.getAnnotation(HoverflyValidate.class);
hoverfly.assertThatNoDiffIsReported(hoverflyValidate.reset());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.specto.hoverfly.junit5.api;

import io.specto.hoverfly.junit5.HoverflyExtension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation used along with {@link HoverflyExtension} to run Hoverfly in diff mode
* By default, it tries to compare simulation file from default Hoverfly test resources path ("src/test/resources/hoverfly")
* with filename equals to the fully qualified class name of the annotated class.
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HoverflyDiff {

/**
* Hoverfly configurations
* @see HoverflyConfig
*/
HoverflyConfig config() default @HoverflyConfig;

/**
* Simulation source to import for comparision
* @see HoverflySimulate.Source
*/
HoverflySimulate.Source source() default @HoverflySimulate.Source;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.specto.hoverfly.junit5.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation used to to verify if any discrepancy is detected.
* Can be used at class and method level.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HoverflyValidate {

boolean reset() default false;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.specto.hoverfly.junit5;

import io.specto.hoverfly.junit.core.Hoverfly;
import io.specto.hoverfly.junit.core.HoverflyMode;
import io.specto.hoverfly.junit5.api.HoverflyConfig;
import io.specto.hoverfly.junit5.api.HoverflyDiff;
import io.specto.hoverfly.junit5.api.HoverflySimulate;
import io.specto.hoverfly.junit5.api.HoverflyValidate;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

public class HoverflyDiffTest {

private OkHttpClient client = new OkHttpClient();

@Nested
@ExtendWith(HoverflyExtension.class)
@HoverflyDiff(
source = @HoverflySimulate.Source(value = "hoverfly/diff/captured-simulation-for-diff.json",
type = HoverflySimulate.SourceType.CLASSPATH),
config = @HoverflyConfig(proxyLocalHost = true, captureHeaders = "Date")
)
class NestedNoDiffTest {

@Test
void shouldInjectCustomInstanceAsParameterWithRequiredMode(Hoverfly hoverfly) {
assertThat(hoverfly.getMode()).isEqualTo(HoverflyMode.DIFF);
}

@Test
@HoverflyValidate(reset = true)
void shouldValidateHoverflyHealthApi(Hoverfly hoverfly) throws IOException {

final Request request = new Request.Builder()
.url("http://localhost:" + hoverfly.getHoverflyConfig().getAdminPort() + "/api/health")
.build();

final Response response = client.newCall(request).execute();

assertThat(response.code()).isEqualTo(200);
}
}

@Nested
@ExtendWith(HoverflyExtension.class)
@HoverflyDiff(
source = @HoverflySimulate.Source(value = "hoverfly/diff/captured-wrong-simulation-for-diff.json",
type = HoverflySimulate.SourceType.CLASSPATH),
config = @HoverflyConfig(proxyLocalHost = true, captureHeaders = "Date")
)
class NestedDiffTest {

@Test
void shouldInjectCustomInstanceAsParameterWithRequiredMode(Hoverfly hoverfly) {
assertThat(hoverfly.getMode()).isEqualTo(HoverflyMode.DIFF);
}

@Test
void shouldValidateHoverflyHealthApiAndFailWhenDifferent(Hoverfly hoverfly) throws IOException {

final Request request = new Request.Builder()
.url("http://localhost:" + hoverfly.getHoverflyConfig().getAdminPort() + "/api/health")
.build();

final Response response = client.newCall(request).execute();
assertThat(response.code()).isEqualTo(200);

Throwable thrown = catchThrowable(() -> {
hoverfly.assertThatNoDiffIsReported(true);
});

assertThat(thrown).isInstanceOf(AssertionError.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"data" : {
"pairs" : [ {
"request" : {
"path" : [ {
"matcher" : "exact",
"value" : "/api/v2/hoverfly/mode"
} ],
"method" : [ {
"matcher" : "exact",
"value" : "GET"
} ],
"destination" : [ {
"matcher" : "exact",
"value" : "localhost:64921"
} ],
"scheme" : [ {
"matcher" : "exact",
"value" : "http"
} ],
"query" : { },
"body" : [ {
"matcher" : "exact",
"value" : ""
} ],
"headers" : {
"Accept-Encoding" : [ {
"matcher" : "exact",
"value" : "gzip"
} ],
"Connection" : [ {
"matcher" : "exact",
"value" : "Keep-Alive"
} ],
"User-Agent" : [ {
"matcher" : "exact",
"value" : "okhttp/3.12.0"
} ]
}
},
"response" : {
"status" : 200,
"body" : "{\"mode\":\"capture\",\"arguments\":{\"headersWhitelist\":[\"*\"]}}",
"encodedBody" : false,
"templated" : false,
"headers" : {
"Content-Length" : [ "57" ],
"Content-Type" : [ "application/json; charset=utf-8" ],
"Date" : [ "Fri, 15 Mar 2019 18:22:53 GMT" ],
"Hoverfly" : [ "Was-Here" ]
}
}
}, {
"request" : {
"path" : [ {
"matcher" : "exact",
"value" : "/api/health"
} ],
"method" : [ {
"matcher" : "exact",
"value" : "GET"
} ],
"destination" : [ {
"matcher" : "glob",
"value" : "*"
} ],
"scheme" : [ {
"matcher" : "exact",
"value" : "http"
} ],
"query" : { },
"body" : [ {
"matcher" : "exact",
"value" : ""
} ],
"headers" : {
"Accept-Encoding" : [ {
"matcher" : "exact",
"value" : "gzip"
} ],
"Connection" : [ {
"matcher" : "exact",
"value" : "Keep-Alive"
} ],
"User-Agent" : [ {
"matcher" : "exact",
"value" : "okhttp/3.12.0"
} ]
}
},
"response" : {
"status" : 200,
"body" : "{\"message\":\"Hoverfly is healthy\"}\n",
"encodedBody" : false,
"templated" : false,
"headers" : {
"Content-Length" : [ "34" ],
"Content-Type" : [ "application/json; charset=utf-8" ],
"Date" : [ "Fri, 15 Mar 2019 18:22:53 GMT" ],
"Hoverfly" : [ "Was-Here" ]
}
}
} ],
"globalActions" : {
"delays" : [ ]
}
},
"meta" : {
"schemaVersion" : "v5",
"hoverflyVersion" : "v1.0.0-rc.2",
"timeExported" : "2019-03-15T19:22:53+01:00"
}
}
Loading