This library is based on testcontainers which launches (docker)-containers providing test resources such as databases or – as in our case – proxy servers, to mock (potentially flaky) third party services.
The overall goal of this library is to simplify the process of recording and replaying
the responses of a third party service. It does so with a JUnit 5 extension
which processes launches an configures a proxy for every test annotated with @WithProxy
.
All library artifacts are released as github packages which can be found here:
<repositories>
<repository>
<id>github</id>
<name>GitHub Apache Maven Packages</name>
<url>https://maven.pkg.github.com/traum-ferienwohnungen/testcontainers-proxy</url>
</repository>
</repositories>
Since github requires authentication (with a token) they are also published on bintray:
<repositories>
<repository>
<id>bintray</id>
<name>JFrog Bintray Apache Maven Packages</name>
<url>https://dl.bintray.com/traum-ferienwohnungen/maven</url>
</repository>
</repositories>
This library assumes org.testcontainers:testcontainers
is available at runtime.
Therefore, the minimum set of dependencies looks as follows:
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.12.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.traum</groupId>
<artifactId>testcontainers-proxy-mountebank</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
To get integration with quarkus add the following:
<dependency>
<groupId>com.traum</groupId>
<artifactId>testcontainers-proxy-quarkus</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
The library provides a JUnit 5 extension which starts a (mountebank) proxy
for any method annotated with @WithProxy
. That annotation can be configured:
initialImposters
path to JSON file with initial imposters which the proxy is initialized with unlessreplayImposters
exists or is outdated (depending oninitPolicy
)replayImposters
path to JSON file which the recorded request-response-pairs are written to ready for replayrecordImposters
path to JSON file which the recorded request-response-pairs are written to with debugging detailsinitPolicy
the policy determining which set of imposters are used to initialize the proxy with
All paths, initialImposters
, replayImposters
, and recordImposters
, may contain placeholders, namely {method.name}
and {class.name}
,
which will be replaced with the test-method name and test-class name, respectively.
If @WithProxy
applied a class-level, the properties act as defaults at method-level.
However, you still have to explicitly annotate any test-method which needs a proxy.
See MountebankExtensionIT.java
@ExtendWith(MountebankExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@MountebankExtension.WithProxy(recordImposters = "target/proxy/{class.name}-{method.name}-debug.json")
class MountebankExtensionIT {
private static final String IMPOSTERS_OCTOCAT = "target/proxy/replay/github-users-octocat.json";
private static final String RESPONSE_OCTOCAT = "target/proxy/github-users-octocat-response.json";
private final HttpClient client = HttpClient.newBuilder().build();
@Test
@Order(1)
@MountebankExtension.WithProxy(
initialImposters = "src/test/resources/proxy/github-proxy.json",
replayImposters = IMPOSTERS_OCTOCAT,
initPolicy = MountebankExtension.WithProxy.InitPolicy.IF_REPLAY_NONEXISTENT)
void initialize(MountebankProxy proxy) throws IOException {
assertFalse(Files.exists(Paths.get(IMPOSTERS_OCTOCAT)));
final String previousBody = GET(URI.create(proxy.getUrl() + "/users/octocat")).join();
final Path responseBodyPath = Path.of(RESPONSE_OCTOCAT);
Files.createDirectories(responseBodyPath.getParent());
Files.write(responseBodyPath, previousBody.getBytes(), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
}
@Test
@Order(2)
@MountebankExtension.WithProxy(
initialImposters = "src/test/resources/proxy/github-proxy.json",
replayImposters = IMPOSTERS_OCTOCAT,
initPolicy = MountebankExtension.WithProxy.InitPolicy.IF_REPLAY_NONEXISTENT)
void nowReplay(MountebankProxy proxy) throws IOException {
assertTrue(Files.exists(Paths.get(IMPOSTERS_OCTOCAT)));
final String body = GET(URI.create(proxy.getUrl() + "/users/octocat")).join();
final String previousBody = Files.readString(Path.of(RESPONSE_OCTOCAT));
assertEquals(body, previousBody);
}
private CompletableFuture<String> GET(URI uri) {
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(uri)
.header("Accept", "application/vnd.github.v3+json")
.GET()
.build();
return client.sendAsync(getRequest, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
assertTrue(response.statusCode() >= 200 && response.statusCode() < 300);
return response.body();
});
}
@BeforeAll
static void cleanup() {
Stream.of(IMPOSTERS_OCTOCAT, RESPONSE_OCTOCAT).map(Path::of).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException("failed to delete file " + path, e);
}
});
}
}
First, extend MountebankProxyTestResourceLifecycleManager
and override start()
to obtain the proxy URL:
public class ProxyTestResource extends MountebankProxyTestResourceLifecycleManager {
/**
* Optionally pass additional imposter ports to expose them from the container.
* The default constructor implicitly enables port
* {@value com.traum.mountebank.MountebankProxy.DEFAULT_PROXY_PORT}.
*/
public ProxyTestResource() {
super(5050, 6060);
}
@Override
public Map<String, String> start() {
super.start();
return Map.of("api.url", "http://" + getProxy().getImposterAuthority(5050));
}
}
Then, use the class through @QuarkusTestResource
:
@QuarkusTest
@QuarkusTestResource(ProxyTestResource.class) // handles proxy lifecycle
@ExtendWith(MountebankExtension.class) // handles annotation processing
class SomeQuarkusTest {
@Test
@MountebankExtension.WithProxy(initialImposters = "path/to/initial-imposters.json")
void test() {
given()
.when().get("/resource-which-calls-proxied-service")
.then()
.statusCode(200)
.body(is("(proxied) response"));
}
}