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 a Stack

    • What should happen when pop is called on an empty Stack?

      • Throw an exception

    • What should happen when pop is called on a Stack with one element in it?

      • Return the top and result in an empty stack

    • What should happen when pop is called on a Stack 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 states

  • Perhaps the simplest state to test under is when the ArrayStack is empty

  • Below are example unit tests for all methods within the ArrayStack class when it is empty

  • Consider, for example, the two push related tests

    • There 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 the ArrayStackTest class

  • The below code shows an example of the setup method with the two push unit tests updated to not include the creation of the ArrayStack

    • For brevity, only the two push tests are shown

    • This 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 it

  • Notice the inclusion of the new class field called preState

  • This preState will effectively be a duplicate of classUnderTest that can be used to check that a method had no side effect

  • Consider peek_singleton_unchanged for an example

    • Calling peek should not have any side effect; it should not mutate the object in any way

    • To verify this, one can assert equality between preState and classUnderTest after calling peek on classUnderTest

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 code

    • This 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 tests

  • Further, 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 tests

  • But the addSingleton() setup code only runs before the singleton tests, but after createStack() is run

    • This way the ArrayStack instances exist before the pushing takes place inside addSingleton()

 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 pattern

    • Nested 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 class

    • This 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