3

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?

  • 4
    Possible duplicate of How do you unit test private methods? – Robert Harvey Mar 8 '18 at 4:24
  • Having large/complex tests is ok, but by covering with tests as much as possible you will get benefits of refactoring and changing code design for free. – Fabio Mar 8 '18 at 5:12
  • 1
    You can always create some factory/builder classes which create setup for every test case in readable and maintainable way – Fabio Mar 8 '18 at 5:14
5

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.

  • Dropping this in a comment because I'm not sure on the stance on self-linking in answers but I recently played with the idea of building a fluent query builder for Cypher (the Neo4j query language) test- first. Might be a good example. All the tests are at the CypherQuery level because what I care about is producing valid queries. As I add functionality, I need to break things down into concepts of different types of expressions internally - but I don't test those independently - they are just there to produce the CypherQuery level behaviour. If I did test them separately, adding more... – Ant P Mar 8 '18 at 8:56
  • ... functionality would be a nightmare as all of the test boundaries would be changing all of the time. They are just implementation details - I write a new test for the CypherQuery and then internally write whatever code I need to in order to satisfy it. Have a scan through the commits: github.com/antplant/cypherfunc/commits/master – Ant P Mar 8 '18 at 8:56
2

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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