JUnit5 & TestContainers
Catalog tribe demo

debop@coupang.com
Agenda
• JUnit 5

• TestContainers
JUnit 5
JUnit 5
• JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

• Current version = 5.3.2

• No public modifier needed

• Require Java 8 or higher

• Support in IntelliJ IDEA and Eclipse IDE
Setup
testImplementation “org.junit.jupiter:junit-jupiter-api:$jupiter_version”
testRuntimeOnly “org.junit.jupiter:junit-jupiter-engine:$jupiter_version”
Example import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@BeforeEach
fun setup() {
facts.clear()
}
@Test
fun `facts must have unique name`() {
facts.put("foo", 1)
facts.put("foo", 2)
facts.size shouldEqual 1
assertEquals(2, facts["foo"] !!)
}
Annotations
JUnit 4 JUnit 5
@BeforeClass /
@AfterClass
@BeforeAll / @AfterAll
@Before / @After @BeforeEach/@AfterEach
@Test @Test
@Test(expected=…) Assertions.assertThrows(…) { … }
@Ignore @Disabled
@Nested
@DisplayName
@DisplayName("FactMap operation test")
class FactMapTest {
@Nested
@DisplayName("When same key")
inner class MutableFactMap {
@Test
@DisplayName("When insert duplicated key, update old value")
fun `put same item`() {
val o1 = facts.put("foo", 1)
val o2 = facts.put("foo", 2)
assertNull(o1)
assertEquals(1, o2)
}
}
@Test
fun `facts must have unique name`() {
// …
}
}
@Nested
Assertions
• org.junit.jupiter.api.Assertions

• Use of lambdas for lazy evaluation of messages

• Grouping of assertions

• New way to handle exceptions
Assertions
@Test
fun `put same item`() {
val o1 = facts.put("foo", 1)
val o2 = facts.put("foo", 2)
assertNull(o1) { "o1 should be null" }
assertEquals(1, o2) { "o2 should be 1" }
}
Assertions
assertAll(
Executable { assertNull(o1) { "o1 should be null" } },
Executable { assertEquals(1, o2) { "o2 should be 1" } }
)
assertThrows(IllegalArgumentException::class.java) {
facts.put("", 1)
}
assertTimeout(Duration.ofMillis(200)) {
Thread.sleep(150)
}
assertTimeoutPreemptively(Duration.ofMillis(200)) {
Thread.sleep(150)
}
Assumptions
If assumption fails -> test is skipped
@Test
fun `already exists`() {
// facts is empty -> assumeFalse is failed -> skip assertEquals
assumeFalse { facts.isEmpty() }
// Skip tests
assertEquals(2, 1)
}
Dynamic Tests
@TestFactory
fun testRules(): Stream<DynamicTest> {
return IntStream.range(1, 3)
.mapToObj { it ->
dynamicTest("test for input=$it") {
assertEquals(it * 2, it + it)
}
}
}
https://www.baeldung.com/junit5-dynamic-tests
Conditional Test Execution
• ExecutionCondition as Extension API

• DisabledCondition is simplest example with
@Disabled annotation
Conditional Test Execution
• @EnabledOnOS(…)

• @EnabledOnJre(…)

• @EnabledIfSystemProperty(named=“”, matches=“”)

• @EnabledIfEnvironmentalVariable(named=“”,
matches=“”)

• @EnabledIf(“”) - Support for script, EXPERIMENTAL
Parameterized Tests
• Experimental feature

• Need “junit-jupiter-params”

• Resources

• JUnit 5 Parameterized Tests: Using Different Input 

• JUnit 5 ­ Parameterized Tests
Parameterized Tests
@ParameterizedTest
@ValueSource(strings = ["java8", "java9", "java10"])
fun `parameterized test`(param:String) {
param shouldContain "java"
}
Parameterized Tests
@ParameterizedTest(name = "[{index}]=> {arguments}")
@ValueSource(strings = ["java8", "java9", "java10"])
fun `parameterized test`(param: String) {
param shouldContain "java"
}
Parameterized Tests
• @ValueSource for String, Int, Long, Double)

• @EnumSource(…)

• @MethodSource(“methodName”)

method must return Stream, Iterator or Iterable

• @CsvSource({“foo, bar”, “foo2, bar2”})

• @CsvFileSource(resources=…)

• @ArgumentSource(MyArgProvider.class)
Parallel Test Execution
• Resources

• JUnit 5 Parallel Execution

• JUnit 5 Parallel Test Execution
Parallel Test Execution
junit.jupiter.testinstance.lifecycle.default = per_class
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
#junit.jupiter.execution.parallel.config.strategy = dynamic
#junit.jupiter.execution.parallel.config.dynamic.factor = 1
#junit.jupiter.execution.parallel.config.strategy = fixed
#junit.jupiter.execution.parallel.config.fixed.parallelism = 4
#junit.jupiter.execution.parallel.config.strategy = custom
#junit.jupiter.execution.parallel.config.custom.class = ...
Parallel Test Execution
• Synchronization for shared resources

• @Execution(CONCURRENT)

• @Execution(SAME_THREAD)

• ResourceLock(value=…, mode=…)

• Value

custom|SYSTEM_PROPERTIES|SYSTEM_OUT|SYSTEM_ERR

• Mode

READ | READ_WRITE

• JUnit 5 Synchronization
What else?
• @Tag and filtering in build script

• @RepeatedTest with dynamic placeholder for
@DisplayName

• @TestTemplate/
TestTemplateInvocationContextProvider

• Extension API, extensions registered via @ExtendWith
• Custom Extensions in kotlinx-junit-jupiter
TestContainers
Introduction
• Java library to launch Docker containers during Tests

• Integration tests against the data access layer

• Integration tests with external dependencies

e.g. message broker, database, cache …

• UI Tests with containerized, Selenium compatible,
Web browsers
Introduction
• Current version : 1.10.3

• Requires Docker installation

• Requires Java 8 or higher

• Compatible with JUnit 4 / 5
Usecases
• Data access layer integration tests: use a containerized instance of a
MySQL, PostgreSQL or Oracle database to test your data access layer code for
complete compatibility, but without requiring complex setup on developers'
machines and safe in the knowledge that your tests will always start with a
known DB state.Any other database type that can be containerized can also be
used.
• Application integration tests: for running your application in a short-lived
test mode with dependencies, such as databases, message queues or web servers.
• UI/Acceptance tests: use containerized web browsers, compatible with
Selenium, for conducting automated UI tests. Each test can get a fresh instance of
the browser, with no browser state, plugin variations or automated browser
upgrades to worry about.And you get a video recording of each test session, or
just each session where tests failed.
JUnit 4
@ClassRule
public static GenericContainer mysql =
new MySQLContainer().withExposedPorts(3306);
@Test
public void getExposedPorts() {
List<Integer> ports = mysql.getExposedPorts();
assertThat(ports).contains(3306);
}
JUnit 4 + Kotlin
static {
INSTANCE = createPostgreSQLContainer();
HOST = INSTANCE.getContainerIpAddress();
PORT = INSTANCE.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT);
JDBC_URL = INSTANCE.getJdbcUrl();
}
public static PostgreSQLContainer createPostgreSQLContainer() {
PostgreSQLContainer container = new PostgreSQLContainer();
container.withLogConsumer(new Slf4jLogConsumer(log));
container.withDatabaseName("test");
container.start();
return container;
}
JUnit 5 static GenericContainer mysql =
new MySQLContainer().withExposedPorts(3306);
@BeforeAll
static void setup() {
mysql.start();
}
@AfterAll
static void tearDown() {
mysql.stop();
}
@Test
void getExposedPorts() {
List<Integer> ports = mysql.getExposedPorts();
assertThat(ports).contains(3306);
}
Generic Container
• Offers flexible support for any container image as test
dependency

• Reference public docker images

• Internal dockerized services
Generic Container
• withExposedPorts(…)

• withEnv(…)

• withLabel(…)

• getContainerIpAddress()

• getMappedPort(…)
Generic Container - Kotlin
object RedisContainer : KLogging() {
// For Kotlin language spec
class KGenericContainer(imageName: String) : GenericContainer<KGenericContainer>(imageName)
val instance by lazy { startRedisContainer() }
private fun startRedisContainer() = KGenericContainer("redis:4.0.11").apply {
withExposedPorts(6379)
setWaitStrategy(HostPortWaitStrategy())
withLogConsumer(Slf4jLogConsumer(log))
start()
}
val host: String by lazy { instance.containerIpAddress }
val port: Int by lazy { instance.getMappedPort(6379) }
val url: String by lazy { "redis://$host:$port" }
}
Specialized Container
• Create images from Dockerfile

• withFileFromString(…)

• withFileFromClasspath(…)

• Use Dockerfile DSL to define Dockerfiles in code
Specialized Container
• Use database container to test database specific
features

• No local setup or VM

• 100% database compatibility instead of H2

• MySQL

• PostgreSQL

• Oracle XE
Future
• TestContainers 2.x

• API cleanup

• Decoupling from JUnit 4 to support other
framework directly
Alternatives
• TestNG

• Other JVM languages

• Groovy, Spock, Testcontainers-Spock

• Kotlin, Testcontainers (with workaround)
Resources
• JUnit 5

• JUnit 5 Basic

• JUnit 5 : Next step in automated testing

• How to perform a productivee testing using JUnit 5 on Kotlin 

• Test Containers

• Test containers Official site

• Docker Test Containers in Java Test

• TestContainers and Spring Boot

• Don’t use In-Memory Database (H2, Congo) for Tests
Q&A
Thank you!

JUnit5 and TestContainers

  • 1.
    JUnit5 & TestContainers Catalogtribe demo debop@coupang.com
  • 2.
    Agenda • JUnit 5 •TestContainers
  • 3.
  • 4.
    JUnit 5 • JUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage • Current version = 5.3.2 • No public modifier needed • Require Java 8 or higher • Support in IntelliJ IDEA and Eclipse IDE
  • 5.
  • 6.
    Example import org.junit.jupiter.api.Assertions.assertEquals importorg.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @BeforeEach fun setup() { facts.clear() } @Test fun `facts must have unique name`() { facts.put("foo", 1) facts.put("foo", 2) facts.size shouldEqual 1 assertEquals(2, facts["foo"] !!) }
  • 7.
    Annotations JUnit 4 JUnit5 @BeforeClass / @AfterClass @BeforeAll / @AfterAll @Before / @After @BeforeEach/@AfterEach @Test @Test @Test(expected=…) Assertions.assertThrows(…) { … } @Ignore @Disabled @Nested @DisplayName
  • 8.
    @DisplayName("FactMap operation test") classFactMapTest { @Nested @DisplayName("When same key") inner class MutableFactMap { @Test @DisplayName("When insert duplicated key, update old value") fun `put same item`() { val o1 = facts.put("foo", 1) val o2 = facts.put("foo", 2) assertNull(o1) assertEquals(1, o2) } } @Test fun `facts must have unique name`() { // … } }
  • 9.
  • 10.
    Assertions • org.junit.jupiter.api.Assertions • Useof lambdas for lazy evaluation of messages • Grouping of assertions • New way to handle exceptions
  • 11.
    Assertions @Test fun `put sameitem`() { val o1 = facts.put("foo", 1) val o2 = facts.put("foo", 2) assertNull(o1) { "o1 should be null" } assertEquals(1, o2) { "o2 should be 1" } }
  • 12.
    Assertions assertAll( Executable { assertNull(o1){ "o1 should be null" } }, Executable { assertEquals(1, o2) { "o2 should be 1" } } ) assertThrows(IllegalArgumentException::class.java) { facts.put("", 1) } assertTimeout(Duration.ofMillis(200)) { Thread.sleep(150) } assertTimeoutPreemptively(Duration.ofMillis(200)) { Thread.sleep(150) }
  • 13.
    Assumptions If assumption fails-> test is skipped @Test fun `already exists`() { // facts is empty -> assumeFalse is failed -> skip assertEquals assumeFalse { facts.isEmpty() } // Skip tests assertEquals(2, 1) }
  • 14.
    Dynamic Tests @TestFactory fun testRules():Stream<DynamicTest> { return IntStream.range(1, 3) .mapToObj { it -> dynamicTest("test for input=$it") { assertEquals(it * 2, it + it) } } } https://www.baeldung.com/junit5-dynamic-tests
  • 15.
    Conditional Test Execution •ExecutionCondition as Extension API • DisabledCondition is simplest example with @Disabled annotation
  • 16.
    Conditional Test Execution •@EnabledOnOS(…) • @EnabledOnJre(…) • @EnabledIfSystemProperty(named=“”, matches=“”) • @EnabledIfEnvironmentalVariable(named=“”, matches=“”) • @EnabledIf(“”) - Support for script, EXPERIMENTAL
  • 17.
    Parameterized Tests • Experimentalfeature • Need “junit-jupiter-params” • Resources • JUnit 5 Parameterized Tests: Using Different Input • JUnit 5 ­ Parameterized Tests
  • 18.
    Parameterized Tests @ParameterizedTest @ValueSource(strings =["java8", "java9", "java10"]) fun `parameterized test`(param:String) { param shouldContain "java" }
  • 19.
    Parameterized Tests @ParameterizedTest(name ="[{index}]=> {arguments}") @ValueSource(strings = ["java8", "java9", "java10"]) fun `parameterized test`(param: String) { param shouldContain "java" }
  • 20.
    Parameterized Tests • @ValueSourcefor String, Int, Long, Double) • @EnumSource(…) • @MethodSource(“methodName”)
 method must return Stream, Iterator or Iterable • @CsvSource({“foo, bar”, “foo2, bar2”}) • @CsvFileSource(resources=…) • @ArgumentSource(MyArgProvider.class)
  • 21.
    Parallel Test Execution •Resources • JUnit 5 Parallel Execution • JUnit 5 Parallel Test Execution
  • 22.
    Parallel Test Execution junit.jupiter.testinstance.lifecycle.default= per_class junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent #junit.jupiter.execution.parallel.config.strategy = dynamic #junit.jupiter.execution.parallel.config.dynamic.factor = 1 #junit.jupiter.execution.parallel.config.strategy = fixed #junit.jupiter.execution.parallel.config.fixed.parallelism = 4 #junit.jupiter.execution.parallel.config.strategy = custom #junit.jupiter.execution.parallel.config.custom.class = ...
  • 23.
    Parallel Test Execution •Synchronization for shared resources • @Execution(CONCURRENT) • @Execution(SAME_THREAD) • ResourceLock(value=…, mode=…) • Value
 custom|SYSTEM_PROPERTIES|SYSTEM_OUT|SYSTEM_ERR • Mode
 READ | READ_WRITE • JUnit 5 Synchronization
  • 24.
    What else? • @Tagand filtering in build script • @RepeatedTest with dynamic placeholder for @DisplayName • @TestTemplate/ TestTemplateInvocationContextProvider • Extension API, extensions registered via @ExtendWith • Custom Extensions in kotlinx-junit-jupiter
  • 25.
  • 26.
    Introduction • Java libraryto launch Docker containers during Tests • Integration tests against the data access layer • Integration tests with external dependencies
 e.g. message broker, database, cache … • UI Tests with containerized, Selenium compatible, Web browsers
  • 27.
    Introduction • Current version: 1.10.3 • Requires Docker installation • Requires Java 8 or higher • Compatible with JUnit 4 / 5
  • 28.
    Usecases • Data accesslayer integration tests: use a containerized instance of a MySQL, PostgreSQL or Oracle database to test your data access layer code for complete compatibility, but without requiring complex setup on developers' machines and safe in the knowledge that your tests will always start with a known DB state.Any other database type that can be containerized can also be used. • Application integration tests: for running your application in a short-lived test mode with dependencies, such as databases, message queues or web servers. • UI/Acceptance tests: use containerized web browsers, compatible with Selenium, for conducting automated UI tests. Each test can get a fresh instance of the browser, with no browser state, plugin variations or automated browser upgrades to worry about.And you get a video recording of each test session, or just each session where tests failed.
  • 29.
    JUnit 4 @ClassRule public staticGenericContainer mysql = new MySQLContainer().withExposedPorts(3306); @Test public void getExposedPorts() { List<Integer> ports = mysql.getExposedPorts(); assertThat(ports).contains(3306); }
  • 30.
    JUnit 4 +Kotlin static { INSTANCE = createPostgreSQLContainer(); HOST = INSTANCE.getContainerIpAddress(); PORT = INSTANCE.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT); JDBC_URL = INSTANCE.getJdbcUrl(); } public static PostgreSQLContainer createPostgreSQLContainer() { PostgreSQLContainer container = new PostgreSQLContainer(); container.withLogConsumer(new Slf4jLogConsumer(log)); container.withDatabaseName("test"); container.start(); return container; }
  • 31.
    JUnit 5 staticGenericContainer mysql = new MySQLContainer().withExposedPorts(3306); @BeforeAll static void setup() { mysql.start(); } @AfterAll static void tearDown() { mysql.stop(); } @Test void getExposedPorts() { List<Integer> ports = mysql.getExposedPorts(); assertThat(ports).contains(3306); }
  • 32.
    Generic Container • Offersflexible support for any container image as test dependency • Reference public docker images • Internal dockerized services
  • 33.
    Generic Container • withExposedPorts(…) •withEnv(…) • withLabel(…) • getContainerIpAddress() • getMappedPort(…)
  • 34.
    Generic Container -Kotlin object RedisContainer : KLogging() { // For Kotlin language spec class KGenericContainer(imageName: String) : GenericContainer<KGenericContainer>(imageName) val instance by lazy { startRedisContainer() } private fun startRedisContainer() = KGenericContainer("redis:4.0.11").apply { withExposedPorts(6379) setWaitStrategy(HostPortWaitStrategy()) withLogConsumer(Slf4jLogConsumer(log)) start() } val host: String by lazy { instance.containerIpAddress } val port: Int by lazy { instance.getMappedPort(6379) } val url: String by lazy { "redis://$host:$port" } }
  • 35.
    Specialized Container • Createimages from Dockerfile • withFileFromString(…) • withFileFromClasspath(…) • Use Dockerfile DSL to define Dockerfiles in code
  • 36.
    Specialized Container • Usedatabase container to test database specific features • No local setup or VM • 100% database compatibility instead of H2 • MySQL • PostgreSQL • Oracle XE
  • 37.
    Future • TestContainers 2.x •API cleanup • Decoupling from JUnit 4 to support other framework directly
  • 38.
    Alternatives • TestNG • OtherJVM languages • Groovy, Spock, Testcontainers-Spock • Kotlin, Testcontainers (with workaround)
  • 39.
    Resources • JUnit 5 •JUnit 5 Basic • JUnit 5 : Next step in automated testing • How to perform a productivee testing using JUnit 5 on Kotlin • Test Containers • Test containers Official site • Docker Test Containers in Java Test • TestContainers and Spring Boot • Don’t use In-Memory Database (H2, Congo) for Tests
  • 40.
  • 41.