Modern best practices for Java testing

This article is a Japanese translation of Modern Best Practices for Testing in Java. Translated and published with permission from the author of the original article. I'm new to translation, so I think there are some strange things, but please forgive me.

The same article is posted on the Hatena blog. https://dhirabayashi.hatenablog.com/entry/2020/04/21/190009


Test code that is easy to maintain and read is important for establishing good test coverage, which allows you to implement and refactor new features without fear of breaking anything. This article contains many best practices I've gained over the years writing unit and integration tests in Java. It also includes modern technologies such as JUnit 5, AssertJ, Testcontainers and Kotlin. Some may seem obvious, and some may be incompatible with what you've read in books about software development and testing.

TL;DR

Basic

Given, When, Then The test should consist of three blocks separated by one blank line. Each block of code should be as short as possible. Let's use subfunctions to shorten the block.

//Good example
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "Smartphone"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("Smartphone");
}

Use “actual *” and “expected *” prefixes

//bad example
ProductDTO product1 = requestProduct(1);

ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);

If you want to use variables in assertions about the same value, prefix the variable names with "actual" or "expected". This makes it easier to read and makes the intent of the variable clear. What's more, it reduces the risk of confusing expected and measured values.

//Good example
ProductDTO actualProduct = requestProduct(1);

ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); //Great and clear

Use fixed values rather than random values

Avoid random values as they can make your tests unstable, difficult to debug, omit error messages, and make it difficult to track errors in your code.

//bad example
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad

Instead, use fixed values for everything. Fixed values output error messages that make the test more reproducible, easier to debug, and easier to track to the relevant lines of code.

//Good example
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");

You can reduce the amount of typing by [Use helper function](use #helper function a lot).

Write a small and clear test

Make heavy use of helper functions

Extract small or recurring code into subfunctions and give them descriptive names. It's powerful in that it keeps the test short and makes it easy to get the point of the test at a glance.

//bad example
@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }

    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Good example
@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
//Good example(Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")
//Good example(Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()

This helper function is implemented in Kotlin like this:

fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())

fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")

Don't overuse variables

Extracting values that are used multiple times into variables is a common practice for developers.

//bad example
@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );

    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}

Unfortunately, this inflates the test code significantly. Moreover, it is difficult to trace the value from the resulting test failure message back to the relevant line of code.

KISS principle> DRY principle

//Good example
@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}

As long as your test code is kept short (it's highly recommended), it's okay to know where the same values are used. On top of that, the methods are even shorter and therefore easier to understand. And finally, the failure message in this case makes it easier to trace back the code.

Do not extend existing tests to "test just one more small thing"

//bad example
public class ProductControllerTest {
    @Test
    public void happyPath() {
        //A lot of code here...
    }
}

Adding tests for rare cases to existing tests (of Happypath [^ happy_path]) is fascinating. [^ happy_path]: [Translation] It seems to mean something like a normal system. However, the test is large and difficult to understand. It makes it difficult to keep track of all the relevant test cases covered by that big test. These tests are commonly referred to as "happy path tests" [^ happy_path_test]. [^ happy_path_test]: [Translation] Here, the size of one test and the difficulty of understanding the contents are considered as problems, but even if you look it up, the word "happy path test" does not seem to have such an meaning. And I honestly don't know what that means. However, even if I don't understand the meaning of this sentence, it doesn't hurt so much, so I leave it as it is. If such a test fails, it's hard to know exactly what went wrong.

//Good example
public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}

Instead, create a new test method with a descriptive name that tells you everything about the expected behavior. Yes, you write more, but you can create a clear test that fits your purpose, testing only the relevant behavior. Again, helper functions reduce the amount of typing. And finally, adding a purpose-built test with a descriptive name is a great way to record implemented behavior.

Assert only what you want to test

Think about what you really want to test. Just because you can, avoid asserting more than you need to. In addition, keep in mind what you have already tested in the previous test. Normally, you don't have to assert the same thing multiple times in every test. This keeps the test short, clear, and not distracting from the expected behavior.

Consider an example. Test for HTTP endpoints that return product information. The test suite should include the following tests:

  1. A large "mapping test" that asserts that all values retrieved from the database are correctly returned in the correct format as a correctly mapped JSON payload. If ʻequals () is implemented correctly, you can easily assert it with AssertJ's ʻisEqualTo () (for a single element) or containsOnly () (for multiple elements).
String responseJson = requestProducts();

ProductDTO expectedDTO1 = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "evelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
        .containsOnly(expectedDTO1, expectedDTO2);
  1. A test that checks the correct behavior of the query parameter ? Category. We want to test if it is filtered correctly, not if all the properties are set correctly. It has already been tested in the above case. Therefore, it is sufficient to compare only the returned product IDs.
String responseJson = requestProductsByCategory("Office");

assertThat(toDTOs(responseJson))
        .extracting(ProductDTO::getId)
        .containsOnly("1", "2");
  1. A test that checks for rare cases or special business logic. For example, is a particular value in the payload calculated correctly? In this case, I'm only interested in certain JSON fields in the payload. Therefore, only relevant fields should be checked to clarify and document the scope of the logic under test. Again, you don't have to reassert all the fields, because it doesn't matter here.
assertThat(actualProduct.getPrice()).isEqualTo(100);

Self-contained test

Do not hide related parameters (in helper function)

//bad example
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Yes, you should use helper functions for data generation and assertions. But you have to parameterize them. Let's define parameters for everything that is important for the test and needs to be controlled by the test. Don't force readers of the source to jump to the function definition to understand the test. Rule of thumb: You should be able to get a feel for the test by looking only at the test methods.

//Good example
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

Insert data correctly in test method

Everything in the test method needs to be correct. Moving the reusable data insert code to the @Before method is fascinating, but then the source reader must fly around to fully understand what the test is doing. Will not be. Again, a helper function that inserts data helps to put this repetitive task on one line.

Prefer composition over inheritance

Do not create complex inheritance hierarchies in your test classes.

//bad example
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}

Such a hierarchy makes it difficult to understand, and you are likely to end up inheriting a base test class that contains a lot of things you don't need for your current tests. This distracts readers of the code and may cause bugs. Inheritance is not flexible: It is not possible to use everything inherited from ʻAllInklusiveBaseTest, but is there anything inherited from its superclass ʻAdvancedBaseTest? [^ inheritance] What's more, code readers have to fly between multiple base classes to get the big picture. [^ inheritance]: I didn't understand the meaning, but it's hard to tell whether the members inherited from ʻAllInklusiveBaseTest are derived from ʻAllInklusiveBaseTest or ʻAdvancedBaseTest`. I presume that it is difficult to understand what is inherited from where when the inheritance hierarchy is deep. However, there is a possibility that the sentence is strange due to a mistranslation ...

"Duplicates are better than false abstractions" RDX in 10 Modern Software Over-Engineering Mistakes

We recommend that you use the composition instead. Write a small code snippet and class for each particular fixture task (start test database, generate schema, insert data, start mock web server). Let's reuse these parts in the method with @BeforeAll or by assigning the generated object to the field of the test class. So you build all the new test classes by reusing these parts. Just like a Lego block. In this way, all tests have a fixture that fits them perfectly, is easy to understand, and has nothing to do with happenings. The test class is self-contained because all associations are correct within the test class.

//Good example
public class MyTest {
    //Composition instead of inheritance
    private JdbcTemplate template;
    private MockWebServer taxService;

    @BeforeAll
    public void setupDatabaseSchemaAndMockWebServer() throws IOException {
        this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
        this.taxService = new MockWebServer();
        taxService.start();
    }
}

//Another file
public class DatabaseFixture {
    public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
        PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
        db.start();
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName("org.postgresql.Driver")
                .username(db.getUsername())
                .password(db.getPassword())
                .url(db.getJdbcUrl())
                .build();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        SchemaCreator.createSchema(template);
        return template;
    }
}

Repeat:

KISS principle> DRY principle

Dump test is great: compare output with hardcoded values

Do not reuse production code

The test should test the production code: instead of reusing the production code. If you reuse production code in your tests, you may miss bugs due to the reused code, as the code is no longer tested [^ reuse_production].

//bad example
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));

ProductDTO actualDTO = requestProduct(1);

//Reuse of production code
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);

[^ reuse_production]: Here we are using the production code to generate the expected value. Since we are using the production code for both the expected value generation and the measured value generation, it is considered to be virtually the same as not testing anything.

Instead, when writing a test, think in terms of input and output. The test sets the input value and compares the actual output value with the hard-coded value. In most cases, code reuse is not necessary.

//Good example
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));

Do not write the same logic as the production code in the test

The mapping code is a common example of logic being reinvented in a test. Let's say our test includes a mapEntityToDto () method that returns a DTO return value as used to assert that it contains the same value as the first inserted entity in the test. In this case, you are likely to write the same logic in your test code as your production code. It may contain bugs.

//bad example
ProductEntity inputEntity = new ProductEntity(1, "evelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);

ProductDTO actualDTO = requestProduct(1);

 // mapEntityToDto()Contains the same mapping logic as the production code
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);

Again, the solution is to compare ʻactualDTO` with a manually generated reference object that contains hard-coded values. It's very simple, easy to understand, and error-free.

//Good example
ProductDTO expectedDTO = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);

If you don't want to compare all the values and therefore don't want to generate a complete reference object, consider comparing only the sub-objects or only the related values.

Don't write too much logic

Again, testing is mostly about inputs and outputs. It is to provide an input and compare the measured and expected values. Therefore, you don't have to and shouldn't write too much logic in your tests. If you implement logic with lots of loops and conditions, the tests are harder to understand and more error-prone. In addition, for complex assertion logic, AssertJ's powerful assertions do the hard work for you. [^ too_much_logic] [^ too_much_logic]: AssertJ will do it for you, so you don't have to work hard to write the logic.

Realistic test

Focus on testing full vertical slides

In general, it is recommended to use a mock to test each class individually. But it has serious drawbacks: you all Internal refactoring will destroy all tests, as we are not testing the classes in an integrated manner, but each internal class has a test. And finally, you have to write and maintain various tests. Screen Shot 2020-04-10 at 23.17.05.png

Instead, we suggest Focus on Integration Testing (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests). An "integration test" is a test that brings all the classes together (like production code) and goes through all the tech layers (HTTP, business logic, database) in a perfect vertical slide. This method tests behavior rather than implementation. These tests are accurate, close to production, and robust against internal refactorings. Ideally, you only need to write one test.

Screen Shot 2020-04-11 at 22.51.32.png * It is recommended to pay attention to integration testing (= write real objects together and test everything at once) *

There is much more to say about this topic. Check out my blog post "Focus on Integration Tests Instead of Mock-Based Tests" for more information. ..

Do not use in-memory database for testing

Screen Shot 2020-04-11 at 22.57.41.png * If you use an in-memory database, you will be testing against a database that is different from your production environment *

Use an in-memory database for testing (H2, HSQLDB, [Fongo](https:: //github.com/fakemongo/fongo)) reduces reliability and test scope. In-memory databases and databases used in production may behave differently and return different results. As a result, testing based on an immature in-memory database does not guarantee the correct behavior of production applications. What's more, you easily run into situations where you can't use (or test) certain (database-specific) features. This is because in-memory databases do not support or behave differently. For more information on this, check out the article Don't use In-Memory Databases for Tests. please.

The solution is to run a test on the actual database. Fortunately, a library called Testcontainers provides a nice Java API for managing containers directly in your test code. To speed things up, see here (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#execution-speed).

Java/JVM

Use -noverify -XX: TieredStopAtLevel = 1

Always add the -noverify -XX: TieredStopAtLevel = 1 JVM option to your run settings. This saves 1-2 seconds of JVM startup time before the test is run. This is especially useful during the initial development of tests, where you run tests frequently through the IDE.

Update: Starting with Java 13, -noverify is deprecated. [^ nooverify] [^ nooverify]: This "update" is a faithful translation of "Update" in the original article, and was originally in the original article. This is not an update of the translated version you are reading.

Tip: IntelliJ IDEA allows you to add this argument to the "JUnit" launch configuration template, so you don't have to add an argument for each new execution configuration.

screenshot-idea-run-config-template-default-vm-options-marked.png

Use AssertJ

AssertJ is a very fluid [^ fluent] type secure API with a great variety of assertions and descriptive error messages. A powerful and mature assertion library. [^ fluent]: fluent. It means that you can write in a method chain. All the assertions you want are here. This keeps your test code short while avoiding the need to write complex assertion logic with loops and conditions. Here are some examples:

assertThat(actualProduct)
        .isEqualToIgnoringGivenFields(expectedProduct, "id");

assertThat(actualProductList).containsExactly(
        createProductDTO("1", "Smartphone", 250.00),
        createProductDTO("1", "Smartphone", 250.00)
);

assertThat(actualProductList)
        .usingElementComparatorIgnoringFields("id")
        .containsExactly(expectedProduct1, expectedProduct2);

assertThat(actualProductList)
        .extracting(Product::getId)
        .containsExactly("1", "2");

assertThat(actualProductList)
        .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));

assertThat(actualProductList)
        .filteredOn(product -> product.getCategory().equals("Smartphone"))
        .allSatisfy(product -> assertThat(product.isLiked()).isTrue());

Avoid ʻassertTrue () and ʻassertFalse ()

Avoid simple ʻassertTrue () and ʻassertFalse () assertions as they will output a mysterious error message:

//bad example
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);
expected: <true> but was: <false>

Instead, use AssertJ's assertion, which prints a good error message, without any special customization [^ out_of_the_box].

//Good example
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);
Expecting:
 <[Product[id=1, name='Samsung Galaxy']]>
to contain:
 <[Product[id=2, name='iPhone']]>
but could not find:
 <[Product[id=2, name='iPhone']]>

If you really have to check for booleans, [AssertJ's ʻas () `](http://joel-costigliola.github.io/assertj/assertj-core-" to improve the error message Consider using features-highlight.html). [^ out_of_the_box]: The original text is "out-of-the-box", which means that you can use it with the default settings. As you will see later, AssertJ allows you to customize the error message using the as () method, but I think it means that you will get an error message that you don't have to do.

Use JUnit 5

JUnit5 is a state-of-the-art technology for (unit) testing. It is actively developed and offers many powerful features (like parameterized tests, grouping, conditional tests, lifecycle control).

Use parameterized test

Parameterized tests allow one test to be run multiple times with different values. This way you can easily test multiple cases without adding any test code. JUnit 5 provides a great way to write such tests. @ValueSource, @EnumSource, @CsvSource, and @MethodSource.

//Good example
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
    client.perform(get("/products").param("token", invalidToken))
            .andExpect(status().is(400))
}

@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
    // ...
}

I strongly recommend using these extensively. Because you can test more cases with minimal effort.

Finally, I would like to emphasize the @CsvSource and @MethodSource that can be used for more advanced parameterized test scenarios where you can also control the expected value with parameters.

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}

@MethodSource is powerful when used in combination with a dedicated test object that contains all relevant test parameters and expected values. Unfortunately, in Java, writing these data structures (POJOs) is tedious. That's why I use Kotlin Data Classes to show an example of this feature below. is.

data class TestData(
    val input: String?,
    val expected: Token?
)

@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
}

private fun validTokenProvider() = Stream.of(
    TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
    TestData(input = "151175_13521", expected = Token(151175, "13521")),
    TestData(input = "151144375_id", expected = Token(151144375, "id")),
    TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
    TestData(input = null, expected = null)
)

Group tests

JUnit5's @ Nested is useful for grouping test methods. A sensible group can be a particular type of test (such as ʻInputIsXY, ʻErrorCases), or each method under a test into a group. (GetDesign and ʻUpdateDesign`)

public class DesignControllerTest {
    @Nested
    class GetDesigns {
        @Test
        void allFieldsAreIncluded() {}
        @Test
        void limitParameter() {}
        @Test
        void filterParameter() {}
    }
    @Nested
    class DeleteDesign {
        @Test
        void designIsRemovedFromDb() {}
        @Test
        void return404OnInvalidIdParameter() {}
        @Test
        void return401IfNotAuthorized() {}
    }
}

screenshot-group-test-methods.png

Easy-to-read test name with @DisplayName or Kotlin backticks

In Java, use JUnit 5's @DisplayName to write an easy-to-read test description.

public class DisplayNameTest {
    @Test
    @DisplayName("Design is removed from database")
    void designIsRemoved() {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404() {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401() {}
}

screenshot-displayname.png

In Kotlin, you can write the method name in the backticks, and you can also include a half-width space. This makes it easier to read while avoiding redundancy.

@Test
fun `design is removed from db`() {}

Mock remote services

In order to test the HTTP client, you need to mock the remote service. I prefer to use OkHttp's WebMockServer (https://github.com/square/okhttp/tree/master/mockwebserver) for that.

MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
        .addHeader("Content-Type", "application/json")
        .setBody("{\"name\": \"Smartphone\"}"));

ProductDTO productDTO = client.retrieveProduct("1");

assertThat(productDTO.getName()).isEqualTo("Smartphone");

Use Awaitility for asynchronous code assertions

Awaitility is a library for testing asynchronous code. It's easy to define how often you make assertions until you finally fail.

private static final ConditionFactory WAIT = await()
        .atMost(Duration.ofSeconds(6))
        .pollInterval(Duration.ofSeconds(1))
        .pollDelay(Duration.ofSeconds(1));

@Test
public void waitAndPoll(){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
        assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
    });
}

This way you can avoid using the unstable Thread.sleep () in your tests.

However, it's much easier to test the sync code. That's why you should [separate asynchronous execution from actual logic](#separate asynchronous execution from actual logic).

No bootstrap DI required (Spring)

(Spring) The DI framework bootstrap takes a few seconds to start testing. This slows down the feedback cycle, especially during the initial development of the test.

That's why I don't usually use DI in integration testing. I manually instantiate the required objects by calling new and put them together. If you're using constructor injection, it's pretty easy. Most of the time you want to test your business logic. No DI is needed for that. As an example, check out My Posts about Integration Testing (https://phauer.com/2019/focus-integration-tests-mock-based-tests/#integration-tests).

On the other hand, Spring Boot 2.2 will introduce features that make it easy to use lazy bean initialization, which should significantly speed up DI-based testing. [^ spring] [^ spring]: Released at the time of translation. Probably this.

Make your implementation testable

Do not use static access. never. From now on.

Static access is an anti-pattern. First, it obscures dependencies and side effects, makes the entire code difficult to understand, and makes it error-prone. Second, static access compromises testability. You can no longer exchange objects. But in testing, you want to use a mock or a real object with different settings (like DAO pointing to the test database).

So instead of statically accessing code, write that code in a non-static method, instantiate the class, and pass that object to the constructor of the required object.

//bad example
public class ProductController {
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = ProductDAO.getProducts();
        return mapToDTOs(products);
    }
}
//Good example
public class ProductController {
    private ProductDAO dao;
    public ProductController(ProductDAO dao) {
        this.dao = dao;
    }
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = dao.getProducts();
        return mapToDTOs(products);
    }
}

Fortunately, DI frameworks like Spring provide an easy way to avoid static access. Because it handles the creation and placement of all objects for us.

Parameterization

Let's test control all the relevant parts of the class. This can be achieved by creating constructor parameters from this aspect.

For example, suppose your DAO has an upper limit of 1000 queries. To test this limit, you would be required to generate 1001 database entries in your test. By making this upper limit a constructor parameter, the upper limit can be set. In a production environment, this parameter is 1000. In the test, you can set 2. Only three test entries are required to test its cap function.

Use constructor injection

Field injection is evil because it is less testable. You must * must * use the bootstrap of the DI environment in the test, or the hacky reflection magic. That's why constructor injection is the preferred method. This is because it makes it easier to control the dependent objects in your tests.

Java requires a few boilerplate.

//Good example
public class ProductController {

    private ProductDAO dao;
    private TaxClient client;

    public CustomerResource(ProductDAO dao, TaxClient client) {
        this.dao = dao;
        this.client = client;
    }
}

Kotlin makes the same thing more concise.

//Good example
class ProductController(
    private val dao: ProductDAO,
    private val client: TaxClient
){
}

ʻDo not use Instant.now ()ornew Date ()`

Don't do anything like get the current timestamp by calling ʻInstant.now ()ornew Date ()` in your production code. If you want to test that behavior.

//bad example
public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update()
            .set("dateModified", now);
        Query query = Query()
            .addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}

The problem is that the generated timestamp cannot be controlled by the test. You cannot assert the exact value because it will always be different for each test run. Instead, use Java's Clock class.

//Good example
public class ProductDAO {
    private Clock clock; 

    public ProductDAO(Clock clock) {
        this.clock = clock;
    }

    public void updateProductState(String productId, State state) {
        Instant now = clock.instant();
        // ...
    }
}

In testing, you can now generate a clock mock and pass it to ProductDAO to configure that clock mock to return a fixed timestamp. After calling ʻupdateProductState ()`, it asserts whether the defined timestamp has been inserted into the database.

Separate asynchronous execution from actual logic

Testing asynchronous code is tricky. Libraries like Awaitility can help, but it's still tedious and testing is still unstable. If possible, it makes sense to split the (often synchronous) business logic from asynchronous execution.

For example, you can put your business logic inside a ProductController and test it with a simple synchronous run. Asynchronous and parallel logic is aggregated in ProductScheduler and can be tested separately.

//Good example
public class ProductScheduler {

    private ProductController controller;

    @Scheduled
    public void start() {
        CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
        CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
        String usResult = usFuture.get();
        String germanyResult = germanyFuture.get();
    }
}

Kotlin My post about Best Practices for Unit Testing in Kotlin is a lot of Kotlin-specific for writing tests in Kotlin. Contains recommendations.

Recommended Posts

Modern best practices for Java testing
Modern best practices for Java testing
Modern Java environment for Windows using Chocolatey
For JAVA learning (2018-03-16-01)
2017 IDE for Java
Java for statement
Compare PDF output in Java for snapshot testing
[Java] Package for management
[Java] for statement / extended for statement
Best practices for log infrastructure design in container operations
Countermeasures for Java OutOfMemoryError
Try Easy Ramdom, a PropertyBase Testing tool for java
NLP for Java (NLP4J) (2)
NLP for Java (NLP4J) (1)
Java update for Scala users
Java debug execution [for Java beginners]
[Java] Precautions for type conversion
Books used for learning Java
Java Performance Chapter 2 Performance Testing Approach
2018 Java Proficiency Test for Newcomers-Basics-
Streamline Java testing with Spock
Java thread safe for you
[Java] Summary of for statements
Java for beginners, data hiding
[Java] Tips for writing source
Java installation location for mac
Java application for beginners: stream
Java while and for statements
What Java Engineers Choose for Browser "Automatic Testing"? Selenium? Selenide? Geb?