diff --git a/docs/pages/junit5/extension.rst b/docs/pages/junit5/extension.rst index 4c3d65ec..d77d485c 100644 --- a/docs/pages/junit5/extension.rst +++ b/docs/pages/junit5/extension.rst @@ -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 ------------ diff --git a/junit5/src/main/java/io/specto/hoverfly/junit5/HoverflyExtension.java b/junit5/src/main/java/io/specto/hoverfly/junit5/HoverflyExtension.java index 2045c049..0eeddd99 100644 --- a/junit5/src/main/java/io/specto/hoverfly/junit5/HoverflyExtension.java +++ b/junit5/src/main/java/io/specto/hoverfly/junit5/HoverflyExtension.java @@ -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; @@ -27,7 +29,7 @@ * 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 (_). @@ -35,9 +37,12 @@ * * {@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(); @@ -67,14 +72,9 @@ 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 -> { @@ -82,7 +82,6 @@ public void beforeAll(ExtensionContext context) { capturePath = source.getCapturePath(); }); } - source = getSimulationSource(path, type); } else if (isAnnotated(annotatedElement, HoverflyCore.class)) { HoverflyCore hoverflyCore = annotatedElement.getAnnotation(HoverflyCore.class); @@ -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()) { @@ -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); } @@ -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()); + } + } } diff --git a/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyDiff.java b/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyDiff.java new file mode 100644 index 00000000..3e560c0b --- /dev/null +++ b/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyDiff.java @@ -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; + +} diff --git a/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyValidate.java b/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyValidate.java new file mode 100644 index 00000000..c0ca8710 --- /dev/null +++ b/junit5/src/main/java/io/specto/hoverfly/junit5/api/HoverflyValidate.java @@ -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; + +} diff --git a/junit5/src/test/java/io/specto/hoverfly/junit5/HoverflyDiffTest.java b/junit5/src/test/java/io/specto/hoverfly/junit5/HoverflyDiffTest.java new file mode 100644 index 00000000..0f105afb --- /dev/null +++ b/junit5/src/test/java/io/specto/hoverfly/junit5/HoverflyDiffTest.java @@ -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); + } + } +} diff --git a/junit5/src/test/resources/hoverfly/diff/captured-simulation-for-diff.json b/junit5/src/test/resources/hoverfly/diff/captured-simulation-for-diff.json new file mode 100644 index 00000000..f3b00b46 --- /dev/null +++ b/junit5/src/test/resources/hoverfly/diff/captured-simulation-for-diff.json @@ -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" + } +} \ No newline at end of file diff --git a/junit5/src/test/resources/hoverfly/diff/captured-wrong-simulation-for-diff.json b/junit5/src/test/resources/hoverfly/diff/captured-wrong-simulation-for-diff.json new file mode 100644 index 00000000..f6a877a6 --- /dev/null +++ b/junit5/src/test/resources/hoverfly/diff/captured-wrong-simulation-for-diff.json @@ -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" : "{\"myothermessage\":\"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" + } +} \ No newline at end of file diff --git a/src/main/java/io/specto/hoverfly/junit/core/Hoverfly.java b/src/main/java/io/specto/hoverfly/junit/core/Hoverfly.java index 8a7ab50f..94b33b2e 100644 --- a/src/main/java/io/specto/hoverfly/junit/core/Hoverfly.java +++ b/src/main/java/io/specto/hoverfly/junit/core/Hoverfly.java @@ -59,6 +59,7 @@ import static io.specto.hoverfly.junit.core.HoverflyConfig.localConfigs; import static io.specto.hoverfly.junit.core.HoverflyMode.CAPTURE; +import static io.specto.hoverfly.junit.core.HoverflyMode.DIFF; import static io.specto.hoverfly.junit.core.HoverflyUtils.checkPortInUse; import static io.specto.hoverfly.junit.core.HoverflyUtils.readSimulationFromString; import static io.specto.hoverfly.junit.dsl.matchers.HoverflyMatchers.any; @@ -517,6 +518,8 @@ private void waitForHoverflyToBecomeHealthy() { private void setModeWithArguments(HoverflyMode mode, HoverflyConfiguration config) { if (mode == CAPTURE) { hoverflyClient.setMode(mode, new ModeArguments(config.getCaptureHeaders(), config.isStatefulCapture())); + } else if (mode == DIFF) { + hoverflyClient.setMode(mode, new ModeArguments(config.getCaptureHeaders())); } else { hoverflyClient.setMode(mode); }