DEV Community

Kieran Bond
Kieran Bond

Posted on

Unit Testing - Behaviours and critical units

Originally posted on my substack.

Contents

  • How I like to write automated tests
  • Critical Unit testing
  • Why I think what I think

How I like to write automated tests

Preface: I don't always work this way. It takes patience and discipline; my natural behaviour is to be gung-ho and write the implementation first. It's a constant battle and situation dependant. This methodology may not work for you, but it works for me.

My workflow looks something like this:

  • Picking a specific user behaviour to implement.

  • Thinking about how this behaviour is interacted with - What does the API look like?

  • Defining the API for the behaviour in code - Focusing on the inputs and outputs specifically.

  • Write this into a test scenario - using the AAA (Arrange, Act, Assert) pattern.

  • Write the bare minimum code required to compile/run the test - ensuring it fails.

  • Make the test pass - using hardcoded values, the bare minimum

  • Refactor/clean - constantly checking the test still passes, still only writing the bare minimum needed

  • Repeat for different scenarios related to the behaviour; if required - specific input scenarios may need to be tested.

This workflow is close to the classic red/green TDD testing pattern if not precisely that. 

It works well for me, but I think it is essential to be incredibly fussy about focusing on a particular behaviour that is well-defined and singular. Concentrating on precise behaviours ensures that the test is focused and has minimum code. This makes it easier to reason about, as well as to maintain.

I like to ensure my tests avoid as much implementation detail as possible so that they are easily understood and maintained. Any time you leak implementation details into a test, you add tight coupling to the implementation and stray further from a focus on behaviours.

I also like to ensure that any API I design only provides access to functional behaviours. If the API is not for a specific behaviour, it is trying to do too much and needs to be split into multiple, more focused interfaces. It can sometimes be easier to think about user behaviours - what exactly are they trying to achieve when interacting with this API?

Advantages of this methodology

  • You only write the code you need.

  • A focused test is easier to debug when your implementation has failed to make it pass.

  • Your API is more straightforward to reason about - A focus on behaviours leads to simple APIs with simple tests. There should not be any unknown or unexpected behaviours when using it.

  • Your tests should catch regressions in behaviour.

  • Your tests are loosely coupled to implementations; thus, the implementation is easy to refactor.

Disadvantages of this methodology

  • It requires discipline and time to avoid leaking implementation details into your tests.

  • Using it can feel slow and unnatural (like TDD often does).

  • Using it is more challenging in 'established' codebases.

  • It can require more planning/design up front.

  • It can struggle when being used for fixing bugs and writing regression tests for those bugs.

  • It is not great for 'negative' tests - ensuring something did not happen (I generally disagree with this test style). 

Critical Unit testing

Sometimes, we have things that are hard to test from an API perspective - such as a complicated algorithm.

Often, these are 'units' that are critical to the system. Think of a part of your system that needs to be performant; this is likely a good example.

In these cases, I lean more towards 'typical' unit testing - Plaster it in tests that make you confident it works correctly. With the caveat: only do this if the unit is genuinely critical!

Covering a 'unit' in many tests has disadvantages - it's harder to maintain and often harder to reason about. You will likely leak implementation details. This is why it's only reasonable if it's a very critical unit and complex to test behaviorally.

The advantage of covering the 'unit' in tests is that it's easier to test the complex scenarios and branches, easier to catch regressions, and gives you high confidence. 

Confidence matters most here - after all, that's why we write tests. We want to feel confident about what we have implemented, changed, broken.

Why I think what I think

How did I get to this style of automated testing? What makes me think this is the right way for me?

TDD

To me, the essence of TDD is: 

  • Design and define an API.

  • Write a test against the API.

  • Write the bare minimum code required to make your test pass.

  • Repeat.

The simplicity and focus of TDD leads to well-designed software.

I highly recommend reading 'Test Driven Development: By Example', written by Kent Beck. This book helped shape how I use TDD. It's a bit basic, but so is TDD.

Ian Cooper

Ian Cooper is great. He transformed how I looked at unit testing and TDD, which helped me enjoy the TDD process.

I highly recommend this talk by Ian:

In Summary

Give this ‘methodology’ a go. Experiment with it; try it in a pairing session.

The key point is to focus on the behaviours of the system and user. That’s the whole reason we’re building these things, right?

Top comments (0)