Table of Contents
Since I spent countless hours finding the correct setup for Cucumber, TestContainers and Junit5 (Jupiter) for my Spring Boot project, I hope this tutorial can somehow save you time and help you avoid needless pain.
Before you begin, I hope you already know what Cucumber, TestContainers, and Junit5 (and of course Spring Boot) are. If you don’t know any of them, feel free to pause this and search on the internet for a quick introduction to such software. They are simply amazing tools that help developers create better software.
Testcontainers (https://www.testcontainers.org/) is an amazing software that helps you spawn containers to do integration tests. I use it when developing with Spring Boot and I don’t have to set up mocks. Imagine having a system that is identical to your production system just for testing.
Cucumber(https://cucumber.io/) I first use cucumber in an EJB project and I was hooked. I love the way I can write tests in a human-readable style and steps can be reused easily.
What are we building?
I want to keep the business logic of this project simple since we only focus on setting up tests for this tutorial. The application in this tutorial does only one thing: Create users.
User.java class
package com.datmt.spring.test_setup.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Getter @Setter @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; private String name; }
UserRepository.class
package com.datmt.spring.test_setup.repository; import com.datmt.spring.test_setup.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface UserRepository extends JpaRepository<User, Long> { List<User> findUsersByName(String name); }
Creating a base class to start neccesarry containers
Let’s create a class to start all the containers that we need. In this example, we only need one database container (which I use PostgreSQL). In real life scenario, you can put as many containers as you need.
This approach is quite convenient because, for tests that need to access the containers, you simply extend this base class. No need to create and start containers on every test class you create.
ContainerBase.java
package com.datmt.spring.test_setup.base; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers public abstract class ContainerBase { public static final PostgreSQLContainer DB_CONTAINER; static { DB_CONTAINER = new PostgreSQLContainer("postgres:13.6-alpine") .withDatabaseName("openexl") .withUsername("root") .withPassword("root"); DB_CONTAINER.start(); } // @Container // private static PostgreSQLContainer container = DB_CONTAINER; @DynamicPropertySource public static void overrideProps(DynamicPropertyRegistry registry) { registry.add("spring.datasource.username", DB_CONTAINER::getUsername); registry.add("spring.datasource.password", DB_CONTAINER::getPassword); registry.add("spring.datasource.url", DB_CONTAINER::getJdbcUrl); } }
Let’s take a minute to see what I did in this class.
The first thing is the annotation @Testcontainers
@Testcontainers is a JUnit Jupiter extension to activate automatic startup and stop of containers used in a test case.
The test containers extension finds all fields that are annotated with Container and calls their container lifecycle methods. Containers declared as static fields will be shared between test methods. They will be started only once before any test method is executed and stopped after the last test method has been executed. Containers declared as instance fields will be started and stopped for every test method.
From the java docs of testcontainers
As the docs mentioned, it is necessary to use @Container
annotation (the part I commented). However, since I started the container in the static block, it is not necessary to use that annotation (correct me if I’m wrong).
Now, any class that extends this class will have the container ready before any test is executed.
Last but not least is the code block under @DynamicPropertySource
Here I replaced the DB connection specified in the application.properties file with the info from the container. Without this step, the test will try to connect to the configurations specified in application.properties
file (which is either dangerous or unsuccessful).
Now we are done with testcontainers, let’s move on with Cucumber.
Setting up Cucumber with Junit5
Setting up Cucumber is quite simple. You only need to know where the feature files are. Also, as mentioned in the previous section, the cucumber test class need to extend the ContainerBase
class.
CucumberBase.java
package com.datmt.spring.test_setup.base; import io.cucumber.junit.CucumberOptions; import io.cucumber.spring.CucumberContextConfiguration; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith(SpringExtension.class) @CucumberContextConfiguration @SpringBootTest @CucumberOptions(plugin = {"pretty"}, features = "src/test/resources/features") public class CucumberBase extends ContainerBase { }
You can see here, I declared the feature files location at src/test/resources/features
and also extend the ContainerBase class.
Now we are ready to write some Gherkin!
Writing Cucumber test in Gherkin
Since the main business of the application is to create users, let’s create a test on that.
Feature: Create user Scenario: Create user and save to database Given I create an user named "Jolie" and store to database When I search for that user by the name "Jolie" Then I should find at least one result
The scenario is quite simple. All it does is create a user named Jolie. Then, after the creation, it executes a find function with a user name and makes sure at least one result is returned.
This test is not perfect because there may be other tests running and creating a user name Jolie. However, since we only have one test, this is acceptable.
In real life, you make need to have a better validation method (such as cleaning the database first or using a very unique/randomly generated name…).
Now we have the business requirements. Let’s create the code execution for each step.
UserSteps.java
package com.datmt.spring.test_setup.steps; import com.datmt.spring.test_setup.entity.User; import com.datmt.spring.test_setup.repository.UserRepository; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; public class UserSteps { @Autowired UserRepository userRepository; List<User> users; @Given("I create an user named {string} and store to database") public void iCreateAnUserNamedAndStoreToDatabase(String arg0) { User user = new User(); user.setName(arg0); userRepository.save(user); } @When("I search for that user by the name {string}") public void iSearchForThatUserByTheName(String arg0) { users = userRepository.findUsersByName(arg0); } @Then("I should find at least one result") public void iShouldFindAtLeastOneResult() { Assertions.assertTrue(users.size() > 0); } }
We follow each statement in the Gherkin file here. First, we take the name of the user passed as an argument and create a user with that name in the database. Then, we execute the find function to find the user with that name and assign the result to a list.
Finally, we assert that the size of the users
list is > 0.
Let’s run the test and see what’s happening.
Executing Cucmber test with Testcontainers
When I clicked “run” on the feature file, there was a lot of text in the console. However, the most interesting lines are:
Do you see the lines with the whale icons? That’s where testcontainers magic happened. It started the container (in this case is the PostgreSQL container) so our test can connect and interact with that database.
And the good news is our test has passed 😀
Conclusion
We have successfully set up a project that supports Cucumber, TestContainers, and JUnit5 (Jupiter) in Spring Boot. There is something I omitted in this post for brevity. However, there is a startup script to create the “users” table on the database so we can create the user. This part is handled by liqibase. You can view the repo here for all the code: Github repo
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.