Question

I'm new to TDD, and relatively new to software development in general (e.g. < 4 years experience), but I am trying to learn.

I have been toying with TDD but ran into what I know realize is a common problem. I have been testing both higher level API contracts, sometimes with integration tests when necessary, and lower level implementation dependent unit tests.

I recently watched a talk by Ian Cooper who suggests that we should not test internals, only what the intended actual functionality of the program (e.g. whatever the contract of the program is). Note that I am paraphrasing here, and may have missed the message.

It makes sense to me that it would help by allowing to refactor without worrying about tests that break the internals, but at the same time it feels weird not testing a lot of the internal functions (private or not) that I write to help me achieve the bigger functionality at hand. Furthermore, sometimes if I try to write tests that only test at a high contract level, the tests become too large/complex.

Also doesn't this contradict the major push for tons of unit tests and that in general the more unit tests the better because they help us identify the specific problem when they fail rather than being omnibus?

How do I balance this? Is it just a balancing act that I will learn over time through experience?

Was it helpful?

Solution

Also doesn't this contradict the major push for tons of unit tests and that in general the more unit tests the better because they help us identify the specific problem when they fail rather than being omnibus?

It sort of does, yes, but this is not necessarily a bad thing.

One of the key benefits of TDD is that it not only helps you define your contracts but also what is meaningful as a "unit."

You write a test for a particular piece of functionality at a particular level, covering whatever you feel is a meaningful behaviour.

If you overfit your tests - as the above mantra would have you - not only do you end up with tests that really don't describe anything meaningful, you also end up with code that is very difficult to refactor and reorganise because every small change in production code ends up a breaking change for tests.

So, let the internals be the internals - that might be a method, a class, or a cluster of cohesive classes. How do you define which? You don't. You find where you feel is a comfortable starting point and you write a test that outlines the first thing you want to do.

Then you go with the flow.

As a unit grows, you might find you need to add some abstraction or reorganise the internals of the unit in order to extend the behaviour to satisfy some new tests. Fine - go ahead. Add some classes, extract some interfaces. You'll know if you've broken anything because your tests are still green.

And if you have a problem that is more granular in scope (e.g. the higher level test is going red under certain conditions due to some edge case in a lower-level component), you can of course go ahead and write a more granular test - scope the test to what you care about as the implementor.

The thing to bear in mind is that any entry point for a test fixture is a hard boundary that will be difficult to change. Anything that isn't a hard boundary is trivial to change.

Remember that the goal of TDD is not to produce a test suite - that is a nice side-effect. The goal of TDD is to help you write working code.


As an aside, this kind of approach is works very will with the "tell, don't ask" principle - you pass your output mechanism into the "unit" at the top-level and assert everything against a fake output implementation:

VendingMachine vendingMachine = new VendingMachine();

vendingMachine.insertCoins(5);
vendingMachine.displayTo(fakeDisplay);

assertThat(fakeDisplay.toString()).isEqualTo("5");

As you add more complex behaviours, you still interact with the VendingMachine interface with your tests and assert against the display but, internally, you might have more composition going on - the display might get passed around to more specialised subcomponents etc. etc.

OTHER TIPS

If the internals of your class are so complex that they require thorough testing, then your class is violating the SRP (doing too much). Refactor it into a client of a new external utility class, and test that utility class separately.

Licensed under: CC-BY-SA with attribution
scroll top