I am not very good at writing unit tests so I have a dilemma.
I have a binary_search_tree
and I am writing tests for insert
and contains
methods:
struct BinarySearchTree {};
TEST(BinarySearchTree, Insert) {
BinarySearchTree t;
t.insert(5);
EXPECT_TRUE(t.contains(5));
}
TEST(BinarySearchTree, Contains) {
BinarySearchTree t;
t.insert(5);
EXPECT_FALSE(t.contains(2);
EXPECT_TRUE(t.contains(5);
}
As you can see both of them are related each other and these test cases may be totally equal because:
- We test insertion by inserting element and checking does the tree contain it.
- We test existense of element by inserting it and checking does the tree contain it.
So I have 2 options:
- Don’t write tests for contains because it will be tested in { insertion, deletion, replacement, etc } tests.
- Write almost same content in all of these tests.
What is better?
1
Unit tests test a code unit, which may be larger than a method. It is usually not possible to test setter–getter and constructor–getter pairs separately, and that is OK. You’ll naturally have test cases like “contains inserted elements” and “doesn’t contain elements that weren’t inserted”. In such cases, I focus my tests on that method which modifies the structure. Here, I’d probably use a single test case for contains–insert.
TEST(BinarySearchTree, ContainsInsertedElements) {
BinarySearchTree t;
EXPECT_FALSE(t.contains(5)); // precondition
t.insert(5); // act
EXPECT_TRUE(t.contains(5)); // assert
}
(and another case that inserting the same element again also works, and a high-level non-unit test case that I can insert multiple numbers which should produce an expected tree structure.)
For contains
, I might switch to more white-boxy tests. In particular, insert and contains will probably have a common find operation that could be tested more carefully, and we can then assume that contains is likely correct as well.
Essentially duplicating a test just because the test body uses two methods does not seem like a good idea. Tests are code, and guidelines such as “don’t repeat yourself” still apply (within reason). Also, duplicate tests don’t add value: if one fails the other does too. It is better to focus on more general contracts of the unit under test, rather than narrowly focussing on each method no matter how useless it might be in isolation.
5
Your test cases, if done well, serve two purposes:
- They check that no existing functionality has broken when making changes to the code in the future;
- They document how the code is to be used and how it behaves.
If you just had one check,
TEST(BinarySearchTree, Insert) {
BinarySearchTree t;
t.insert(5);
EXPECT_FALSE(t.contains(2));
EXPECT_TRUE(t.contains(5));
}
Then it fails to achieve the second benefit: EXPECT_FALSE(t.contains(2));
is buried inside an Insert
check and so isn’t obvious to the user.
So whilst it is slightly more code to write the two tests, it’s much clearer what the tests are doing if you do. That’s a huge advantage when compared to just saving a few lines of code.