24

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?

  • 20
    You are taking a too narrow view on the term "unit". BitVectoris a perfectly fine unit size for unit testing and immediately resolves your issues that public members of BitVector need each other to make meaningful tests. – Bart van Ingen Schenau Jul 29 at 9:09
  • You know too much implementation details up front. Is your development really test-driven? – herby Jul 30 at 20:44
  • @herby No, that's why I'm practicing. Although that seems like a really unattainable standard. I don't thing I've ever programmed anything without a pretty clear mental approximation of what the implementation will entail. – Alexander Jul 30 at 20:50
  • @Alexander You should try to relax that, otherwise it will be test-first, but not test-driven. Just say vague "I'll do a bit vector with one 64bit int as a backing store" and that's it; from that point on do TDD red-green-refactor one after another. Implementation details, as well as API, should emerge from trying to make tests run (the former), and from writing those tests in the first place (the latter). – herby Jul 30 at 20:54
66

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.

  • 6
    @Alexander You sound like a man in need of a clear definition of unit testing. The best one I know comes from Michael Feathers – candied_orange Jul 29 at 1:16
  • 14
    @Alexander you are treating each method as an independently testable piece of code. That is the source of your difficulties. These difficulties disappear if you test the object as a whole, without trying to divide it into smaller parts. Dependencies between objects are not comparable with dependencies between methods. – amon Jul 29 at 7:36
  • 9
    @Alexander "a piece of code" is an arbitrary measurement. Just by initializing a variable you are using many "pieces of code." What matters is that you are testing a cohesive behavioural unit as defined by you. – Ant P Jul 29 at 8:40
  • 9
    "From what I've read, I got that the impression that if you break only piece of code, only unit tests directly relating to that code should fail." That seems to be a very hard rule to follow. (e.g. if you write a vector class, and you make an error on the index method, you will probably have tons of breakage across all the code that uses that vector class) – jhominal Jul 29 at 9:45
  • 4
    @Alexander Also, look into the "Arrange, Act, Assert" pattern for tests. Basically, you set up the object in whatever state it needs to be in (Arrange), call the method you're actually testing (Act) and then verify that its state changed according to your expectations. (Assert). Stuff you set up in Arrange would be "preconditions" for the test. – GalacticCowboy Jul 29 at 15:18
5

How exactly do you get around these sorts of issues?

You revise your thinking on what a "unit test" is.

An object that manages a mutable data in memory is fundamentally a state machine. So any valuable use case is going to, at a minimum, invoke a method to put information into the object, and invoke a method to read a copy of information out of the object. In the interesting use cases, you are also going to be invoking additional methods that change the data structure.

In practice, this often looks like

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

or

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

The "unit test" terminology -- well, it has a long history of not being very good.

I call them unit tests, but they don't match the accepted definition of unit tests very well -- Kent Beck, Test Driven Development by Example

Kent wrote the first version of SUnit in 1994, the port to JUnit was in 1998, the first draft of the TDD book was early 2002. The confusion had a lot of time to spread.

The key idea of these tests (more accurately called "programmer tests" or "developer tests") is that the tests are isolated from each other. The tests don't share any mutable data structures, so they can be run concurrently. There are no worries that the tests must be run in a specific order to correctly measure the solution.

The primary use case for these tests is that they are run by the programmer between edits to her own source code. If you are performing the red green refactor protocol, an unexpected RED always indicates a fault in your last edit; you revert that change, verify that the tests are GREEN, and try again. There isn't a lot of advantage in trying to invest in a design where each and every possible bug is caught by only one test.

Of course, is a merge introduces a fault, then finding that fault is no longer trivial. There are various steps you can take to ensure that faults are easy to localize. See

1

In general (even if not using TDD) you should strive to write tests as much as possible while pretending you don't know how it is implemented.

If you're actually doing TDD that should already be the case. Your tests are an executable specification of the program.

How the call graph looks underneath the tests is irrelevant, as long as the tests themselves are sensible and well maintained.

I think your problem is your understanding of TDD.

Your problem in my opinion is that you are "mixing" your TDD personas. Your "test", "code", and "refactor" personas operate completely independently of each other, ideally. In particular your coding and refactoring personas have no obligations to the tests other than to make/keep them running green.

Sure, in principle, it would be best if all tests were orthogonal and independent of each other. But that is not a concern of your other two TDD personas, and it is definitely not a strict or even necessarily realistic hard requirement of your tests. Basically: Don't throw out your common sense feelings about code quality to try to fulfill a requirement that nobody is asking of you.

protected by gnat Jul 30 at 11:25

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.