Component Test Automation in Spring Boot WebFlux Microservices

Hi all, in this article, we will learn how to create a component test automation solution for Spring Boot WebFlux Microservices. Let’s get started! :)

Prerequisite: This is not an entry-level article. Knowing the project reactor (spring web flux) and spring boot is required.

As you can see microservices testing types in the diagram below, component testing is a part of the Microservices Testing Strategy and is one of the most important parts. We should test all business functionality in this layer by using mocks/stubs.

We can implement component tests for Spring Boot WebFlux projects with the WebTestClient library, and we are using the Wiremock tool for the mocking. Component testing is a functional testing type, and with component tests, we are testing the core functionality of the services based on business requirements.

The low-level tests should be covered in Unit Tests as much as possible. We should focus on business scenarios and requirements in component testing as much as possible.

As seen in the diagram below, we are mocking all service integrations in component tests. In this way, we isolate the service from the real integration systems. Using webtestclient’s autoconfiguration implementation, we start the service on localhost, then send requests to the service under test and check the results with our expectations. Based on each request, we should mock the interactions between the service and its integrations.

I highly suggest running the component tests as early as possible in your pipelines, especially PR pipelines, before merging your changes to the master/main branch. You can see the tests in your PR pipelines in the below order.

Unit Tests ->

Integration Tests ->

Contract Tests ->

Component Tests ->

Quality Checks/ Dependency Checks/ Security & Vulnerability Checks / Microservice Level Performance Checks, etc.

This way, you can detect the functional and behavioral problems as early as possible and filter the bugs to go to the main branch. This way, you will see fewer problems in your CD (Deployment) pipelines and in the later stages of the Software Development Life Cycle (SDLC).

After this mini introduction and guidance, you may think about when we will start to implement a component test solution and make our hands dirty with some code. :) Let’s start with our solution architecture, then I will show some codes.

The Solution Architecture

To have a modularized structure, we should use multiple libraries to reduce the complexity of our solution. It is wise to have a core library for tests, and we can keep common things for all service testing in this core library. We can keep the following things inside this library:

  • Converters (Serializer/Deserializers)
  • DTOs 
  • Utilities (HeaderUtils, QueryParam Utils, WebTestClient Utils, XML Utils, YAML utils, JSON utils, DB Utils, etc.)
  • Data Builders

In the microservices’ pom.xml file, we need to add these dependencies to reach the test core functionalities. You need to have your test core’s dependency. 

<dependency>
    <groupId>com.yourprojectname.test.core</groupId>
    <artifactId>test-core</artifactId>
    <version>${test.core.version}</version>
    <scope>test</scope>
</dependency>

We can use either Cucumber or similar libraries to add Gherkin flavor to our test codes or do this with vanilla Java. 

We have three main keywords here. These are Given, When, and Then. In the Given methods, we prepare data and do pre-conditions. In the When methods, we do an action such as sending requests to the service with prepared data, and in the Then methods, we do assertions and verifications. A sample and simple test code look like below.

@Test
@DisplayName("I GET a response from the service successfully.")
void iGetResponseSuccessfully() {
    steps
        .givenIHaveARequest(headers.defaultHeaders.get())
        .whenIGet()
        .thenIRetrieveResponseSuccessfully();
}

Component Test Code Structure

Inside the microservices project repository, under the test package, I suggest creating a ct (Component Tests) package and having the below sub-folders. As you can see in the code structure, we create test data like Headers, Query Parameters, Payloads, and form request objects in the data package. We write service-specific error messages in the expectations package. In the Steps package, we write all details of the tests in the Given, When, Then classes. The mocks package contains stubs implementations, and we write our component tests in the tests package.

Data: Headers, Query Parameters, Payloads, Request Objects, etc.

Expectations: Service-specific expected error messages.

Config: Test specific configurations.

Assertions: Service-specific assertions.

Steps: Given, When, Then steps of component tests.

Mocks: Mocks implementations.

Tests: We write tests in the tests package.

Component Test Annotation 

On top of component tests, we use ComponentTest annotation. In ComponentTest annotation, we declare the active profile as the “component,” AutoConfigureWebtestClient (for creating the test client automatically) with a proper timeout, and AutoConfigureWiremock (for mocking external dependencies and systems) with an open and unused port number.

/**
 * In order to run the tests on specific port.
 *
 * @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
 * properties = { "server.port=8042","management.server.port=9042" }, classes = Application.class)
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("ComponentTest")
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class)
@ActiveProfiles(value = "component", resolver= SystemPropertyActiveProfileResolver.class) 
@AutoConfigureWebTestClient(timeout = "90000")
@AutoConfigureWireMock(port = 4089)
@Import(ElapsedTimeAspect.class) //This is a custom aspect oriented programming annotation. You do not need it.
public @interface ComponentTest {
}

Here, we have application-component.yaml file for the component test configuration under test resources folder.

To run the component tests via maven commands with a specific profile, you may need SystemPropertyActiveProfileResolver class. This is completely optional. I will share its content below.

public class SystemPropertyActiveProfileResolver implements ActiveProfilesResolver {
    private final DefaultActiveProfilesResolver defaultActiveProfilesResolver = new DefaultActiveProfilesResolver();

    @Override
    public String[] resolve(Class<?> testClass) {
        final String springProfileKey = "spring.profiles.active";

        return System.getProperties().containsKey(springProfileKey)
            ? System.getProperty(springProfileKey).split("\\s*,\\s*")
            : defaultActiveProfilesResolver.resolve(testClass);
    }
}

Also, here I used custom Aspect-Oriented-Programming annotation @ElapsedTime. This is also optional, but if you want to have this option, you need to add the spring aop dependency in your pom.xml and then add the below classes to your test core or service’s project. I preferred to keep this in the test core library because it is a common function for all service tests.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.0.0</version>
</dependency>
@Documented
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ElapsedTime {
}
@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Generated {
}
@Aspect
@Configuration
@Slf4j
@Generated
public class ElapsedTimeAspect {
    @Around("@annotation(elapsedTime)")
    public Object around(final ProceedingJoinPoint proceedingJoinPoint, final ElapsedTime elapsedTime) throws Throwable {
        final long startTime = System.currentTimeMillis();
        final Object obj = proceedingJoinPoint.proceed();
        final long duration = System.currentTimeMillis() - startTime;
        log.info("Elapsed time of {} class's {} method is {}", proceedingJoinPoint
                .getSignature()
                .getDeclaringTypeName(),
            proceedingJoinPoint
                .getSignature()
                .getName(), duration + " ms.");
        return obj;
    }
}

Base Steps 

We should declare all instances common for the Given, When, and Then steps. A sample base steps class is shown below. (Note: @Data is a Lombok annotation that contains both @Getter and @Setter annotations.)

@Data
public abstract class Base<T extends Base<T>> {
    @Autowired
    @Qualifier("ctWebTestClient")
    WebTestClient ctWebTestClient;
    @Autowired
    @Qualifier("ctPayloads")
    Payloads      payloads;
    @Autowired RequestObjectBuilder requestObjectBuilder;
    @Autowired Headers              headers;

    WebTestClient.ResponseSpec responseSpec;
    SkeletonResponseDTO        response;

    public ResponseDomainErrorDTO getErrors() {
        return getErrorObjectFromResponse(responseSpec);
    }

    public abstract T getThis();
}

Here, I used getErrorObjectFromResponse method from the test core library. It is a conversion method, and its content is as follows.

public static ResponseDomainErrorDTO getErrorObjectFromResponse(final WebTestClient.ResponseSpec response) {
    return response
        .expectBody(ResponseDomainErrorDTO.class)
        .returnResult()
        .getResponseBody();
}

The ResponseDomainErrorDTO is our error DTO class, which can be different for your services. In our case, it looks like this.

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseDomainErrorDTO {
    private String             errorMessage;
    private Collection<String> errorRecords;
    private String             errorCode;
}

SkeletonResponseDTO is the response DTO class, and it is service specific response DTO class. We should convert the WebTestClient. ResponseSpec to this ResponseDTO class, and for this, we will use the below method, which is also inside the test core project. Do not forget, we should put all common methods inside the test core project.

public static <T> T getObjectFromResponse(final WebTestClient.ResponseSpec response, final Class<T> type) {
    return response
        .expectBody(type)
        .returnResult()
        .getResponseBody();
}

Given Steps 

In the Given class, we prepare our requests and do prerequisite operations based on our service details and business requirements. A sample Given steps class is shown below.

public abstract class Given<T extends Given<T>> extends Base<T> {
    public RequestObjectDTO requestObject;
    
    @Step
    public T givenIHaveARequest(Consumer<HttpHeaders> headersConsumer) {
        requestObject = requestObjectBuilder.buildRequestObject(headersConsumer);
        return getThis();
    }

    @Step
    public T givenIHaveARequest(Consumer<HttpHeaders> headersConsumer, String payload) {
        requestObject = requestObjectBuilder.buildRequestObject(headersConsumer, payload);
        return getThis();
    }

    @Step
    public T givenIHaveARequestWithBadVersion(String version) {
        requestObject = new RequestObjectDTO()
                .headersConsumer(headers.defaultHeaders.get())
                .version(version);
        return getThis();
    }
}

As you saw in the Given class, we use RequestObjectDTO and RequestObjectBuilder methods. I keep the RequestObjectDTO in the test core library and RequestObjectBuilder in the project repo. 

@Getter
@Setter
@Accessors(fluent = true)
public class RequestObjectDTO {
    private String version = "v1";
    private String url;
    private String resourcePath = "";
    private MultiValueMap<String, String> queryParams;
    private Consumer<HttpHeaders> headersConsumer;
    private String body;
}

You can create request object builder methods based on your service requirements. You can see some examples in the code below.

@TestComponent
public class RequestObjectBuilder {
    /**
     * Based on your requirements you can create methods which takes query params, resource path, body etc.
     */
    public RequestObjectDTO buildRequestObject(Consumer<HttpHeaders> headersConsumer) {
        return new RequestObjectDTO()
            .headersConsumer(headersConsumer)
            .resourcePath("/cttest");
    }

    public RequestObjectDTO buildRequestObjectForVersion2(Consumer<HttpHeaders> headersConsumer, String documentId) {
        return new RequestObjectDTO()
            .headersConsumer(headersConsumer)
            .resourcePath("/" + "ct" + documentId)
            .version("v2");
    }

    public RequestObjectDTO buildRequestObjectWithPayload(Consumer<HttpHeaders> headersConsumer, String payload) {
        return new RequestObjectDTO()
            .headersConsumer(headersConsumer)
            .resourcePath("/" + "ct")
            .body(payload);
    }
}

When Steps 

We send requests to our service in the When class using GET, POST, PUT, and DELETE method calls from the test core library. I will also share the code snippets of the core library in the next section of this article. Here, I also used Aspect-Oriented-Programming with @ElapsedTime aspect annotation. I may create one article for this in the future. @Step is allure reports annotation.

In this class, I used the WebTestClient Util class which is in the test core library. As I mentioned in this article, we should keep the common classes and utilities in the test core library.

public interface WebTestClientUtil {
    BiFunction<WebTestClient, RequestObjectDTO, WebTestClient.ResponseSpec> post = (webTestClient, requestObjectDTO) ->
        webTestClient
            .post()
            .uri(buildURI(requestObjectDTO))
            .headers(requestObjectDTO.headersConsumer())
            .body(fromValue(requestObjectDTO.body()))
            .exchange();

    BiFunction<WebTestClient, RequestObjectDTO, WebTestClient.ResponseSpec> put = (webTestClient, requestObjectDTO) ->
        webTestClient
            .put()
            .uri(buildURI(requestObjectDTO))
            .headers(requestObjectDTO.headersConsumer())
            .body(fromValue(requestObjectDTO.body()))
            .exchange();

    BiFunction<WebTestClient, RequestObjectDTO, WebTestClient.ResponseSpec> get = (webTestClient, requestObjectDTO) ->
        webTestClient
            .get()
            .uri(buildURI(requestObjectDTO))
            .headers(requestObjectDTO.headersConsumer())
            .exchange();

    BiFunction<WebTestClient, RequestObjectDTO, WebTestClient.ResponseSpec> patch = (webTestClient, requestObjectDTO) ->
        webTestClient
            .patch()
            .uri(buildURI(requestObjectDTO))
            .headers(requestObjectDTO.headersConsumer())
            .body(fromValue(requestObjectDTO.body()))
            .exchange();

    BiFunction<WebTestClient, RequestObjectDTO, WebTestClient.ResponseSpec> delete = (webTestClient, requestObjectDTO)
        -> Objects
        .isNull(requestObjectDTO.body()) ? webTestClient
        .delete()
        .uri(buildURI(requestObjectDTO))
        .headers(requestObjectDTO.headersConsumer())
        .exchange() :
        ((WebTestClient.RequestBodySpec) ((WebTestClient.RequestBodySpec) webTestClient
            .delete()
            .uri(buildURI(requestObjectDTO)))
            .headers(requestObjectDTO.headersConsumer()))
            .body(fromValue(requestObjectDTO.body()))
            .exchange();

}

In the WebTestClientUtil, we are consuming the UriBuilderUtil classes buildURI method. If you do not have API versioning like v1, v2, v3 etc. You do not need to have version logic in the below code snippet.

public interface UriBuilderUtil {

    static String buildURI(final RequestObjectDTO requestObjectDto) {
        final String pathSegment = requestObjectDto.version().concat(requestObjectDto.resourcePath());

        final UriComponents uriComponents = UriComponentsBuilder
            .newInstance()
            .pathSegment(pathSegment)
            .queryParams(requestObjectDto.queryParams())
            .build();

        return uriComponents.toUriString();

    }
}

and below is the When class’s content. We send the WebTestClient instance and the RequestObject instance to the WebTestClientUtil methods.  @Step is Allure Reports annotation. @ElapsedTime is custom annotation which we did by using spring aop.

public abstract class When<T extends When<T>> extends Given<T> {
    @Step
    @ElapsedTime
    public T whenIGet() {
        responseSpec = get.apply(ctWebTestClient, requestObject);
        return getThis();
    }

    @Step
    @ElapsedTime
    public T whenIPost() {
        responseSpec = post.apply(ctWebTestClient, requestObject);
        return getThis();
    }

    @Step
    @ElapsedTime
    public T whenIPut() {
        responseSpec = put.apply(ctWebTestClient, requestObject);
        return getThis();
    }

    @Step
    @ElapsedTime
    public T whenIDelete() {
        responseSpec = delete.apply(ctWebTestClient, requestObject);
        return getThis();
    }
}

Then Steps

In the Then class, we do assertions and verifications, such as status checks and response assertions based on business requirements.

public abstract class Then<T extends Then<T>> extends When<T> {
    @Step
    public T thenIRetrieveResponseSuccessfully() {
        responseSpec
            .expectStatus()
            .isEqualTo(200);
        response = ResponseSerializerUtils.getObjectFromResponse(responseSpec, SkeletonResponseDTO.class);
        return getThis();
    }

    @Step
    public T thenIRetrieveResponseSuccessfully(int status) {
        responseSpec
            .expectStatus()
            .isEqualTo(status);
        response = ResponseSerializerUtils.getObjectFromResponse(responseSpec, SkeletonResponseDTO.class);
        return getThis();
    }

    @Step
    public T thenISeeExpectedError(ErrorExpectations errorExpectations) {
        verifyErrors(responseSpec, Tuples.of(errorExpectations, getErrors()));
        return getThis();
    }

    @Step
    public T thenISeeExpectedErrorsAndRecords(ErrorExpectations errorExpectations, String[] errorRecords) {
        verifyErrorAndRecords(responseSpec, Tuples.of(errorExpectations, getErrors()), errorRecords);
        return getThis();
    }

    @Step
    public <E> T thenRunAssertions(BiConsumer<Steps, E> biConsumer, Steps steps, E expectations) {
        biConsumer.accept(steps, expectations);
        return getThis();
    }

    @Step
    public <E> T thenRunAssertions(BiConsumer<Steps, List<E>> biConsumer, Steps steps, List<E> expectations) {
        biConsumer.accept(steps, expectations);
        return getThis();
    }

    @Step
    public T thenRunAssertions(Consumer<Steps> consumer, Steps steps) {
        consumer.accept(steps);
        return getThis();
    }

    @Step
    public <E, A> T thenFieldEquals(E expected, A actual) {
        assertEquals(expected, actual);
        return getThis();
    }

    @Step
    public <O> T thenFieldIsNull(O object) {
        Assertions.assertNull(object);
        return getThis();
    }

    @Step
    public <O> T thenFieldIsNotNull(O object) {
        assertNotNull(object);
        return getThis();
    }

    @Step
    public T thenFieldIsTrue(Boolean bool) {
        assertTrue(bool);
        return getThis();
    }

    @Step
    public T thenFieldIsFalse(Boolean bool) {
        assertFalse(bool);
        return getThis();
    }

    @Step
    public T thenJsonStringsEqual(String expectedObject, String actualObject, JSONCompareMode jsonCompareMode) {
        JSONAssert.assertEquals(expectedObject, actualObject, jsonCompareMode);
        return getThis();
    }

    @Step
    public <E> T thenListIsEmpty(List<E> element) {
        assertThat(element, is(empty()));
        return getThis();
    }

    public <O> T thenObjectIsEmpty(O object) {
        assertEquals(is(empty()), object);
        return getThis();
    }

    @Step
    public T thenStringIsNotEmpty(String actual) {
        assertThat(actual, not(emptyString()));
        return getThis();
    }

    @Step
    public T thenStringIsEmpty(String actual) {
        assertThat(actual, is(emptyString()));
        return getThis();
    }
}

Here verifyErrors method is used for error verification which is located inside the test core library.

public final class ErrorVerifierUtil {
    public static void verifyErrors(final WebTestClient.ResponseSpec responseSpec,
        final Tuple2<ErrorExpectations, ResponseDomainErrorDTO> errorTuple2) {
        responseSpec
            .expectStatus()
            .isEqualTo(errorTuple2
                .getT1()
                .getStatusCode());

        assertEquals(errorTuple2
                .getT1()
                .getErrorCode(), errorTuple2
                .getT2()
                .getErrorCode(),
            "\nExpected Status Code: " + errorTuple2
                .getT1()
                .getErrorCode()
                + " \ndoes not match with\nActual Status Code: " + errorTuple2
                .getT2()
                .getErrorCode());

        assertEquals(errorTuple2
                .getT1()
                .getErrorMessage(), errorTuple2
                .getT2()
                .getErrorMessage(),
            "\nExpected Message: " + errorTuple2
                .getT1()
                .getErrorMessage()
                + " \ndoes not match with\nActual Message: " + errorTuple2
                .getT2()
                .getErrorMessage());
    }

    public static void verifyErrorAndRecords(final WebTestClient.ResponseSpec responseSpec,
        final Tuple2<ErrorExpectations, ResponseDomainErrorDTO> errorTuple2, final String[] expectedErrorMessages) {
        verifyErrors(responseSpec, errorTuple2);
        IntStream
            .range(0, errorTuple2
                .getT2()
                .getErrorRecords()
                .size())
            .forEach(i -> assertTrue(errorTuple2
                    .getT2()
                    .getErrorRecords()
                    .toArray()[i]
                    .toString()
                    .contains(Arrays
                        .stream(expectedErrorMessages)
                        .toArray()[i].toString()),
                "\nActual Message: " + errorTuple2
                    .getT2()
                    .getErrorRecords()
                    .toArray()[i].toString()
                    + "\nExpected Message: " + Arrays
                    .stream(expectedErrorMessages)
                    .toArray()[i].toString()));
    }
}

Top Level Steps Class to Reach all Steps

With the top-level Steps class, we can reach all methods of all steps.

@TestComponent("ctSteps")
public class Steps extends Then<Steps> {
    @Override
    public Steps getThis() {
        return this;
    }
    //You can add some common complex scenarios by using given, when, and then methods here if you want.
}

We covered all the step classes. Let’s move on.

Assertions Class

If you have some complex assertions or want to keep assertions in a separate class, you can do it as shown below. I used here Consumer interface. You can learn Java Functional Programming Interfaces here.

@TestComponent
public class CtAssertions implements Consumer<Steps> {
    Consumer<Steps> ctAssertions = (steps) -> steps.thenFieldIsNotNull(steps.getResponse().getId());

    @Override
    public void accept(Steps steps) {
        ctAssertions.accept(steps);
    }
}

Configurations

If you want to read some configuration values from the test configuration file (application-component.yaml or application-component.properties) or scan some external libraries etc. you can use TestConfiguration as shown below.

@Getter
@Setter
@TestConfiguration
@ComponentScan(value = {
    "com.swtestacademy.external.library.whichIMayNeedForTests",
    "com.swtestacademy.test.data.util.anotherlibrary" })
public class CtConfig {
    @Value("${service.configwhichIMayNeed:dummy}")
    private String configWhichIMayNeed;
}

Headers 

We can form some common headers which we can use in our component tests, as shown below. For this, I will use HeaderUtil, which should be in the test core library, or you can keep it under a util package in your service repository, which I don’t suggest this way. Try to keep all common functions in the test core library.

Below is the header utility code, which should be inside the test core library.

public final class HeaderUtil {
    private static MultiValueMap<String, String> headers = new HttpHeaders();

    private HeaderUtil() {
    }

    public static Consumer<HttpHeaders> headerConsumer() {
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_STREAM_JSON.toString());
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_STREAM_JSON.toString());
        headers.add("YourHeaderKey",  "YourHeaderValue");
        return httpHeaders -> httpHeaders.addAll(headers);
    }

    public static void addHeader(final String headerKey, final String headerValue) {
        headers.add(headerKey, headerValue);
    }

    public static MultiValueMap<String, String> getHeaders() {
        return headers;
    }

    public static void removeHeader(final String headerKey) {
        headers.remove(headerKey);
    }

    public static void modifyHeader(final String headerKey, final String headerValue) {
        headers.remove(headerKey);
        headers.add(headerKey, headerValue);
    }

    public static void clearHeaders() {
        headers.clear();
    }
}

Below is the Headers code inside the service repository under the ct package, which we use in the component tests.

@TestComponent
public class Headers {
    public Supplier<Consumer<HttpHeaders>> defaultHeaders = () -> {
        HeaderUtil.headerConsumer();
        HeaderUtil.modifyHeader(ACCEPT, "application/json");
        HeaderUtil.modifyHeader(CONTENT_TYPE, "application/json");
        HeaderUtil.addHeader("MyHeader", "MyHeaderValue");
        HeaderUtil.addHeader("some_other_header_Id_1", "Test");
        return (httpHeaders) -> httpHeaders.addAll(HeaderUtil.getHeaders());
    };

    public Function<String, Consumer<HttpHeaders>> headersWithAuthToken = (token) -> {
        defaultHeaders.get();
        HeaderUtil.addHeader(AUTHORIZATION, "Bearer " + token);
        return (httpHeaders) -> httpHeaders.addAll(HeaderUtil.getHeaders());
    };
}

Note: You can follow the same approach for query parameters if your service has query parameters. I do not want to repeat the same approach I have shown for headers. 

Payloads

As shown below, you can form the payloads (bodies) for your tests. The details and how you can create the payloads depend on your service’s requirements.

@TestComponent("ctPayloads")
@Getter
public class Payloads {
    public Supplier<String> samplePayload = () -> {
        SkeletonRequestDTO skeletonPostRequestDTO = SkeletonRequestDTO.builder()
            .id("1231723871263817263")
            .name("onur")
            .build();
        return JsonUtil.getJsonStringFromPojo(skeletonPostRequestDTO);
    };

    public BiFunction<String, String, String> buildPayload = (id, name) -> {
        SkeletonRequestDTO skeletonPostRequestDTO = SkeletonRequestDTO.builder()
            .id(id)
            .name(name)
            .build();
        return JsonUtil.getJsonStringFromPojo(skeletonPostRequestDTO);
    };
}

Here, I use JsonUtil class’s getJsonStringFromPojo method. Here is a sample mini snippet of the JSON Utility class. I will create another article for JSON manipulation. I do not want to make it longer here.

public interface JsonUtil {

    /**
     * This method will be used for mapping OBJECT_MAPPER serializer. This is a sample object mapper. It may differ based on the service requirements.
     */
    Integer INDENT_FACTOR      = 4;
    Supplier<ObjectMapper> OBJECT_MAPPER = () -> {
        final ObjectMapper mapper = new ObjectMapperConfigurer(new ObjectMapper()).getObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return mapper;
    };

    
    /**
     * This method gives you json string from pojo.
     *
     * @param jsonObj pass jsonObject
     */
    static String getJsonStringFromPojo(final Object jsonObj) {
        try {
            return OBJECT_MAPPER
                .get()
                .writeValueAsString(jsonObj);
        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }
    }

}

RequestObject Builder

In Given classes, we are building or, in other words preparing the requests, and for this, we use RequestObjectBuilder class.

@TestComponent
public class RequestObjectBuilder {
    /**
     * Based on your requirements you can create methods which takes query params, resource path, body etc.
     */
    public RequestObjectDTO buildRequestObject(Consumer<HttpHeaders> headersConsumer) {
        return new RequestObjectDTO()
            .headersConsumer(headersConsumer)
            .resourcePath("/cttest")
            .version("v1");
    }

    public RequestObjectDTO buildRequestObject(Consumer<HttpHeaders> headersConsumer, String payload) {
        return new RequestObjectDTO()
            .headersConsumer(headersConsumer)
            .version("v1")
            .resourcePath("/" + "ct" )
            .body(payload);
    }
}

Mock Builder

We use wiremock to create the mocks as follows.

@TestComponent
public class MockBuilder {
    /**
     * Create mocks for all clients and external connections.
     */
    public MockBuilder mockDownStreamPost(String responseFileName) {
        stubFor(post("/rest/mock-service/v1/normal-content")
            .willReturn(aResponse()
                .withStatus(201)
                .withHeader("Content-Type", "application/json;charset=UTF-8")
                .withHeader("X-Application-Context", "application:prod")
                .withBodyFile("json/" + responseFileName)));
        return this;
    }

    public MockBuilder mockDownStreamGet(String responseFileName) {
        stubFor(get("/rest/mock-service/v1/normal-content")
            .willReturn(aResponse()
                .withHeader("Content-Type", "application/json")
                .withBodyFile("json/" + responseFileName)));
        return this;
    }
}

The mock response files must be in the test resources __files folder.

and I have a MockInitializer class to group the mocks which we defined in MockBuilder class.

@TestComponent
public class MockInitializer {
    @Autowired
    MockBuilder mockBuilder;

    public void initializeServiceMocks() {
        mockBuilder.mockDownStreamGet("skeleton-downstream-response.json");
        mockBuilder.mockDownStreamPost("skeleton-downstream-response.json");
    }
}

Error Expectations

We can group our error expectations inside the test core library as an enum class.

@Getter
@AllArgsConstructor
public enum ErrorExpectations {
    BAD_REQUEST(
        400,
        "400.031.001",
        "The Request which you have provided is not valid."),
    VERSION_NOT_SUPPORTED(
        404,
        "404.031.002",
        "The version is not supported.");

    private final int    statusCode;
    private final String errorCode;
    private final String errorMessage;
}

Base Test Class

In the base test class, we initialize the classes we use inside component tests and do before-after operations for the tests.

public class BaseCT {
    @Autowired
    protected MockInitializer mockInitializer;
    @Autowired
    @Qualifier("ctSteps")
    protected Steps           steps;
    @Autowired
    @Qualifier("ctPayloads")
    protected Payloads        payloads;
    @Autowired
    protected Headers         headers;
    @Autowired
    protected CtAssertions ctAssertions;

    @AfterEach()
    public void tearDown() {
        WireMock.reset();
        HeaderUtil.clearHeaders();
    }
}

Test Class

We should write the tests in a Given-When-Then fashion. They should be readable, lean, clean, data-driven, and maintainable. I give some random naming, but I suggest using meaningful names for your tests, like:

  • GivenIHaveCartData
  • WhenICreateACart
  • ThenISeeCartCreatedSuccefully

And please do not use too many Given-When-Then methods for your tests. Tests should be lean, short, simple, and easy to read. Do not create a Given-When-Then hell.

@ComponentTest
@Feature("CT Feature 1.")
@DisplayName("CT Feature 1 Tests.")
class SwTestAcademyCT extends BaseCT {
    @BeforeEach
    void initStub() {
        mockInitializer.initializeServiceMocks();
    }

    @Test
    @DisplayName("I GET a response from the service successfully.")
    void iGetResponseSuccessfully() {
        steps
            .givenIHaveARequest(headers.defaultHeaders.get())
            .whenIGet()
            .thenIRetrieveResponseSuccessfully();
    }

    @Test
    @DisplayName("I do a POST call successfully.")
    void iPostSuccessfully() {
        steps
            .givenIHaveARequest(headers.defaultHeaders.get(), payloads.getSamplePayload().get())
            .whenIPost()
            .thenIRetrieveResponseSuccessfully(201);
    }

   @Test
   @DisplayName("I run the assertions successfully.")
   void iPostSuccessfullyAndRunAssertions() {
        steps
             .givenIHaveARequest(headers.defaultSkeletonHeaders.get(), payloads.getSamplePayload().get())
             .whenIPost()
             .thenRunAssertions(ctAssertions, steps);
}

    @Test
    @DisplayName("I see 404 Version Not Supported Error. (This is a sample negative test case implementation.)")
    void iSeeVersionNotSupportedError() {
        steps
            .givenIHaveARequestWithBadVersion("v1242")
            .whenIGet()
            .thenISeeExpectedError(VERSION_NOT_SUPPORTED);
    }
}

We run the tests with the below maven command.

mvn test ‘-Dtest=com.swtestacaedmy.myservice.ct.**’

Thanks,
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.