Unit Testing in Swift – Resolving Cyclical Dependencies

swift-languagetddunit testing

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:

  1. An initializer that constructs empty bit vectors
  2. A count property of type Int
  3. An isEmpty property of type Bool
  4. An equality operator (==). NB: this is a value-equality operator akin to Object.equals() in Java, not a reference equality operator like == in Java.

I'm running into a bunch of cyclical dependancies:

  1. The unit test that tests my initializer need to verify that the newly constructed BitVector. It can do so in one of 3 ways:

    1. Check bv.count == 0
    2. Check bv.isEmpty == true
    3. Check that bv == knownEmptyBitVector

    Method 1 relies on count, method 2 relies on isEmpty (which itself relies on count, so there's no point using it), method 3 relies on ==. In any case, I can't test my initializer in isolation.

  2. The test for count needs to operate on something, which inevitably tests my initializer(s)

  3. The implementation of isEmpty relies on count

  4. The implementation of == relies on count.

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 on count (or whatever other relationships you might have): all you should be caring about is the public interface. For example, you can have three tests:

  • That a newly initialized object has count == 0.
  • That a newly initialized object has isEmpty == true
  • That a newly initialized object equals the known empty object.

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 on count - 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.