- The document provides guidelines for effective unit testing, including what should and should not be unit tested. Specifically, it recommends unit testing "control" classes that contain processing logic, but not "entity" or "boundary" classes without logic.
- It outlines criteria for good unit tests, including testing single features in isolation and having understandable test names and structure. The AAA (Arrange-Act-Assert) pattern is recommended to clearly separate test setup, execution and validation.
- Test names should indicate the target method, expected result and initial state. Code within tests should use comments and "SUT" naming to distinguish the test target from other objects. Self-contained and independent tests are preferred over code reuse across tests
2. Table of Contents
1. What NOT to Unit Test
2. What to Unit Test then?
3. Criteria of a Good Unit Test
4. Unit Test Method Name
5. Unit Test Content
3. 1. What NOT to Unit Test
u Before we delve into this style guide, we need to know what not to unit test.
"What not to test" is very important because any unnecessary test code in our
code base will create noise in our code base and increase our maintenance
cost.
4. u We can roughly divide all classes into 3 types: Entity, Control, Boundary
(http://www.cs.sjsu.edu/~pearce/modules/patterns/enterprise/ecb/ecb.ht
m).
u Entity - Data Container, e.g. DTO
u Boundary - Classes that communicate directly to external services, via network
connection, e.g. REST API, DAO, HttpClient, MessageHandler etc.
u Control - Classes that normally make decision.
6. u Out of these 3 types of classes, in general, we don't need to write unit test
for:
u Entity – Since it contains only logic-less getters and setters method, there is no
value in testing all these getters / setters.
u Boundary - We need to cover boundary class with integration test, we just don't
need to write unit test for it. If you need to unit test it just because your boundary
class (handler class etc.) contains processing logic, you probably need to delegate
the processing logic into control class.
7. u There are some exceptions for the rules above. We may need to write unit
test for the following conditions:
u If the Entity class has validation rules. We need unit test validation logic.
u If the Boundary class has event broadcasting capability, as the example below. We
need to verify that new event is being broadcasted.
u publishEvent(new DataUpdatedEvent()); // example
8. 2. What to Unit Test then?
u Control - It makes decision, it has processing logic inside. It's like our brain.
We need to make sure it's processing logic is correct and yield correct output.
9. 3. Criteria of a Good Unit Test
u A good unit test satisfy 3 criteria:
1. Test a single feature in isolation.
2. Easy to understand by other engineers.
3. Help to identify issue effectively during test failure.
u Most unit tests satisfy #1. This styling guide will focus on #2 and #3.
10. 4. Unit Test Method Name
u A test method name should consist of
<targetMethod>_<expectedResult>_<initialState>.
u Example:
@Test
public void computeTotalWidth_shouldReturnTotalWidth_givenPositiveNumbers() {…}
@Test
public void computeTotalWidth_shouldThrowException_givenNegativeNumbers() {…}
@Test
public void generate_shouldGenerateXYZ_forABConly() {…}
12. u Engineers can fix test failures with confidence if they understand what is the
correct behavior of the code.
u Code reviewers on the other hand can easily spot on any missing test cases.
u As a side benefit, this naming style help programmer to keep his/her mind
flowing when he/she is writing the test. For example, when he/she is stuck at
what to test, he/she can think out loud saying "this method should return
something when xyz is given”.
u In contrast, using names like “testComputeTotalWidth()” gives less context
about it’s intent.
13. 5. Unit Test Content
1. We use AAA style where every unit test has 3 sections - Arrange, Act,
Assert:
u Arrange - Set up initial state (this will normally occupy more than 80% of your test
code)
u Act - Perform action
u Assert - Verify the result
2. It is important to clearly identify these three sections with
comments, allowing us to quickly jump between the “arrange", "act" and
"assert" sections.
3. We use SUT (System Under Test) to denote target instance.
u This helps us identify the target instance from a swarm of mock objects.
14. ✔️Do this
@Test
public void testSearch_shouldReturnTargetIndex_ifFoundInInputValues() {
// 1. arrange
List<Integer> sortedNumbers = Arrays.asList(1, 2, 3);
int targetValue = 2;
BinarySearcher sut = new BinarySearcher();
// 2. assert
int targetIndex = sut.search(sortedNumbers, targetValue);
// 3. assert
Assertions.assertThat(targetIndex).isEqualTo(1);
}
15. ✔️ AAA Pattern
@Test
public void test() {
// 1. arrange
final int TARGET_VALUE = 2;
Node node1 = new Node(8);
Node node2 = new Node(4);
Node node3 = new Node(5);
Node node4 = new Node(3);
Node node5 = new Node(9);
Node node6 = new Node(2);
node1.addChildNode(node2);
node1.addChildNode(node3);
node3.addChildNode(node4);
node1.addChildNode(node5);
node5.addChildNode(node6);
BFSearcher sut = new BFSearcher();
// 2. act
Node node = sut.search(node1, TARGET_VALUE);
// 3. assert
Assertions.assertThat(node).isEqualTo(node6);
}
❌ Without AAA Patter
@Test
public void test() {
final int TARGET_VALUE = 2;
Node node1 = new Node(8);
Node node2 = new Node(4);
Node node3 = new Node(5);
Node node4 = new Node(3);
Node node5 = new Node(9);
Node node6 = new Node(2);
node1.addChildNode(node2);
node1.addChildNode(node3);
node3.addChildNode(node4);
node1.addChildNode(node5);
node5.addChildNode(node6);
BFSearcher sut = new BFSearcher();
Node node = sut.search(node1, TARGET_VALUE);
Assertions.assertThat(node).isEqualTo(node6);
}
16. u AAA Pattern:
u The Arrange section of AAA takes up over 80% of the code in our test, but we can
still easily jump to Act sections by looking at the comments.
u ”sut" naming makes the test target stand out.
u Without AAA Pattern
u We have to start reading the code from beginning.
u Test target buried in the sea of mock objects.
17. 1. A test should be self-contained. We want to understand the code fast. We
don't want to jump around different methods to understand the test flow.
u It is preferred that you do not follow the DRY (Don't Repeat Yourself) principle.
Striving for code reuse will lead to tightly coupled and inflexible test, which
discourages refactoring.
18. ❌ Don’t do this
// Do not do this. Code reader has to scroll
// up and down to understand the whole test logic.
@Test
public void testXYZ(){
// 1. arrange
setup1();
setup2();
doSomething();
Target sut = new Target();
// 2. act
actualResult = sut.doSomething();
// 3. assert
Asserts.assertThat(222, actualResult);
}
private void setup1(){ … }
private void setup2(){ … }
private void doSomething(){ … }