I'm trying to practice TDD, by using it to develop a simple like Bit Vector. I happen to be using Swift, but this is a language-agnostic question.
My BitVector
is a struct
that stores a single UInt64
, and presents an API over it that lets you treat it like a collection. The details don't matter much, but it's pretty simple. The high 57 bits are storage bits, and the lower 6 bits are "count" bits, which tells you how many of the storage bits actually store a contained value.
So far, I have a handful of very simple capabilities:
- An initializer that constructs empty bit vectors
- A
count
property of typeInt
- An
isEmpty
property of typeBool
- An equality operator (
==
). NB: this is a value-equality operator akin toObject.equals()
in Java, not a reference equality operator like==
in Java.
I'm running into a bunch of cyclical dependancies:
-
The unit test that tests my initializer need to verify that the newly constructed
BitVector
. It can do so in one of 3 ways:- Check
bv.count == 0
- Check
bv.isEmpty == true
- Check that
bv == knownEmptyBitVector
Method 1 relies on
count
, method 2 relies onisEmpty
(which itself relies oncount
, so there's no point using it), method 3 relies on==
. In any case, I can't test my initializer in isolation. - Check
-
The test for
count
needs to operate on something, which inevitably tests my initializer(s) -
The implementation of
isEmpty
relies oncount
-
The implementation of
==
relies oncount
.
I was able to partly solve this problem by introducing a private API that constructs a BitVector
from an existing bit pattern (as a UInt64
). This allowed me to initialize values without testing any other initializers, so that I could "boot strap" my way up.
For my unit tests to truly be unit tests, I find myself doing a bunch of hacks, which complicate my prod and test code substantially.
How exactly do you get around these sorts of issues?
Best Answer
You're worrying about implementation details too much.
It doesn't matter that in your current implementation,
isEmpty
relies oncount
(or whatever other relationships you might have): all you should be caring about is the public interface. For example, you can have three tests:count == 0
.isEmpty == true
These are all valid tests, and become especially important if you ever decide to refactor the internals of your class so that
isEmpty
has a different implementation that doesn't rely oncount
- so long as your tests all still pass, you know you haven't regressed anything.Similar stuff applies to your other points - remember to test the public interface, not your internal implementation. You may find TDD useful here, as you'd then be writing the tests you need for
isEmpty
before you'd written any implementation for it at all.