Mock GRPC Services for Unit Testing

On our day to day work we develop applications that include interactions with software components through I/O. Can be a database, a broker or some form of blob storage. Take for example the Cloud Components you interact with: Azure Storage Queue, SQS, Pub/Sub. The communication with those components usually happens with an SDK.

From the start testing will kick in, thus the interaction with those components should be tackled in a testing context. An approach is to use installations (or simulators) of those components and have the code interacting with an actual instance, just like the way it can be achieved by using test containers or by creating infrastructure for testing purposes only.
Another approach is to spin up a mock service of the components and have the tests interacting with it. A good example of this can be Hoverfly. A simulated http service is run during testing and test cases interact with it.
Both can be used on various situations depending on the qualities our testing process requires. We shall focus on the second approach applied on gRPC.

It is well known that most Google Cloud Components come with a gRPC api.
In our scenario we have an application publishing messages to Pub/Sub.

Let’s put the needed dependencies first

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.google.cloud</groupId>
                <artifactId>libraries-bom</artifactId>
                <version>24.1.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-pubsub</artifactId>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-testing</artifactId>
            <version>1.43.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.api.grpc</groupId>
            <artifactId>grpc-google-cloud-pubsub-v1</artifactId>
            <version>1.97.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Let’s start with our publisher class.

package com.egkatzioura.notification.publisher;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.PubsubMessage;

public class UpdatePublisher {

    private final Publisher publisher;
    private final Executor executor;

    public UpdatePublisher(Publisher publisher, Executor executor) {
        this.publisher = publisher;
        this.executor = executor;
    }

    public CompletableFuture<String> update(String notification) {
        PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
                                                           .setData(ByteString.copyFromUtf8(notification))
                                                                   .build();
        ApiFuture<String> apiFuture = publisher.publish(pubsubMessage);

        return toCompletableFuture(apiFuture);
    }

    private CompletableFuture<String> toCompletableFuture(ApiFuture<String> apiFuture) {
        final CompletableFuture<String> responseFuture = new CompletableFuture<>();

        ApiFutures.addCallback(apiFuture, new ApiFutureCallback<>() {
            @Override
            public void onFailure(Throwable t) {
                responseFuture.completeExceptionally(t);
            }

            @Override
            public void onSuccess(String result) {
                responseFuture.complete(result);
            }

        }, executor);
        return responseFuture;
    }

}

The publisher will send messages and return the CompletableFuture of the message Id sent.
So let’s test this class. Our goal is to sent a message and get the message id back. The service to mock and simulate is PubSub.
For this purpose we added the grpc api dependency on maven

        <dependency>
            <groupId>com.google.api.grpc</groupId>
            <artifactId>grpc-google-cloud-pubsub-v1</artifactId>
            <version>1.97.1</version>
            <scope>test</scope>
        </dependency>

We shall mock the api for publishing actions. The class to implement is PublisherGrpc.PublisherImplBase.

package com.egkatzioura.notification.publisher;

import java.util.UUID;

import com.google.pubsub.v1.PublishRequest;
import com.google.pubsub.v1.PublishResponse;
import com.google.pubsub.v1.PublisherGrpc;

import io.grpc.stub.StreamObserver;

public class MockPublisherGrpc extends PublisherGrpc.PublisherImplBase {

    private final String prefix;

    public MockPublisherGrpc(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public void publish(PublishRequest request, StreamObserver<PublishResponse> responseObserver) {
        responseObserver.onNext(PublishResponse.newBuilder().addMessageIds(prefix+":"+UUID.randomUUID().toString()).build());
        responseObserver.onCompleted();
    }

}

As you see the message id will have a prefix we define.

This would be the PublisherGrpc implementation on the server side. Let us proceed to our unit test. The UpdatePublisher class can have a Publisher injected. This publisher will be configured to use the PublisherGrpc.PublisherImplBase created previously.

@Rule
public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();

private static final String MESSAGE_ID_PREFIX = "message";

@Before
public void setUp() throws Exception {
String serverName = InProcessServerBuilder.generateName();

Server server = InProcessServerBuilder
.forName(serverName).directExecutor().addService(new MockPublisherGrpc(MESSAGE_ID_PREFIX)).build().start();

grpcCleanup.register(server);
...

Above we created a GRPC server that services in-process requests. Then we registered the mock service created previously.
Onwards we create the Publisher using that service and create an instance of the class to test.

...

private UpdatePublisher updatePublisher;

@Before
public void setUp() throws Exception {
String serverName = InProcessServerBuilder.generateName();

Server server = InProcessServerBuilder
.forName(serverName).directExecutor().addService(new MockPublisherGrpc(MESSAGE_ID_PREFIX)).build().start();

grpcCleanup.register(server);

ExecutorProvider executorProvider = testExecutorProvider();
ManagedChannel managedChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build();

TransportChannel transportChannel = GrpcTransportChannel.create(managedChannel);
TransportChannelProvider transportChannelProvider = FixedTransportChannelProvider.create(transportChannel);

String topicName = "projects/test-project/topic/my-topic";
Publisher publisher = Publisher.newBuilder(topicName)
.setExecutorProvider(executorProvider)
.setChannelProvider(transportChannelProvider)
.build();

updatePublisher = new UpdatePublisher(publisher, Executors.newSingleThreadExecutor());
...

We pass a Channel to our publisher which points to our InProcessServer. Requests will be routed to the service we registered. Finally we can add our test.

@Test
public void testPublishOrder() throws ExecutionException, InterruptedException {
String messageId = updatePublisher.update("Some notification").get();
assertThat(messageId, containsString(MESSAGE_ID_PREFIX));
}

We did it! We created our in process gRPC Server in order to have tests for our gRPC driven services!

You can find the code on GitHub!

Advertisement

Testing with Hoverfly and Java Part 6: JSON and JsonPath matchers

Previously we used the XML and Xpath Hoverfly matchers.
On this blog we shall focus on rules that assist us with the data exchanged using Json.

The default Json matcher will compare the Json submitted with the Json expected. This means that the submitted Json shall be validated for all the elements and their value. New lines or any extra spaces as long as they don’t change the information that the JSON carries, will not prevent the request from being a success.

Let’s put our initial configuration that will make the Json match.

 


    @BeforeEach
    void setUp() {
        var simulation = SimulationSource.dsl(service("http://localhost:8085")
                .post("/json")
                .body(RequestFieldMatcher.newJsonMatcher("{\"document\":\"document-a\"}"))
                .willReturn(success(SUCCESS_RESPONSE, "application/json"))
                .post("/json/partial")
                .body(RequestFieldMatcher.newJsonPartialMatcher("{\"document\":\"document-a\"}"))
                .willReturn(success(SUCCESS_RESPONSE, "application/json"))
                .post("/jsonpath")
                .body(RequestFieldMatcher.newJsonPathMatch("$.document[1].description"))
                .willReturn(success(SUCCESS_RESPONSE, "application/json"))
        );

        var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
        hoverfly = new Hoverfly(localConfig, SIMULATE);
        hoverfly.start();
        hoverfly.simulate(simulation);
    }

    @AfterEach
    void tearDown() {
        hoverfly.close();
    }

    

In our first example we will try to match the Json of our request with the Json expected.

    @Test
    void testJsonExactMatch() {
        var client = HttpClient.newHttpClient();

        var exactRequest = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8085/json"))
                .POST(HttpRequest.BodyPublishers.ofString("   {\"document\":    \"document-a\"}"))
                .build();

        var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .join();

        Assertions.assertEquals(SUCCESS_RESPONSE, exactResponse);
    }

Also let’s make sure there is going to be a failure on an extra element.

    @Test
    void testJsonNoMatch() {
        var client = HttpClient.newHttpClient();

        var exactRequest = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8085/json"))
                .POST(HttpRequest.BodyPublishers.ofString("{\"doc2\":\"value\", \"document\":\"document-a\"}"))
                .build();

        var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
                .join();

        Assertions.assertEquals(502, exactResponse.statusCode());
    }

Now let’s see the non exact matcher.

    @Test
    void testJsonPartialMatch() {
        var client = HttpClient.newHttpClient();

        var exactRequest = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8085/json/partial"))
                .POST(HttpRequest.BodyPublishers.ofString("{\"doc2\":\"value\", \"document\":\"document-a\"}"))
                .build();

        var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .join();

        Assertions.assertEquals(SUCCESS_RESPONSE, exactResponse);
    }

So far we checked matching the whole payload. Let’s try the Jsonpath approach. The example below does match.

    @Test
    void testJsonPathMatch() {
        var client = HttpClient.newHttpClient();

        var exactRequest = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8085/jsonpath"))
                .POST(HttpRequest.BodyPublishers.ofString("{\"document\":[{\"description\":\"description-1\"},{\"description\":\"description-2\"}]}"))
                .build();

        var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .join();

        Assertions.assertEquals(SUCCESS_RESPONSE, exactResponse);
    }

But the example below won’t match


    @Test
    void testJsonPathNoMatch() {
        var client = HttpClient.newHttpClient();

        var exactRequest = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8085/jsonpath"))
                .POST(HttpRequest.BodyPublishers.ofString("{\"document\":[{\"description\":\"description-1\"}]}"))
                .build();

        var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
                .join();

        Assertions.assertEquals(502, exactResponse.statusCode());
    }

That’s it we did use the Json and JsonPath matchers for the Json based data!

Testing with Hoverfly and Java Part 5: XML and Xpath matchers

Previously we worked with some of the existing Hoverfly matchers like the regex, glob and exact.
Each one serves its purpose but we might want some rules that assist us with the format of the data exchanged through our requests.

On this blog we will focus on the matchers for xml.

 

The default xml matcher will compare the xml submitted with the xml expected. This means that the submitted xml shall be validated node by node value by value. New lines or any extra spaces as long as they don’t change the content that the xml carries will not prevent the request from being a success.

Let’s put our initial configuration that will make the xml match.

	public static final String SUCCESS_RESPONSE = "<response>"
			+ "<result>success</result>"
			+ "</response>";

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.post("/xml")
				.body(RequestFieldMatcher.newXmlMatcher("<document type=\"xml\">"
						+ "xml-request"
						+ "</document>"))
				.willReturn(success(SUCCESS_RESPONSE, "application/xml")));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

So in our first example we will try to match the xml of our request with the xml expected.

	@Test
	void testXmlExactMatch() {
		var client = HttpClient.newHttpClient();

		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/xml"))
				.POST(HttpRequest.BodyPublishers.ofString("  <document type=\"xml\">\n\n"
						+ "xml-request"
						+ "</document>\t"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();

		Assertions.assertEquals(SUCCESS_RESPONSE, exactResponse);
	}

As you see regardless of the new lines and the tabs, our request will be successful since the xml data do match.

Now let’s try to add a node to the xml.

	@Test
	void testXmlNoMatch() {
		var client = HttpClient.newHttpClient();

		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/xml"))
				.POST(HttpRequest.BodyPublishers.ofString("  <document type=\"xml\">\n\n"
						+ "xml-request"
						+ "</document>\t<empty-node>ok</empty-node>"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.join();

		Assertions.assertEquals(502, exactResponse.statusCode());
	}

The xml does not match thus it will fail.

Let’s focus to another problem. Since the data exchanged are dynamic, chances are that exact matches might not be possible. Also you might not need to focus on all the information submitted but just a specific section of the information exchanged. Therefore an XPath matcher becomes handy.

Will enhance the initial setup with an XPath rule.


	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.post("/xml")
				.body(RequestFieldMatcher.newXmlMatcher("<document type=\"xml\">"
						+ "xml-request"
						+ "</document>"))
				.willReturn(success(SUCCESS_RESPONSE, "application/xml"))
				.post("/xpath")
				.body(RequestFieldMatcher.newXpathMatcher("/document/payment[amount=1]"))
				.willReturn(success(SUCCESS_RESPONSE, "application/xml"))
		);

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

If there is a document node with a payment node and the value on the amount node is 1 there will be a match
Let’s go for a positive scenario

	@Test
	void testXpathMatch() {
		var client = HttpClient.newHttpClient();

		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/xpath"))
				.POST(HttpRequest.BodyPublishers.ofString("  <document type=\"xml\">\n\n"
						+ "<payment><amount>142</amount></payment>"
						+ "<payment><amount>1</amount><currency>GBP</currency></payment>"
						+ "<payment>invalid</payment>"
						+ "</document>\t"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();

		Assertions.assertEquals(SUCCESS_RESPONSE, exactResponse);
	}

As expected we got a match.
Let’s go for a negative scenario.

	@Test
	void testXpathNoMatch() {
		var client = HttpClient.newHttpClient();

		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/xpath"))
				.POST(HttpRequest.BodyPublishers.ofString("  <document type=\"xml\">\n\n"
						+ "<payment><amount>142</amount></payment>"
						+ "<payment><amount>no-match</amount><currency>GBP</currency></payment>"
						+ "<payment>invalid</payment>"
						+ "</document>\t"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.join();

		Assertions.assertEquals(502, exactResponse.statusCode());
	}

That’s it we did use the xml and xpath matchers for the xml based data. The next blog shall focus on the JSON based matchers.

Testing using TestContainers

Part of our everyday ci/cd tasks involve using containers in order for the tests to take effect.
So what if you could control the containers you use through your tests and serve your scenarios better.
Also what if you could do this in a more managed way?

Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

You pretty much can guess what is all about. Our tests can spin up the containers with the parameters needed. We will get started by using it in our tests with Junit.

It all starts with the right dependencies. Supposing we use maven for this tutorial.

	<properties>
		<junit-jupiter.version>5.4.2</junit-jupiter.version>
		<testcontainers.version>1.15.0</testcontainers.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>testcontainers</artifactId>
			<version>${testcontainers.version}</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>${testcontainers.version}</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

I shall use an example we already have with Hoverfly.
We can use Hoverfly on our tests either by running it using Java or having a Hoverfly container with the test cases preloaded.
On the previous blog Hoverfly was integrated in our tests through the Java binary.
For this blog we shall use the Hoverfly container.

Our end result will look like this.

package com.gkatzioura.hoverfly.docker;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public class ContainerBasedSimulation {

	private static final String SIMULATION_HOST_PATH = ContainerBasedSimulation.class.getClassLoader().getResource("simulation.json").getPath();

	@Container
	public static GenericContainer gcs = new GenericContainer("spectolabs/hoverfly")
			.withExposedPorts(8888)
			.withExposedPorts(8500)
			.withCommand("-webserver","-import","/var/hoverfly/simulation.json")
			.withClasspathResourceMapping("simulation.json","/var/hoverfly/simulation.json" ,BindMode.READ_ONLY);


	@Test
	void testHttpGet() {
		var hoverFlyHost = gcs.getHost();
		var hoverFlyPort = gcs.getMappedPort(8500);
		var client = HttpClient.newHttpClient();
		var request = HttpRequest.newBuilder()
				.uri(URI.create("http://"+hoverFlyHost+":"+ hoverFlyPort +"/user"))
				.build();
		var res = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-user\"}",res);
	}

}

Let’s break it down.

The @Testcontainers annotation is needed for the Jupiter integration.

@Testcontainers
public class ContainerBasedSimulation {
}

We shall use a container image that is not preloaded among the test containers available (for example Elastic Search), thus we shall use the GenericContainer class.

@Container
public static GenericContainer gcs = new GenericContainer("spectolabs/hoverfly")

Since we want to load to the container a simulation, we need to set the path to our simulation from our host machine. By using withClasspathResourceMapping we directly specify files in our classpath, for example the test resources.

			.withClasspathResourceMapping("simulation.json","/var/hoverfly/simulation.json",BindMode.READ_ONLY);

Hoverfly needs the simulation and the admin port to be exposed so we shall instruct Testcontainers to expose those ports and map them to host.

new GenericContainer("spectolabs/hoverfly")
			.withExposedPorts(8888)
			.withExposedPorts(8500)

We need to have a simulation placed on the container. By using withFileSystemBind we specify the local path and the path on the container.

...
.withFileSystemBind(SIMULATION_HOST_PATH,"/var/hoverfly/simulation.json" ,BindMode.READ_ONLY)
...

Also docker images might need to have some extra commands, therefore we shall use .withCommand, to pass the commands needed.

...
.withCommand("-webserver","-import","/var/hoverfly/simulation.json")
...

Technically we can say we are ready to go and connect to the container, however when running test containers the container is not accessible through the port specified to do the binding. After all, if tests run on parallel there is going to be a collision. So what Testcontainers do is to map the exposed port of the container to a random local port.
This way port collisions are avoided.

	@Test
	void testHttpGet() {
		var hoverFlyHost = gcs.getHost();
		var hoverFlyPort = gcs.getMappedPort(8500);
		var client = HttpClient.newHttpClient();
		var request = HttpRequest.newBuilder()
				.uri(URI.create("http://"+hoverFlyHost+":"+ hoverFlyPort +"/user"))
				.build();
		var res = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-user\"}",res);
	}

Using GenericContainer.getMappedPort(8500) we can get the port we have to use to interact with the container. Also getHost() is essential too since it won’t always direct to localhost.

Last but not least while testing if your are curious enough and do a docker ps.

docker ps 
>04a322447226        testcontainers/ryuk:0.3.0   "/app"                   3 seconds ago       Up 2 seconds        0.0.0.0:32814->8080/tcp    testcontainers-ryuk-fb60c3c6-5f31-4f4e-9ab7-ce25a00eeccc

You shall see a container running which is not the one we instructed through our unit test. The ryuk container is responsible for removing containers/networks/volumes/images by given filter after specified delay.

That’s it! We just achieved running the container we needed through our a test and we successfully migrated a previous test to one using test containers.

Testing with Hoverfly and Java Part 4: Exact, Glob and Regex Matchers

Previously we used Hoverfly among its state feature.
So far our examples have been close to an absolute request match, thus on this blog we will focus on utilising the matchers.
Having a good range of matchers is very important because most API interactions are dynamic and you can’t always predict the example. Imagine a JWT signature. You can match the body but the signature might change per environment.

 

There are three type of matchers available.

  • The exact matcher: The fields headers should be an exact match
  • The glob matcher: A match that gives the ability to using the `*`
  • The regex matcher: A matcher that requires you to search again on the internet on how to make a regex
  • XML matcher: This about matching the xml as XML node by node value by value
  • Xpath matcher: Match based on a value match through Xpath
  • JSON matcher: Match the json exactly
  • JSON partial matcher: Match if the json submitted contains the Json values specified
  • JSONPath matcher: Just like the xpath match based the on the json path submitted

Let’s start with the exact matcher.

public class ExactMatcherTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/exact")
				.header("Origin", RequestFieldMatcher.newExactMatcher("internal-server"))
				.willReturn(success("{\"exact\":true}", "application/json")));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

	@Test
	void testExactMatcherSuccess() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/exact"))
				.header("Origin","internal-server")
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();

		Assertions.assertEquals("{\"exact\":true}", exactResponse);
	}

	@Test
	void testExactMatcherFailure() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/exact"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.join();

		Assertions.assertEquals(502, exactResponse.statusCode());
	}

}

The failures or success come based on wether the header was matching exactly or not.

We shall use the glob match for a request parameter.


public class GlobMatcher {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/glob")
				.queryParam("userName", RequestFieldMatcher.newGlobMatcher("john*"))
				.willReturn(success("{\"glob\":true}", "application/json")));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

	@Test
	void testGlobMatcherSuccess() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/glob?userName=johnDoe"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();

		Assertions.assertEquals("{\"glob\":true}", exactResponse);
	}

	@Test
	void testGlobMatcherFailure() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/glob?userName=nojohnDoe"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.join();

		Assertions.assertEquals(502, exactResponse.statusCode());
	}

}

Last let’s head for the regex matcher. The regex matcher will just check for a capital letter: ([A-Z])\w+

public class RegexMatcherTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.post("/regex")
				.body(RequestFieldMatcher.newRegexMatcher("([A-Z])\\w+"))
				.willReturn(success("{\"regex\":true}", "application/json")));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

	@Test
	void testRegexMatcherSuccess() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/regex"))
				.POST(HttpRequest.BodyPublishers.ofString("Contains capital letter"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();

		Assertions.assertEquals("{\"regex\":true}", exactResponse);
	}

	@Test
	void testRegexMatcherFailure() {
		var client = HttpClient.newHttpClient();
		var exactRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/regex"))
				.POST(HttpRequest.BodyPublishers.ofString("won't match due to capital letter missing"))
				.build();

		var exactResponse = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
				.join();

		Assertions.assertEquals(502, exactResponse.statusCode());
	}

}

That’s it we did use the basic matchers for exact, glob, and regex based. The next blog shall focus on the xml based matchers.

Run a docker PostgreSQL instance locally for Testing

Running a PostgreSQL instance ad-hoc for testing is not always as bootstraping as it should be. This blog will run a PostgreSQL instance that connects to your workstation’s network and instead of using one of the popular tools like dbeaver we shall use the client that comes with the instance. Also we shall run a bootstrap script to have some data pre-inserted.

Let’s get started by running the instance. On purpose I will use another port. On scenarios of multiple instances running in your workstation, port collisions are likely. The workaround would be to choose port 5433.

docker run --rm --name test-instance -e POSTGRES_PASSWORD=password -p 5433:5432 postgres

This will run PostgreSQL and you shall be able to connect to port 5433. On a CTRL-C the instance will be stopped and destroyed.

Now instead of using an external tool to connect let’s use the instance itself, it comes with psql pre-installed.

docker exec -it test-instance /bin/bash
> psql postgres postgres
postgres=# \h
Available help:
  ABORT                            ALTER TRIGGER                    CREATE RULE                      DROP GROUP                       LISTEN
  ALTER AGGREGATE                  ALTER TYPE                       CREATE SCHEMA                    DROP INDEX                       LOAD
  ALTER COLLATION                  ALTER USER                       CREATE SEQUENCE                  DROP LANGUAGE                    LOCK
.....
postgres=# \q

The instance works and connections from the outside are possible.

Next step would be to bootstrap a db initialization script.

#!/bin/bash
set -e
 
psql -v ON_ERROR_STOP=1 --username postgres --dbname postgres <<-EOSQL
    create schema test_schema;
 
    create table test_schema.employee(
        id  SERIAL PRIMARY KEY,
        firstname   TEXT    NOT NULL,
        lastname    TEXT    NOT NULL,
        email       TEXT    not null,
        age         INT     NOT NULL,
        salary         real,
        unique(email)
    );
 
    insert into test_schema.employee (firstname,lastname,email,age,salary)
    values ('John','Doe 1','john1@doe.com',18,1234.23);

EOSQL

Supposing the file with the script is called init_db.sh

Let’s run the command with the initialization schema mounted.

docker run --rm --name test-instance -v /path/to/init_db.sh:/docker-entrypoint-initdb.d/init-db-script.sh -e POSTGRES_PASSWORD=password -p 5433:5432 postgres

And let’s check the results.

docker exec -it test-instance /bin/bash
>psql postgres postgres
postgres=# SELECT*FROM test_schema.employee;
 id | firstname | lastname |     email     | age | salary
----+-----------+----------+---------------+-----+---------
  1 | John      | Doe 1    | john1@doe.com |  18 | 1234.23
(1 row)

That’s it! You created a Postgresql database through docker, you did connect to it also you added a bootstrap script with data.

Testing with Hoverfly and Java Part 3: State

Previously we simulated a delay scenario using Hoverfly. Now it’s time to dive deeper and go for a state based testing. By doing a stateful simulation we can change the way the tests endpoints behave based on how the state changed.

 

Hoverfly does have a state capability. State in a hoverfly simulation is like a map. Initially it is empty but you can define how it will get populated per request.

Our strategy would be to have a request that initializes the state and then specifies other requests that change that state.

public class SimulationStateTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/initialize")
				.willReturn(success("{\"initialized\":true}", "application/json")
						.andSetState("shouldSucceed", "true")
				)
				.get("/state")
				.withState("shouldSucceed", "false")
				.willReturn(serverError().andSetState("shouldSucceed", "true"))
				.get("/state")
				.withState("shouldSucceed", "true")
				.willReturn(success("{\"username\":\"test-user\"}", "application/json")
						.andSetState("shouldSucceed", "false"))

		);

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

}

Unfortunately on the state we can only specify values in a key value fashion and not by passing a function for a key.
However with the right workaround many scenarios could be simulated.

In the example we first initialize the state and the we issue requests that behave differently based on the state, but also they do change the state.

So we expect to have a continuous first succeed and then fail mode, which can be depicted in the following test.

	@Test
	void testWithState() {
		var client = HttpClient.newHttpClient();
		var initializationRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/initialize"))
				.build();
		var initializationResponse = client.sendAsync(initializationRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"initialized\":true}", initializationResponse);

		var statefulRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/state"))
				.build();

		for (int i = 0; i < 100; i++) {
			var response = client.sendAsync(statefulRequest, HttpResponse.BodyHandlers.ofString())
					.join();

			int statusCode = i % 2 == 0 ? 200 : 500;

			Assertions.assertEquals(statusCode, response.statusCode());
		}
	}

That’s all about stateful simulation. On the next part we shall proceed on Hoverfly matchers

Testing with Hoverfly and Java Part 2: Delays

On the previous post we implemented json and Java based Hoverfly scenarios..
Now it’s time to dive deeper and use other Ηoverfly features.

A big part of testing has to do with negative scenarios. One of them is delays. Although we always mock a server and we are successful to reproduce erroneous scenarios one thing that is key to simulate in todays microservices driven world is delay.

So let me make a server with a 30 secs delay.

public class SimulateDelayTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/delay")
				.willReturn(success("{\"username\":\"test-user\"}", "application/json").withDelay(30, TimeUnit.SECONDS)));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

}

Let’s add the Delay Test

@Test
void testWithDelay() {
   var client = HttpClient.newHttpClient();
   var request = HttpRequest.newBuilder()
         .uri(URI.create("http://localhost:8085/delay"))
         .build();
   var start = Instant.now();
   var res = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
         .thenApply(HttpResponse::body)
         .join();
   var end = Instant.now();
   Assertions.assertEquals("{\"username\":\"test-user\"}", res);

   var seconds = Duration.between(start, end).getSeconds();
   Assertions.assertTrue(seconds >= 30);
}

Delay simulation is there, up and running, so let’s try to simulate timeouts.

	@Test
	void testTimeout() {
		var client = HttpClient.newHttpClient();
		var request = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/delay"))
				.timeout(Duration.ofSeconds(10))
				.build();
		assertThrows(HttpTimeoutException.class, () -&gt; {
					try {
						client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join();
					} catch (CompletionException ex) {
						throw ex.getCause();
					}
				}

		);
	}

That’s it we got delays and timeouts!
Other test scenarios should contain state which is covered on the next tutorial.

Testing with Hoverfly and Java Part 1: Get started with Simulation Mode

These days a major problem exists when it comes to testing code that has to do with various cloud services where test tools are not provided.
For example although you might have the tools for local Pub/Sub testing, including Docker images you might not have anything that can Mock BigQuery.

This causes an issue when it comes to the CI jobs, as testing is part of the requirements, however there might be blockers on testing with the actual service. The case is, you do need to cover all the pessimistic scenarios you need to be covered (for example timeouts).

And this is where Hoverfly can help.

Hoverfly is a lightweight, open source API simulation tool. Using Hoverfly, you can create realistic simulations of the APIs your application depends on

Our first examples will have to do with simulating just a web server. The first step is to add the Hoverfly dependency.

    <dependencies>
        <dependency>
            <groupId>io.specto</groupId>
            <artifactId>hoverfly-java</artifactId>
            <version>0.12.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Instead of using the Hoverfly docker image we shall use the Java Library for some extra flexibility.

We got two options on configuring the Hoverfly simulation mode. One is through the Java dsl and the other one is through json.
Let’s cover both.

The example below uses the Java DSL. We spin up hoverfly on 8085 and load this configuration.

class SimulationJavaDSLTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/user")
				.willReturn(success("{\"username\":\"test-user\"}", "application/json")));

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

	@Test
	void testHttpGet() {
		var client = HttpClient.newHttpClient();
		var request = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/user"))
				.build();
		var res = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-user\"}",res);
	}
}

Now let’s do the same with Json. Instead of manually trying things with json we can make the code do the work for us.

var simulation = SimulationSource.dsl(service("http://localhost:8085")
			.get("/user")
			.willReturn(success("{\"username\":\"test-user\"}", "application/json")));

var simulationStr = simulation.getSimulation()
System.out.println(simulationStr);

We can get the JSON generated by the Java DSL. The result would be like this.

{
  "data": {
    "pairs": [
      {
        "request": {
          "path": [
            {
              "matcher": "exact",
              "value": "/user"
            }
          ],
          "method": [
            {
              "matcher": "exact",
              "value": "GET"
            }
          ],
          "destination": [
            {
              "matcher": "exact",
              "value": "localhost:8085"
            }
          ],
          "scheme": [
            {
              "matcher": "exact",
              "value": "http"
            }
          ],
          "query": {},
          "body": [
            {
              "matcher": "exact",
              "value": ""
            }
          ],
          "headers": {},
          "requiresState": {}
        },
        "response": {
          "status": 200,
          "body": "{\"username\":\"test-user\"}",
          "encodedBody": false,
          "templated": true,
          "headers": {
            "Content-Type": [
              "application/json"
            ]
          }
        }
      }
    ],
    "globalActions": {
      "delays": []
    }
  },
  "meta": {
    "schemaVersion": "v5"
  }
}

Let’s place this one on the resources folder of tests under the name simulation.json

And with some code changes we get exactly the same result.


public class SimulationJsonTests {

	private Hoverfly hoverfly;

	@BeforeEach
	void setUp() {
		var simulationUrl = SimulationJsonTests.class.getClassLoader().getResource("simulation.json");
		var simulation = SimulationSource.url(simulationUrl);

		var localConfig = HoverflyConfig.localConfigs().disableTlsVerification().asWebServer().proxyPort(8085);
		hoverfly = new Hoverfly(localConfig, SIMULATE);
		hoverfly.start();
		hoverfly.simulate(simulation);
	}

	@AfterEach
	void tearDown() {
		hoverfly.close();
	}

	@Test
	void testHttpGet() {
		var client = HttpClient.newHttpClient();
		var request = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/user"))
				.build();
		var res = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-user\"}",res);
	}

}

Also sometimes there is the need of combining simulations regardless they json or Java ones. This can also be facilitated by loading more that one simulations.

	@Test
	void testMixedConfiguration() {
		var simulationUrl = SimulationJsonTests.class.getClassLoader().getResource("simulation.json");
		var jsonSimulation = SimulationSource.url(simulationUrl);


		var javaSimulation = SimulationSource.dsl(service("http://localhost:8085")
				.get("/admin")
				.willReturn(success("{\"username\":\"test-admin\"}", "application/json")));

		hoverfly.simulate(jsonSimulation, javaSimulation);

		var client = HttpClient.newHttpClient();
		var jsonConfigBasedRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/user"))
				.build();
		var userResponse = client.sendAsync(jsonConfigBasedRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-user\"}",userResponse);

		var javaConfigBasedRequest = HttpRequest.newBuilder()
				.uri(URI.create("http://localhost:8085/admin"))
				.build();
		var adminResponse = client.sendAsync(javaConfigBasedRequest, HttpResponse.BodyHandlers.ofString())
				.thenApply(HttpResponse::body)
				.join();
		Assertions.assertEquals("{\"username\":\"test-admin\"}",adminResponse);
	}

That’s it, we are pretty setup to continues exploring Hoverfly and it’s capabilities. The next blog is about delays.

Testing has become Mandatory (and there are no more excuses)

There are many posts out there about the value of automated testing and why it is a must through the life cycle of a software product.

Most people and teams totally agree with this statement, so what goes wrong?

It is just a prototype

It seems to be a valid statement, but if we consider how many times a prototype made it into production then it is not so valid.
In fact most prototype codebase ends up to production because this is the goal of a prototype.
Adding tests in a prototype phase makes absolutely sense.

Too difficult to test

Yes there cases where testing is really difficult due to limitations. For example the android and he iphone emulators do not give you the ability
to set accelerometer events, or you use a service which provides you with no test utilities at all, or a testing environment.
Even if mocking would not make an absolute test case, it really can assist you on making a specification on how things work.
The other scenario is testing being hard to implement due to the codebase.
Consider this as an indicator that things are not simple enough. If it is just your codebase then your are lucky, It is up to you to make it more testable and more simple.

Too much to do, too little time

This is our weakest spot and the one that we are most prone too succumb.
Suppose you develop a smartphone application. In case of manual testing each mistake will cost you 3-5 minutes. Starting the emulator or even worse deploy to a physical device, load the application and press some buttons to create events, and in case of an error repeat and loose other 3-5 minutes.
In case you develop a server application things might get even worse. Connect to the sever, upload the application, check that that the upload was successful and then manually test that it works ok, and in case of an error repeat and loose some time again.
Also keep in mind that in case of projects with more than one developers involved, mistakes are more common to happen.
We tend to believe that the best case will happen where everything will work as expected.
It might work most of the time but in case of a problem you loose big. We end up being hooked on an “It’s ok all it needs is another version upload” mode.
Next time this happens just count how much time you end up loosing by testing manually until everything is ok.
Then estimate how much time tests will cost you and how much time you win in such cases.

Everything changes so rapidly

Yes a project on its initial phase, will have a completely different codebase in two months than its initial one.
But since big changes are made more errors are likely to occur. A minimal amount of tests ensures that basic features would continue to work.

All in all testing is not as hard as it used to be.
Nowadays almost every service or utility that we use comes with some test utils.
As the software industry becomes more challenging, codebases without tests would become extinct.
Pick whatever methodology you want but just do it. Your life will become much easier.