9. Unit Testing Collections
There has been an emphasis on writing code defensively
Ideally, code is written in such a way that is, not only correct, but easy to test, understand, and reason about
For example:
Immutability — If the data cannot change, it makes the program more predictable
Avoiding side effects — Functions/methods do not modify anything
Functions operate independently — Self contained functionality is easier to understand and use
However, collections pose a problem
Mutable — their states change constantly through adding and removing data
Side effects — their methods may have side effects (modifying the contents of the collection)
Interconnected methods — Adding and removing data from a collection are inextricably connected
Additionally, their expected behaviour may be different depending on their state
Consider the
pop
method for aStack
What should happen when
pop
is called on an emptyStack
?Throw an exception
What should happen when
pop
is called on aStack
with one element in it?Return the top and result in an empty stack
What should happen when
pop
is called on aStack
in a more general case?Return the top and result in a changed stack
9.1. Empty Stack Tests
To simplify the process of writing unit tests for the
ArrayStack
, start with one of the statesPerhaps the simplest state to test under is when the
ArrayStack
is emptyBelow are example unit tests for all methods within the
ArrayStack
class when it is emptyConsider, for example, the two
push
related testsThere is a test that it returns what is expected
There is also a test to check that it had the proper side effect
This is an example of the added complexity of testing a method with a side effect
One cannot simply test the output given the input
There is also a necessity to test the side effects
1@Test
2void push_empty_returnsTrue() {
3 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
4 assertTrue(classUnderTest.push(11));
5}
6
7@Test
8void push_empty_newTop() {
9 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
10 classUnderTest.push(11);
11 assertEquals(11, classUnderTest.peek());
12}
13
14@Test
15void pop_empty_throwsNoSuchElementException() {
16 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
17 assertThrows(NoSuchElementException.class, () -> classUnderTest.pop());
18}
19
20@Test
21void peek_empty_throwsNoSuchElementException() {
22 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
23 assertThrows(NoSuchElementException.class, () -> classUnderTest.peek());
24}
25
26@Test
27void isEmpty_empty_returnsTrue() {
28 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
29 assertTrue(classUnderTest.isEmpty());
30}
31
32@Test
33void size_empty_returnsZero() {
34 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
35 assertEquals(0, classUnderTest.size());
36}
37
38@Test
39void toString_empty_returnsEmptyString() {
40 ArrayStack<Integer> classUnderTest = new ArrayStack<>();
41 assertEquals("", classUnderTest.toString());
42}
Note
In the above tests, the name of the reference variable to the ArrayStack
being tested is classUnderTest
.
This is a common convention — use the name classUnderTest
for the object being used to test the class.
9.1.1. Common Setup Code
Notice that each of the above example tests starts with the exact same code
The creation of an
ArrayStack
ArrayStack<Integer> classUnderTest = new ArrayStack<>();
To help simplify each of the individual tests, this setup code can be removed and added to its own method
This method will be annotated with
@BeforeEach
To make use of this, however, the variable
classUnderTest
will be made a field for theArrayStackTest
classThe below code shows an example of the setup method with the two
push
unit tests updated to not include the creation of theArrayStack
For brevity, only the two
push
tests are shownThis would work similarly for each of the previously discussed empty tests
1private ArrayStack<Integer> classUnderTest;
2
3@BeforeEach
4void createStack() {
5 classUnderTest = new ArrayStack<>();
6}
7
8@Test
9void push_empty_returnsTrue() {
10 assertTrue(classUnderTest.push(11));
11}
12
13@Test
14void push_empty_newTop() {
15 classUnderTest.push(11);
16 assertEquals(11, classUnderTest.peek());
17}
18
19.
20.
21.
9.2. Singleton Case Stack Tests
Below are example unit tests for all methods within the
ArrayStack
class when it has a single element within itNotice the inclusion of the new class field called
preState
This
preState
will effectively be a duplicate ofclassUnderTest
that can be used to check that a method had no side effectConsider
peek_singleton_unchanged
for an exampleCalling
peek
should not have any side effect; it should not mutate the object in any wayTo verify this, one can assert equality between
preState
andclassUnderTest
after callingpeek
onclassUnderTest
Note
Although preState
is only actually used on one of the tests so far, it is convenient to always have an
equivalent object to classUnderTest
within the test class. For this reason, preState
is made a class field
and is always setup the same way as classUnderTest
.
1private ArrayStack<Integer> classUnderTest;
2private ArrayStack<Integer> preState;
3
4@BeforeEach
5void createStack() {
6 classUnderTest = new ArrayStack<>();
7 preState = new ArrayStack<>();
8}
9
10// Empty tests excluded here
11
12@Test
13void push_singleton_returnsTrue() {
14 classUnderTest.push(10);
15 preState.push(10);
16 assertTrue(classUnderTest.push(11));
17}
18
19@Test
20void push_singleton_newTop() {
21 classUnderTest.push(10);
22 preState.push(10);
23 classUnderTest.push(11);
24 assertEquals(11, classUnderTest.peek());
25}
26
27@Test
28void pop_singleton_returnsTop() {
29 classUnderTest.push(10);
30 preState.push(10);
31 assertEquals(10, classUnderTest.pop());
32}
33
34@Test
35void pop_singleton_emptyStack() {
36 classUnderTest.push(10);
37 preState.push(10);
38 classUnderTest.pop();
39 assertEquals(new ArrayStack<>(), classUnderTest);
40}
41
42@Test
43void peek_singleton_returnsTop() {
44 classUnderTest.push(10);
45 preState.push(10);
46 assertEquals(10, classUnderTest.peek());
47}
48
49@Test
50void peek_singleton_unchanged() {
51 classUnderTest.push(10);
52 preState.push(10);
53 classUnderTest.peek();
54 assertEquals(preState, classUnderTest);
55}
56
57@Test
58void isEmpty_singleton_returnsFalse() {
59 classUnderTest.push(10);
60 preState.push(10);
61 assertFalse(classUnderTest.isEmpty());
62}
63
64@Test
65void size_singleton_returnsOne() {
66 classUnderTest.push(10);
67 preState.push(10);
68 assertEquals(1, classUnderTest.size());
69}
70
71@Test
72void toString_singleton_returnsCorrectString() {
73 classUnderTest.push(10);
74 preState.push(10);
75 assertEquals("10, ", classUnderTest.toString());
76}
Note
When considering peek_singleton_returnsTop
, it may become clear that push_empty_newTop
and
push_singleton_newTop
are redundant tests since the peek_singleton_returnsTop
will test peek
after a
push
has happened anyways, effectively checking that a push
results in the expected top. When looking at the
code in the tests, it’s clear that the tests are effectively identical. Thus, it is not really necessary to include
any of the push
causing a new top test.
1@Test 2void push_empty_newTop() { 3 classUnderTest.push(11); 4 assertEquals(11, classUnderTest.peek()); 5} 6 7@Test 8void peek_singleton_returnsTop() { 9 classUnderTest.push(10); 10 assertEquals(10, classUnderTest.peek()); 11}
This highlights the complexities caused by the interconnectedness of the collection’s methods — one cannot test
that push
results in a new top without using peek
, and one cannot test peek
without having already
called push
.
9.2.1. Nested Test Classes
Notice that, once again, each of these tests have the same setup code
classUnderTest.push(10);
Unfortunately, unlike with the empty tests, one cannot simply add this to the existing
@BeforeEach
set up codeThis would break the empty tests since the stack will have something added before each test is run
With nested test classes, there is a way to add another
@BeforeEach
setup code that applies to the singleton tests and not the empty testsFurther, this strategy helps group the tests together nicely
Below is an example of using the nested test classes
The
createStack()
setup code will be run before the empty tests and singleton testsBut the
addSingleton()
setup code only runs before the singleton tests, but aftercreateStack()
is runThis way the
ArrayStack
instances exist before the pushing takes place insideaddSingleton()
1private ArrayStack<Integer> classUnderTest;
2private ArrayStack<Integer> preState;
3
4@BeforeEach
5void createStack() {
6 classUnderTest = new ArrayStack<>();
7 preState = new ArrayStack<>();
8}
9
10@Nested
11class WhenNewEmpty {
12
13 @Test
14 void push_empty_returnsTrue() {
15 assertTrue(classUnderTest.push(11));
16 }
17
18 @Test
19 void pop_empty_throwsNoSuchElementException() {
20 assertThrows(NoSuchElementException.class, () -> classUnderTest.pop());
21 }
22
23 // Remaining empty tests excluded here
24
25 @Nested
26 class WhenSingleton {
27
28 @BeforeEach
29 void addSingleton() {
30 classUnderTest.push(10);
31 preState.push(10);
32 }
33
34 @Test
35 void push_singleton_returnsTrue() {
36 assertTrue(classUnderTest.push(11));
37 }
38
39 @Test
40 void pop_singleton_returnsTop() {
41 assertEquals(10, classUnderTest.pop());
42 }
43
44 @Test
45 void pop_singleton_emptyStack() {
46 classUnderTest.pop();
47 assertEquals(new ArrayStack<>(), classUnderTest);
48 }
49
50 // Remaining empty tests excluded here
51
52 }
53}
9.3. General Case Stack Tests
The tests for the more general case of an
ArrayStack
with several elements within it are going to follow the same patternNested test class
Common setup code with a
BeforeEach
In the below code, a noteworthy difference is the use of the
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
annotation before the nested test classThis specifies that a new test instance is created once per test class
For now, do not worry about this detail too much
1private ArrayStack<Integer> classUnderTest;
2private ArrayStack<Integer> preState;
3
4@BeforeEach
5void createStack() {
6 classUnderTest = new ArrayStack<>();
7 preState = new ArrayStack<>();
8}
9
10@Nested
11class WhenNewEmpty {
12
13 @Test
14 void push_empty_returnsTrue() {
15 assertTrue(classUnderTest.push(11));
16 }
17
18 // Remaining empty tests excluded here
19
20
21 @Nested
22 class WhenSingleton {
23
24 @BeforeEach
25 void addSingleton() {
26 classUnderTest.push(10);
27 preState.push(10);
28 }
29
30 @Test
31 void push_singleton_returnsTrue() {
32 assertTrue(classUnderTest.push(11));
33 }
34
35 // Remaining empty tests excluded here
36
37
38 @Nested
39 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
40 class WhenMany {
41
42 @BeforeEach
43 void addMany() {
44 classUnderTest.push(20);
45 classUnderTest.push(30);
46 classUnderTest.push(40);
47 preState.push(20);
48 preState.push(30);
49 preState.push(40);
50 }
51
52 @Test
53 void push_many_returnsTrue() {
54 assertTrue(classUnderTest.push(11));
55 }
56
57 @Test
58 void pop_many_returnsTop() {
59 assertEquals(40, classUnderTest.pop());
60 }
61
62 @Test
63 void pop_many_newTop() {
64 classUnderTest.pop();
65 assertEquals(30, classUnderTest.peek());
66 }
67
68 @Test
69 void peek_many_returnsTop() {
70 assertEquals(40, classUnderTest.peek());
71 }
72
73 @Test
74 void peek_many_unchanged() {
75 classUnderTest.peek();
76 assertEquals(preState, classUnderTest);
77 }
78
79 @Test
80 void isEmpty_many_returnsFalse() {
81 assertFalse(classUnderTest.isEmpty());
82 }
83
84 @Test
85 void size_many_returnsCorrectSize() {
86 assertEquals(4, classUnderTest.size());
87 }
88
89 @Test
90 void toString_many_returnsCorrectString() {
91 assertEquals("40, 30, 20, 10, ", classUnderTest.toString());
92 }
93 }
94 }
95}
Note
The above suggested layout is by no means the correct way or a standard for testing collections. It is simply a strategy to help manage the complexities of testing collections.
9.4. For Next Time
Finish reading Chapter 3
16 pages
9.4.1. Playing Code
Download and play with