The Design Philosophy of Unit Testing
Programs are written primarily for humans to read, and only incidentally for machines to execute.
Programs must be written for people to read, and only incidentally for machines to execute.
The first step in writing code is naming, and the same applies to unit tests.
WWW
How can you make the names of your unit tests understandable? You need to reflect three elements in the naming, abbreviated as the WWW principle.
- What are you testing? (what)
- Under what conditions are you testing? (when)
- What behavior do you expect? (want)
Let's take a look at a unit test example from the MJGA scaffolding.
@Test
void createVerifyGetSubjectJwt_givenUserIdentify_shouldReturnTrueAndGetExpectIdentify() {
String jwt = cookieJwt.createJwt("1");
assertThat(cookieJwt.verifyToken(jwt)).isTrue();
assertThat(cookieJwt.getSubject(jwt)).isEqualTo("1");
}
Breaking down the name into three parts:
-
createVerifyGetSubjectJwt
reflects the object being tested. -
givenUserIdentify
indicates the condition under which the test is conducted. -
shouldReturnTrueAndGetExpectIdentify
describes the expected behavior.
This is a good nameโone that a business person can understand.
AAA
The first hurdle is over; now let's implement the test logic. In fact, with the WWW naming design, the logic implementation already has a clear path. Like naming, test logic also has principles to follow, commonly known as the AAA principle.
- Arrange
- Act
- Assert
Let's continue with a test case from the MJGA scaffolding:
@Test
@WithMockUser
void signIn_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
String stubUsername = "test_04cb017e1fe6";
String stubPassword = "test_567472858b8c";
SignInDto signInDto = new SignInDto();
signInDto.setUsername(stubUsername);
signInDto.setPassword(stubPassword);
when(signService.signIn(signInDto)).thenReturn(1L);
mockMvc
.perform(
post("/auth/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"username": "test_04cb017e1fe6",
"password": "test_567472858b8c"
}
""")
.with(csrf()))
.andExpect(status().isOk());
}
Arrange
This is the preparation phase of the test. In this phase, you need to construct some code that provides context, such as inserting data, building objects, or even more complex mocks and stubs. In the example, we constructed the SignInDto object for subsequent use and assumed the return value of the signIn methodโall in preparation for the subsequent test.
Act
This is the execution phase, corresponding to mockMvc.perform
in the example. The API is just a bit complex here, so you can think of it as a single line of code.
Assert
You need to clearly state what result you expect. In the example, this corresponds to .andExpect(status().isOk());
.
Often, you may need to write some test methods to verify whether a certain logic can run without errors, as the method itself has no return value. This is easy to solve using assertDoesNotThrow
.
// pause & resume job
JobKey firstDataBackupJobKey = dataBackupJobKeys.iterator().next();
assertDoesNotThrow(
() -> {
dataBackupScheduler.pauseJob(firstDataBackupJobKey);
dataBackupScheduler.resumeJob(firstDataBackupJobKey);
});
These coding philosophies are independent of the language and framework you use. Any unit test is written with this mindset. More content on unit testing will be shared in the future.
Final Words
- I am Chuck1sn, a developer long committed to promoting the modern JVM ecosystem.
- Your replies, likes, and bookmarks are the motivation for my continuous updates.
- A simple triple action (like, comment, share) means a lot to me and is greatly appreciated!
- Follow my account to receive article updates as soon as they are published.
PS: All the code examples above can be found in the Github repository. If it helps, please give it a Starโit's a great encouragement to me. Thank you!
Top comments (0)