Skip to content

πŸ§ͺAcceptance Test(ATDD) Module

Good news for you πŸŽ‰

Finally, Lg5 supports Cucumber integration with Spring Boot 3, Testcontainers, and reuses many configurations for testing. This way, you can create acceptance tests for your domain services quickly.

lg5-spring-acceptance-test

Example an Acceptance Test Report with Cucumber

Principal Dependencies

acceptance-test-module(pom.xml)
1
2
3
4
5
6
<dependencies>
    <dependency>
        <groupId>com.lg5.spring</groupId>
        <artifactId>lg5-spring-starter</artifactId>
    </dependency>
</dependencies>

Use lg5-spring-starter to get a Spring context and have dependency injection in the test creation process.

Domain Service Image

️Warning

Before continuing, you will need to have the docker image of your app. If not, remember to add the jib plugin in the container module or generate the docker image in another way. Once you have the docker image, you can continue.

Acceptance Test Drive Development

Test

Feature:
    I as a customer want to create a blank using the repository template

Scenario: the blank should be CREATED when use the repository template
    Given a repository template
    When blank is created
    Then the blank will be created using the repository template

Recommended following Acceptance Test Drive Development allowed to align main goals for one or more user's needs.

Using the traditional workflow ATDD, you need to define acceptance criteria using Gherkin syntax (Given, When, Then).

You need to add the following Dependency and Java classes to your acceptance test. So, you can try the dockerized domain service. For more details, read more about Hexagonal Architecture(Spanish).

Dependencies:

Lg5 tries to simplify dependencies but the power is the same. πŸ‘Œ

acceptance-test-module(pom.xml)
1
2
3
4
5
6
<!-- Test-->
<dependency>
    <groupId>com.lg5.spring</groupId>
    <artifactId>lg5-spring-acceptance-test</artifactId>
    <scope>test</scope>
</dependency>
It is recommended to create the following boot/ directory on your test directory as com.[blanksystem].[blank].service/boot.
 └── test/
    β”œβ”€β”€ java/
    β”‚  └── com.[blanksystem].[blank].service
    β”‚     β”œβ”€β”€ boot/
    β”‚     β”‚  └── AcceptanceTestCase.java
    β”‚     β”‚  └── CucumberHooks.java
    β”‚     β”‚  └── TestContainersLoader.java

To get started, you need to add the following classes:

Important the image name and use the correct version. For instance: com.blanksystem/blank-service with version 1.0.0-alpha.

TestContainersLoader.java
import com.lg5.spring.kafka.config.data.KafkaConfigData;
import com.lg5.spring.testcontainer.config.AppContainerCustomConfig;
import com.lg5.spring.testcontainer.config.ContainerConfig;
import com.lg5.spring.testcontainer.config.KafkaContainerCustomConfig;
import com.lg5.spring.testcontainer.config.PostgresContainerCustomConfig;
import com.lg5.spring.testcontainer.config.WiremockContainerCustomConfig;
import com.lg5.spring.testcontainer.container.AppCustomContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.wiremock.integrations.testcontainers.WireMockContainer;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

@Import({
PostgresContainerCustomConfig.class,
KafkaContainerCustomConfig.class,
WiremockContainerCustomConfig.class,
AppContainerCustomConfig.class
})
public final class TestContainersLoader {

    private final KafkaConfigData kafkaConfigData;

    private final List<ContainerConfig> containerConfigs;

    public TestContainersLoader(KafkaConfigData kafkaConfigData, List<ContainerConfig> containerConfigs) {
        this.kafkaConfigData = kafkaConfigData;
        this.containerConfigs = containerConfigs;
    }

    @Bean
    public AppCustomContainer apiContainer(AppCustomContainer appCustomContainer,
                                           PostgreSQLContainer<?> postgresContainer,
                                           KafkaContainer kafkaContainer,
                                           WireMockContainer wireMockContainer,
                                           GenericContainer<?> schemaRegistryContainer) {

        appWithEnvBuilder(appCustomContainer.getEnvMap(), postgresContainer, kafkaContainer,
                wireMockContainer, schemaRegistryContainer);

        appCustomContainer.start();
        appCustomContainer.initRequestSpecification();
        updateKafkaConfigData(kafkaContainer);

        return appCustomContainer;
    }

    private void updateKafkaConfigData(KafkaContainer kafkaContainer) {
        kafkaConfigData.setBootstrapServers(kafkaContainer.getBootstrapServers());
    }

    private void appWithEnvBuilder(Map<String, String> envMap, PostgreSQLContainer<?> postgreSQLContainer,
                                   KafkaContainer kafkaContainer,
                                   WireMockContainer wireMockContainer,
                                   GenericContainer<?> schemaRegistryContainer) {

        final Map<Class<?>, Consumer<Map<String, String>>> configActions = new HashMap<>();


        addPostgresConfig(postgreSQLContainer, configActions);


        addWiremockConfig(wireMockContainer, configActions);

        addKafkaConfig(kafkaContainer, schemaRegistryContainer, configActions);

        configActions.forEach((configClass, action) -> action.accept(envMap));


    }

    private void addKafkaConfig(KafkaContainer kafkaContainer, GenericContainer<?> schemaRegistryContainer, Map<Class<?>, Consumer<Map<String, String>>> configActions) {
        configActions.put(KafkaContainerCustomConfig.class,
                map -> containerConfigs.stream()
                        .filter(KafkaContainerCustomConfig.class::isInstance)
                        .findFirst()
                        .ifPresent(config -> map.putAll(((KafkaContainerCustomConfig) config)
                                .initializeEnvVariables(kafkaContainer, schemaRegistryContainer))));
    }

    private void addWiremockConfig(WireMockContainer wireMockContainer, Map<Class<?>, Consumer<Map<String, String>>> configActions) {
        configActions.put(WiremockContainerCustomConfig.class,
                map -> containerConfigs.stream()
                        .filter(WiremockContainerCustomConfig.class::isInstance)
                        .findFirst()
                        .ifPresent(config -> map.putAll(config.initializeEnvVariables(wireMockContainer))));
    }

    private void addPostgresConfig(PostgreSQLContainer<?> postgreSQLContainer, Map<Class<?>, Consumer<Map<String, String>>> configActions) {
        configActions.put(PostgresContainerCustomConfig.class,
                map -> containerConfigs.stream()
                        .filter(PostgresContainerCustomConfig.class::isInstance)
                        .findFirst()
                        .ifPresent(config -> map.putAll(config.initializeEnvVariables(postgreSQLContainer))));
    }

This module does not expose any ports, so you must extend the Lg5TestBootPortNone class.

CucumberHooks.java
1
2
3
4
5
6
7
8
9
import com.lg5.spring.integration.test.boot.Lg5TestBootPortNone;
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.context.annotation.Import;

@Import(TestContainersLoader.class)
@CucumberContextConfiguration
public final class CucumberHooks extends Lg5TestBootPortNone {

}
  1. Define the feature file in src/test/resources/features/*.feature
  2. You can find cucumber repor in target/atdd-reports/cucumber-reports.html
    all in one click on this class, and you are ready to use the application.
AcceptanceTestCase.java
import io.cucumber.junit.platform.engine.Constants;
import org.junit.jupiter.api.Test;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.ConfigurationParameters;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

import java.io.File;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameters({
@ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty, json:target/atdd-reports/cucumber.json, " +
"html:target/atdd-reports/cucumber-reports.html"),
@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "com.blanksystem.blank.service")
})
class AcceptanceTestCase {

    @Test
    void test() {
        File feature = new File("src/test/resources/features");
        assertTrue(feature.exists());
    }
}
  1. Create a directory calledsrc/test/resources/features
  2. Create the feature file in example.feature
  3. Create new step definition file
example.feature
1
2
3
4
5
6
7
Feature:
    I as a customer want to create a blank using the repository template

Scenario: the blank should be CREATED when use the repository template
    Given a repository template
    When blank is created
    Then the blank will be created using the repository template
MyStepdefs.java
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
public class MyStepdefs {
    @Given("a repository template")
    public void aRepositoryTemplate() {
    }

    @When("blank is created")
    public void blankIsCreated() {
    }

    @Then("the blank will be created using the repository template")
    public void theBlankWillBeCreatedUsingTheRepositoryTemplate() {
    }
}

With destination.path: ./target/logs where stored logs file from blank-service after that tests finished.
Replace some name by your system and domain name: blanksystem and blank.

`application.image.name: your-docker.images:version

⚠️ Please attention to highlighted lines!!!
⚠️ Disabled liquibase migrations, only test(NOT PRODUCTION).

application-test.yml
application:
  server:
    port: 8080
  image:
    name: com.blanksystem/blank-service:1.0.0-alpha
  traces:
    console:
      enabled: false
    file:
      enabled: true
  log:
    source:
      path: /logs
    destination:
      path: ./target/logs

blanksystem:
  blank:
    events:
      journal:
        blank:
          topic: blank.1.0.event.created
          consumer:
            group: blank-topic-consumer-acceptance-test
spring:
  datasource:
    url: jdbc
  liquibase:
    enabled: false

logging:
  level:
    com.blanksystem: INFO
    io.confluent.kafka: ERROR
    org.apache: ERROR

third:
  basic:
    auth:
      username: admin
      password: pass
  jsonplaceholder:
    url: https://jsonplaceholder.typicode.com
    basic:
      auth:
        username: admin
        password: pass

wiremock:
  config:
    folder: "wiremock/third_system/template.json"
    url: "third.jsonplaceholder.url"
    port: 7070

When do you like to use some TestContainer

if you use some TestContainer CustomConfig enabled, you would added the following properties for each one:

* Postgres Container
    * Add liquibase files with migrations. 
* Kafka Container       
    * `${kafka-config.bootstrap-servers}`       
* SchemaRegistry Container      
    * `${kafka-config.schema-registry-url}`
* KAFKA MODELS
    * If you must have kafka models, So, CREATE AVRO DIRECTORY  For instance: `src/test/resources/avro/example.avsc`
* Wiremock Container        
    * Specify third system url `${wiremock.config.url}`.        
    * Indicate a port binding to connect: `${wiremock.config.port}`.       
    * Directory where stored the mock req/res http `${wiremock.config.folder}`.
    * CREATE WIREMOCK DIRECTORY and a TEMPLATE base, For instance: `src/test/resources/wiremock/third_system/template.json`.

Needs the Spring Context

Add a classic application main with Spring Boot:

Stay alert with the scanBasePackages for your tests, in this case principal package the current system and extras as kafka.

Application.java
package com.blanksystem.blank.service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = {"com.blanksystem", "com.lg5.spring.kafka"})
public class Application {

    public static void main(String[] args) {

        SpringApplication.run(Application.class, args);
    }
}

More dependencies

If you must have Kafka(avro plugin), database, etc. Please include more dependencies.

<dependencies>
    ...
    <!-- if you need to connect a database-->
    <dependency>
        <groupId>com.lg5.spring</groupId>
        <artifactId>lg5-spring-data-jpa</artifactId>
    </dependency>
    <!-- if you need to generate models-->
    <dependency>
        <groupId>com.lg5.spring.kafka</groupId>
        <artifactId>lg5-spring-kafka-model</artifactId>
    </dependency>
    <!-- if you need to produce events-->
    <dependency>
        <groupId>com.lg5.spring.kafka</groupId>
        <artifactId>lg5-spring-kafka-producer</artifactId>
    </dependency>
    <!-- if you need to consume events-->
    <dependency>
        <groupId>com.lg5.spring.kafka</groupId>
        <artifactId>lg5-spring-kafka-consumer</artifactId>
    </dependency>
    ...
</dependencies>
...
<build>
    <plugins>
        <!-- if you need to generate models-->
        <plugin>
            <groupId>org.apache.avro</groupId>
            <artifactId>avro-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Read more at Lg5Spring Wiki.

Project structure

./
β”œβ”€β”€ pom.xml
β”œβ”€β”€ src/
β”‚  β”œβ”€β”€ main/
β”‚  β”‚  β”œβ”€β”€ java/
β”‚  β”‚  β”‚  └── com.blanksystem.blank.service
β”‚  β”‚  β”‚     └── Application.java
β”‚  β”‚  └── resources/
β”‚  β”‚     └── application.yml
β”‚  └── test/
β”‚     β”œβ”€β”€ java/
β”‚     β”‚  └── com.blanksystem.blank.service
β”‚     β”‚     └── boot/
β”‚     β”‚        β”œβ”€β”€ AcceptanceTestCase.java
β”‚     β”‚        β”œβ”€β”€ CucumberHooks.java
β”‚     β”‚        └── TestContainersLoader.java
β”‚     └── resources/
|        └── application-test.yml
β”‚        β”œβ”€β”€ features/
β”‚        β”‚  └── blank-service.feature
β”‚        └── wiremock/
β”‚           └── placeholder/
β”‚        
└── target(autogenerate)/
   └── atdd-reports/
      β”œβ”€β”€ cucumber-reports.html
      └── cucumber.json

Danger

You do not need to create the target/ directory, it is automatically generated by the maven clean install build command

2'DO

  • Add support for a dockerized application without port.