TDD: Why Use Testing APIs?

Michael T. Andemeskel
4 min readJan 2, 2024
SF, From Middle Harbor Shoreline Park, Oakland

Summary

Make testing easier and your test suite better with each test by creating an abstraction layer between your code and your tests. This is the testing API (TAPI). To create a TAPI follow these rules and read this for an example.

  • Don’t access the code you are testing directly from your tests; use the TAPI everywhere this happens.
  • Put your test setup in the TAPI.
  • Put your test teardown in the TAPI.
  • Put all your assertions that directly access the code under test in the TAPI.
  • Put all production imports into the TAPI.
  • Create separate TAPIs for each use case (business need) using inheritance or dependency injection - unless those business needs are codependent.
  • Name the TAPI methods after the business need they accomplish, not what they interact with or do to the code i.e., selectCustomer vs. setCustomerSelectValue or expectCustomerToBeSelected vs. expectCustomerSelectValueToBe — names are insidious ways to couple tests to implementation. It defeats the purpose of the TAPI if its methods have the implementation hardcoded in their names.

Reasoning

One of the most significant pain points I faced as an engineer was testing — how do you write tests but maintain velocity when given only a week or two to deliver? TDD is faster in the medium to long term. But I may not be on the project or team in that time horizon; engineers are being judged and even fired on a weekly or biweekly basis. Why compromise my career now for the benefit of a stranger in the future?

Well, if my predecessor had used TDD, I wouldn’t be in this position. However, I am in this position, and my predecessor has written tests using TDD. I may be worse off because now I have all these failing tests to fix. Maybe, TDD is a waste of time?

No, TDD is probably the best investment you can make. TDD pays off immediately by guiding your design decisions and ensuring every assumption you make is well founded. But it is still gratification delay. TDD is winning the lottery and deciding to take the monthly payments. Not using TDD is taking the lump sum — yes, you get immediate gratification, but it comes with many risks. It is easier to waste the money you have on hand than a 25-year income stream you are locked out of. How can we enjoy the fruits of TDD now while lowering the costs? How can we speed up the frequency of those payments?

This problem became more acute when I began working on my startup full-time. I wanted immediate gratification: ship now and ship often. I read up on architecture and TDD to figure out how to make testing faster and easier in the short term. I found the answer in Chapter 28 of Uncle Bob’s Clean Architecture. The answer was to use an abstraction layer to communicate with the code under test.

The hardest part of testing is not writing the tests, i.e., the asserts, but getting the code into a testable state, running it, introspecting it (the actual testing), and resetting it. We do these four things over and over in every test, but they can be exceedingly difficult due to the vagaries of our implementation. Why not hide all these complications behind a neat abstraction? That’s what TAPIs do. They work because while TDD forces you to write testable code:

  • The sum of the parts may not be testable.
  • Not all frameworks/libraries you use will lend themselves to testability.
  • Sometimes, it is hard to satisfy testability, simplicity, and functionality all in one design — these three goals are not guaranteed to be mutually inclusive.
  • Testable code still needs some extra steps to meet the needs of each test.

So, why not encapsulate testability into a separate but coupled abstraction?

Unfortunately, Uncle Bob does not go into detail on implementing TAPIs, but after a year of experimenting, I’ve found some rules to follow.

  • Don’t access the code you are testing directly from your tests; use the TAPI everywhere this happens.
  • Put your test setup in the TAPI.
  • Put your test teardown in the TAPI.
  • Put all your assertions that directly access the code under test in the TAPI.
  • Put all production imports into the TAPI.
  • Create separate TAPIs for each use case (business need) using inheritance or dependency injection - unless those business needs are codependent.
  • Name the TAPI methods after the business need they accomplish, not what they interact with or do to the code i.e., selectCustomer vs. setCustomerSelectValue or expectCustomerToBeSelected vs. expectCustomerSelectValueToBe — names are insidious ways to couple tests to implementation. It defeats the purpose of the TAPI if its methods have the implementation hardcoded in their names.

These are simple rules that anyone can follow, and they immediately pay off. Tests become easier to read and update because they are written in the business logic being tested. Every additional test will be easier to write than the previous one as the TAPIs grow — crazy, right? But that’s the power of abstractions. TAPIs really shine when testing untested code or refactoring code that is tested, but that’s a story for another day.

If you want to know how I built my TAPI’s, read this.

Sources/Inspiration

  • Clean Architecture: A Craftsman’s Guide to Software Structure and Design, Robert Cecil Martin “Uncle Bob” — Chapter 28
  • Test-Driven Development by Example, Kent Beck — the whole thing!

--

--

Michael T. Andemeskel

I write code and occasionally, bad poetry. Thankfully, my code isn’t as bad as my poetry.