You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ja...@apache.org on 2020/10/29 08:44:49 UTC

[camel-quarkus] 01/02: Add WireMock test support

This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git

commit 796fe87c1d84880c57651c50176a40ca79191a5d
Author: James Netherton <ja...@gmail.com>
AuthorDate: Wed Oct 28 08:52:58 2020 +0000

    Add WireMock test support
---
 integration-tests-support/pom.xml                  |   1 +
 integration-tests-support/wiremock/README.adoc     |  48 +++++
 integration-tests-support/wiremock/pom.xml         |  62 ++++++
 .../camel/quarkus/test/wiremock/MockServer.java    |  32 ++++
 .../WireMockTestResourceLifecycleManager.java      | 211 +++++++++++++++++++++
 pom.xml                                            |   1 +
 poms/bom-test/pom.xml                              |  28 +++
 7 files changed, 383 insertions(+)

diff --git a/integration-tests-support/pom.xml b/integration-tests-support/pom.xml
index cd67962..8c8ad16 100644
--- a/integration-tests-support/pom.xml
+++ b/integration-tests-support/pom.xml
@@ -45,6 +45,7 @@
         <module>test-support</module>
         <module>testcontainers-support</module>
         <module>mock-backend</module>
+        <module>wiremock</module>
     </modules>
 
 </project>
diff --git a/integration-tests-support/wiremock/README.adoc b/integration-tests-support/wiremock/README.adoc
new file mode 100644
index 0000000..0b477e7
--- /dev/null
+++ b/integration-tests-support/wiremock/README.adoc
@@ -0,0 +1,48 @@
+== WireMock test support
+
+This module provides test support for http://wiremock.org/[WireMock]. This enables the HTTP interactions between Camel & third party services to be
+stubbed, recorded & replayed.
+
+=== Usage
+
+Add the following test scoped dependency into the extension integration test pom.xml:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-integration-wiremock-support</artifactId>
+    <scope>test</scope>
+</dependency>
+----
+
+Next create a class that extends the abstract `WireMockTestResourceLifecycleManager`. You'll need to implement abstract methods:
+
+* `getRecordTargetBaseUrl` - To specify the base URL of the service interactions to be recorded
+* `isMockingEnabled` - To determine whether the test should start the mock server or invoke the real service
+
+You can also override the `start` method to perform custom initialization logic and return additional configuration properties that Camel components may need.
+
+`WireMockTestResourceLifecycleManager` sets a system property named `wiremock.url`, which is the base URL to the running WireMock server. 
+In playback mode, you'll need to configure the Camel component under test to direct its API calls to this URL.
+
+==== Recording HTTP interactions
+
+The fundamentals of WireMock record and playback are documented http://wiremock.org/docs/record-playback/[here]. Setup of the `WireMockServer` is already handled by
+`WireMockTestResourceLifecycleManager`. All you need to do is ensure directory `src/test/resources/mappings` exists and to trigger recording by either:
+
+System property `-Dwiremock.record=true`
+
+Or
+
+Environment variable `WIREMOCK_RECORD=true`
+
+When all tests complete, the recorded HTTP interactions will show up in the 'mappings' directory. The recorded stub file names are quite complex, feel free
+to update them to something more human friendly.
+
+By default, stub mapping files are not saved when requests return an unsuccessful response code. You can alter this behaviour by overriding method `isDeleteRecordedMappingsOnError`.
+
+It's important to inspect the recorded files for the presence of any real API keys, secrets or passwords and replace them with made up values.
+
+WireMock generates new stub files on each recording, so it's a good idea to remove the existing contents from the 'mappings' directory
+before a recording run.
diff --git a/integration-tests-support/wiremock/pom.xml b/integration-tests-support/wiremock/pom.xml
new file mode 100644
index 0000000..320ee44
--- /dev/null
+++ b/integration-tests-support/wiremock/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-integration-tests-support</artifactId>
+        <version>1.4.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>camel-quarkus-integration-wiremock-support</artifactId>
+    <name>Camel Quarkus :: Integration Tests :: WireMock :: Support</name>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.camel.quarkus</groupId>
+                <artifactId>camel-quarkus-bom-test</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-integration-test-support</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-integration-test-support-mock-backend</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tomakehurst</groupId>
+            <artifactId>wiremock-jre8</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java
new file mode 100644
index 0000000..b6f29f1
--- /dev/null
+++ b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.test.wiremock;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used by the companion WireMockTestResourceLifecycleManager to inject
+ * {@link com.github.tomakehurst.wiremock.WireMockServer} instances into
+ * test classes.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface MockServer {
+}
diff --git a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java
new file mode 100644
index 0000000..dc64c87
--- /dev/null
+++ b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.test.wiremock;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.recording.RecordingStatus;
+import com.github.tomakehurst.wiremock.recording.SnapshotRecordResult;
+import com.github.tomakehurst.wiremock.stubbing.StubMapping;
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import org.apache.camel.quarkus.test.mock.backend.MockBackendUtils;
+import org.jboss.logging.Logger;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.recordSpec;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+
+public abstract class WireMockTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager {
+
+    protected static final Logger LOG = Logger.getLogger(WireMockTestResourceLifecycleManager.class);
+    protected WireMockServer server;
+
+    /**
+     * Starts the {@link WireMockServer} and configures request / response recording if it has been enabled
+     */
+    @Override
+    public Map<String, String> start() {
+        Map<String, String> properties = new HashMap<>();
+
+        if (isMockingEnabled() || isRecordingEnabled()) {
+            server = createServer();
+            server.start();
+
+            if (isRecordingEnabled()) {
+                String recordTargetBaseUrl = getRecordTargetBaseUrl();
+                if (recordTargetBaseUrl != null) {
+                    LOG.infof("Enabling WireMock recording for %s", recordTargetBaseUrl);
+                    server.startRecording(recordSpec()
+                            .forTarget(recordTargetBaseUrl)
+                            .allowNonProxied(false));
+                } else {
+                    throw new IllegalStateException(
+                            "Must return a non-null value from getRecordTargetBaseUrl() in order to support WireMock recording");
+                }
+            }
+
+            String wireMockUrl = "http://localhost:" + server.port();
+            LOG.infof("WireMock started on %s", wireMockUrl);
+            properties.put("wiremock.url", wireMockUrl);
+        }
+
+        return properties;
+    }
+
+    /**
+     * Stops the {@link WireMockServer} instance if it was started and stops recording if record mode was enabled
+     */
+    @Override
+    public void stop() {
+        if (server != null) {
+            LOG.info("Stopping WireMockServer");
+            if (server.getRecordingStatus().getStatus().equals(RecordingStatus.Recording)) {
+                LOG.info("Stopping recording");
+                SnapshotRecordResult recordResult = server.stopRecording();
+
+                List<StubMapping> stubMappings = recordResult.getStubMappings();
+                if (isDeleteRecordedMappingsOnError()) {
+                    for (StubMapping mapping : stubMappings) {
+                        int status = mapping.getResponse().getStatus();
+                        if (status >= 300 && mapping.shouldBePersisted()) {
+                            try {
+                                String fileName = mapping.getName() + "-" + mapping.getId() + ".json";
+                                Path mappingFilePath = Paths.get("./src/test/resources/mappings/", fileName);
+                                Files.deleteIfExists(mappingFilePath);
+                                LOG.infof("Deleted mapping file %s as status code was %d", fileName, status);
+                            } catch (IOException e) {
+                                LOG.errorf("Failed to delete mapping file %s", e, mapping.getName());
+                            }
+                        }
+                    }
+                }
+            }
+            server.stop();
+        }
+    }
+
+    /**
+     * If mocking is enabled, inject an instance of {@link WireMockServer} into any fields
+     * annotated with {@link MockServer}. This gives full control over creating recording rules
+     * and some aspects of the server lifecycle.
+     *
+     * The server instance is not injected if mocking is explicitly disabled, and therefore the resulting
+     * {@link MockServer} annotated field value will be null.
+     */
+    @Override
+    public void inject(Object testInstance) {
+        if (isMockingEnabled() || isRecordingEnabled()) {
+            Class<?> c = testInstance.getClass();
+            for (Field field : c.getDeclaredFields()) {
+                if (field.getAnnotation(MockServer.class) != null) {
+                    if (!WireMockServer.class.isAssignableFrom(field.getType())) {
+                        throw new RuntimeException("@MockServer can only be used on fields of type WireMockServer");
+                    }
+
+                    field.setAccessible(true);
+                    try {
+                        if (server == null) {
+                            server = createServer();
+                            server.start();
+                        }
+                        LOG.infof("Injecting WireMockServer for field %s", field.getName());
+                        field.set(testInstance, server);
+                        return;
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Determines whether each of the given environment variable names is set
+     */
+    protected boolean envVarsPresent(String... envVarNames) {
+        if (envVarNames.length == 0) {
+            throw new IllegalArgumentException("envVarNames must not be empty");
+        }
+
+        boolean present = true;
+        for (String envVar : envVarNames) {
+            if (System.getenv(envVar) == null) {
+                present = false;
+                break;
+            }
+        }
+
+        return present;
+    }
+
+    /**
+     * Get the value of a given environment variable or a default value if it does not exist
+     */
+    protected String envOrDefault(String envVarName, String defaultValue) {
+        String value = System.getenv(envVarName);
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * Whether recorded stub mapping files should be deleted if the HTTP response was an error code (>= 400).
+     *
+     * By default this returns true. Can be overridden if an error response is desired / expected from the HTTP request.
+     */
+    protected boolean isDeleteRecordedMappingsOnError() {
+        return true;
+    }
+
+    /**
+     * The target base URL that WireMock should watch for when recording requests.
+     *
+     * For example, if a test triggers an HTTP call on an external endpoint like https://api.foo.com/some/resource.
+     * Then the base URL would be https://api.foo.com
+     */
+    protected abstract String getRecordTargetBaseUrl();
+
+    /**
+     * Conditions under which the {@link WireMockServer} should be started.
+     */
+    protected abstract boolean isMockingEnabled();
+
+    /**
+     * Creates and starts a {@link WireMockServer} on a random port. {@link MockBackendUtils} triggers the log
+     * message that signifies mocking is in use.
+     */
+    private WireMockServer createServer() {
+        LOG.info("Starting WireMockServer");
+        MockBackendUtils.startMockBackend(true);
+        return new WireMockServer(options().dynamicPort());
+    }
+
+    /**
+     * Determine whether to enable WireMock record mode:
+     *
+     * http://wiremock.org/docs/record-playback/
+     */
+    private boolean isRecordingEnabled() {
+        String recordEnabled = System.getProperty("wiremock.record", System.getenv("WIREMOCK_RECORD"));
+        return recordEnabled != null && recordEnabled.equals("true");
+    }
+}
diff --git a/pom.xml b/pom.xml
index 892e91d..5274b94 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,7 @@
         <sshd.version>2.3.0</sshd.version>
         <stax2.version>4.2</stax2.version>
         <testcontainers.version>1.14.3</testcontainers.version>
+        <wiremock.version>2.27.2</wiremock.version>
         <zt-exec.version>1.11</zt-exec.version>
 
         <!-- Maven plugin versions (keep sorted alphabetically) -->
diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml
index 1d970b6..1ffb844 100644
--- a/poms/bom-test/pom.xml
+++ b/poms/bom-test/pom.xml
@@ -98,6 +98,11 @@
                 <artifactId>camel-quarkus-integration-testcontainers-support</artifactId>
                 <version>${camel-quarkus.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.camel.quarkus</groupId>
+                <artifactId>camel-quarkus-integration-wiremock-support</artifactId>
+                <version>${camel-quarkus.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>org.apache.ftpserver</groupId>
@@ -166,6 +171,29 @@
                 </exclusions>
             </dependency>
             <dependency>
+                <groupId>com.github.tomakehurst</groupId>
+                <artifactId>wiremock-jre8</artifactId>
+                <version>${wiremock.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>javax.xml.bind</groupId>
+                        <artifactId>jaxb-api</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.checkerframework</groupId>
+                        <artifactId>checker-qual</artifactId>
+                    </exclusion>
+                    <exclusion> <!-- fix dependencyConvergence clash with junit-jupiter -->
+                        <groupId>org.opentest4j</groupId>
+                        <artifactId>opentest4j</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>com.google.code.findbugs</groupId>
+                        <artifactId>jsr305</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
                 <groupId>org.zeroturnaround</groupId>
                 <artifactId>zt-exec</artifactId>
                 <version>${zt-exec.version}</version>