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
By making heavy use of helper functions, parameterized tests, and powerful assertions in AssertJ, not overusing variables, verifying only the relevance, and avoiding writing tests for rare cases. ** Write a small, clear test. ** **
Write a self-contained test by clarifying all relevant parameters, inserting data correctly, and using composition rather than inheritance. ** **
Write a dump test [^ dumptest] by avoiding reuse of production code and focusing on comparing output values with hard-coded values. ** ** [^ dumptest]: I wasn't sure what the test was, but it seems like a test that hardcodes the expected value.
KISS principle> DRY principle
Focus on [^ slide] testing full vertical slides, avoid using in-memory databases, and ** write tests that are close to production. ** ** [^ slide]: It seems to be an integration test.
JUnit 5 and Assert J are very good choices.
Avoid static access, use constructor injection, use Clock
[^ clock], and separate your business logic from asynchronous execution, and work hard to make your implementation easy to test.
[^ clock]: The java.time.Clock class.
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");
}
//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
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).
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");
}
createProductWithCategory ()
) and for complex assertions. Only pass parameters that are relevant to your test to helper functions. For other values, use the appropriate default values. In Kotlin, this is easy to do with the default arguments. In Java, you need to use method chains and overloads to achieve pseudo-default argumentsìnsertIntoDatabase ()
)//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')}")
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.
//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.
//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.
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:
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);
? 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");
assertThat(actualProduct.getPrice()).isEqualTo(100);
//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"));
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.
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
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));
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.
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.
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.
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.
* 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. ..
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
-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.
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());
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.
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).
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)
)
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() {}
}
}
@ Nested
in JUnit 5 *@DisplayName
or Kotlin backticksIn 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() {}
}
@DisplayName
of JUnit5 *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`() {}
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");
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).
(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.
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.
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.
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
){
}
or
new Date ()`Don't do anything like get the current timestamp by calling ʻInstant.now ()or
new 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.
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