3 tips for maintainable unit tests

Although having a good collection of unit tests makes you feel safe and free to refactor, a bad collection of tests can make you scared to refactor. How so? A single change to application code can cause a cascade of failing tests. Here are some tips for avoiding (or fighting back) from that situation.

Tip 1: Test behaviour not structure

The behavior of the system is what the business cares about and it is what you should care about as well from a verification point of view. If requirements change drastically then changes to the system are expected, including the tests. The promise of good unit test coverage is that you can refactor with confidence that your tests will catch any regressions in behavior. However if you are testing the structure of your application rather than the behavior, refactoring will be difficult since you want to change the structure of your code but your tests are asserting that structure! Worse, your test suite might not even test the behavior but you have confidence in them because of the sheer volume of structural tests.

If you test the behavior of the system from the outside you are free to change implementation and your tests remain valid. I am not necessarily talking about integration style tests but actual unit tests whose entry point is a natural boundary. At work we have use-case classes that form this natural entry-point into any functionality.

So let’s look at an example of structural testing, and see the what happens when we try to make a change to the implementation details. As an example, we have a test against a CreatePerson use-case that creates a Person class and persists it if it is a valid person object. The initial design takes in an IValidator to determine whether the person is valid.

Notice how we are asserting against a dependency (IValidator) of the use-case (CreatePerson). Our test has structural knowledge of how CreatePerson is implemented. Let’s see what happens when we want to refactor this code…

Your team has been trying to bring in some new practices like Domain-Driven Design. The team discussed it and the Person class represents an easy start learning. You have been tasked with pulling behavior into the the Person entity and make it less anemic.

As a first try you move the validation logic into the Person class.

Looking at the use-case, we no longer need to inject IValidator. Not only is what we test going to have to change, we are going to have to change the test completely because we no longer have a validator to inject as a mock. We have seen the first signs of our tests being fragile.

Let’s try make our test focus on the behavior we expect instead of relying on the structure of our code.

Don’t worry too much about InMemoryPersonRepository people = Given.People; for now, we will come back to it. All you need to know is that InMemoryPersonRepository implements IPersonRepository.

Since we no longer need IValidator and it’s implementation, we delete those. We also get to delete the test CreatingPerson_WithValidPerson_CallsIsValid as we have a better test now CreatePerson_WithValidName_PersistsPerson that asserts the behavior we care about, the use-case creating and persisting a new person. Yay, less test code, better coverage!

At this point you might be saying “Wait! Unit tests are supposed to test one method, on one class”. No! A unit is whatever you need it to be. I am by no means saying write no tests for your small implementation details, just make sure you are comfortable deleting them if things change. With our focus on behavior tests we can delete those detailed tests freely and still be covered. In-fact, I often just delete the tests after I am done developing the component as I just used TDD for fast feedback loop on the design and implementation. Remember that test code is still code that needs maintenance so the more coverage for less the better.

So back to the code. What does our use-case look like now?

Thats ok. We got rid of a dependency and moved some logic to our Person entity but we can do better. On reviewing your pull request someone in the team pointed out something important. You should be aiming to make unrepresentable states unrepresentable. The business doesn’t allow saving a person without a name so let’s make it so that we can’t create an invalid Person.

Look at that! We refactored the implementation without having to update our test. It still passes without any changes.

This was a contrived example to illustrate the point but I hope this tip helps you write more maintainable tests.

Tip 2: Use in-memory dependencies

You have already seen InMemoryPersonRepository so this tip should be less verbose to explain. The claim is simply that the maintainability of your tests can be increased by using in-memory versions of your dependencies a little more and using mocking frameworks a little less.

I find in-memory versions of something like a repository that speaks to a database preferable to mocking frameworks for a few reasons:

  1. They tend to be easier to update than a mocking framework, especially if creation of the mocks is done in every test or fixture
  2. Coupled with some tooling (see next tip) they lead to far easier setup and readability
  3. They are simple to understand
  4. Great debugging tool

On the down side, they do take a little time to create.

Let’s take a quick look at what the one looks like for our code so far:

Super simple! Put in the work and give it a try, it may not be as sexy as a mocking framework but it really will help make your test suite more manageable.

Tip 3: Build up test tooling

Test tooling in this context means utility classes to make readability and maintainability of the tests easier. A big part of this is about making your tests clear about the setup while still keeping it concise.

Let’s discuss a few helpers you should have in any project…

In-memory dependencies

This was already discussed above. I can’t stress enough how much this improves maintenance and simplifies reasoning about tests.


Builders can be used as an easy way to setup test data. They are a great way of simultaneously avoiding dozens of different setup methods for your tests and a way to make it clear what the actual setup of your test is without diving into some setup method that looks like all the other setup methods.

A little trick is to put an implicit conversion to the class you are building up. Also take a look at Fluency for helping with the creation of builders.

A final note on this point. Just because I use builders a lot does not mean I completely throw mocking frameworks out the window. I just tend to use mocking frameworks for things I really don’t care about and really aren’t likely to change. I also tend to use them within other builders rather than directly in tests. This gives way more control over the grammar that you use to setup your tests.


Not sure what else to call these but it is useful to have a static class that makes access to builders and other types you would use in setup simple. Typically I have Given and A.

This allows me to write some very concise setup code. For example if I needed to populate my person repository with 3 random people I could do so like this:

For completeness the PersonBuilder implementation:

Wrapping up

So those are my 3 tips for making your tests more maintainable. I encourage you to give them a try. Without investing in the maintainability of your tests they can quickly become a burden rather than a boon. I have seen the practices above improve things not only in my teams but other colleagues have converged on similar learnings with the same positive results. Let me know if you find this helpful, or even if there are any points you strongly disagree with. I would love to discuss in the comments. Happy coding!

If you enjoyed this article you might like some of my others on testing:

Threat Modeling – Start using evil personas

Agile teams often use the concept of personas to create more tailored user stories, so could you use evil personas to describe malicious behavior?

Personas are “synthetic biographies of fictitious users of the future product” and “a powerful technique to describe the users and customers of a product in order to make the right product decisions“. The purpose of using personas is to “understand who the beneficiaries of the product are and what the goals they pursue”.

In essence, personas help teams understand if the designed functionality actually fits the end-user desires. This makes it a powerful approach to also identify possible risks by introducing malicious users or ‘evil personas’.

Read more →

Mob Programming in COVID-19 Times

What Is Mob Programming?

Simply put, mob programming is about getting together with at least three developers and start coding on a single keyboard. At any given time one developer is actually typing, the ‘Driver’. All other developers take the ‘Navigator’ role. They all review, discuss and describe what the Driver should be doing and the Driver narrates. The roles are swapped very frequently to keep everyone fresh and engaged. It’s the ultimate form of collaboration and peer review.

Mobbing During Lock Down

So now you know that mob programming is about live coding together on the same piece of code. But how do you do this when everyone is working remotely during this pandemic. With my current team we decided to give it a go regardless. There’s excellent online collaboration tools available these days, so it must be possible to exercise mob programming fully online. We’ve practiced for three days in a row in a mob programming hackathon. Below I’ll describe my experience.

Read more →

Organisational structures to create autonomy: what I’ve learned from my daughter

I’m grateful to learn from my daughter. Be able to see how the brain develops and picks up new concepts, skills and words. Nowadays, I enjoy to sit down and watch her play. As a parent, I also need to help her to achieve her autonomy: emotionally, mentally and physically. It is the job, and my wife and I are navigating through it. There is not a guide on how to parent, and we discuss what is working and what is not working. Sometimes is just not the time for a new skill, other times our daughter doesn’t develop an interest for a particular activity. Well, we are all different, and that is what makes the world colourful!

Read more →

Security by design? Don’t create a YAPWAV!

Security is about making risks visible and mitigating the impact of possible incidents to an acceptable level. The ‘security by design’ philosophy aims for every application or system to be at an acceptable risk level, all the time.

When starting with a ‘secure by design’ approach, often existing security processes are simply bolted onto the development life-cycle. One of the major pitfalls of this approach is requiring teams to do a YAPWAV. YAPWAV stand for the developer’s hell called: Yet Another Process Without Added Value. A YAPWAV is an activity a team solely has to do to please a stakeholder, without noticeably improving the product they’re building.

A classic example of a YAPWAV is the mandatory risk assessment for each software deployment, just for the purpose of satisfying a documentation process. These kinds of security processes are bound to fail as they add no (visible) value to the product the team is building. In the agile philosophy, every action or activity should contribute to the value of the product. The moment an activity is introduced that doesn’t add visible value, teams will decide it’s not worth the effort and stop doing it.

Read more →

Remote collaborative modelling part 1: Check-in

Collaborative modelling is not only an essential practice in Domain-Driven Design for creating a shared understanding of the domain. I believe it is vital in building sustainable and inclusive quality software. Covid-19 has constrained us to move collaborative modelling sessions online, and for almost everyone, this is uncharted territory and can be quite overwhelming. In these series of posts, I hope to give people some guidance and heuristics to start doing more of collaborative modelling in a remote world so that we can build more sustainable and inclusive quality software. We start this series with a practice that can make or break a collaborative modelling session, a Deep Democracy check-in.

Read more →

Diverge and converge to create a Context Map

Context Map was the first visualisation for the Bounded Context pattern from Domain-Driven Design. In a nutshell, it is a map of the different Bounded Contexts and their relationships. I tend to create a Context Map during or after a Big Picture EventStorming. Changing perspectives can be helpful, to challenge assumptions and get the best of different techniques.

However, sometimes it is hard to reach a consensus on the Context Map. I often operate in brownfield projects, with large organisations. Although people agree with the different bounded contexts, it is a process that takes time, and most significant energy. Which can lead to fatigue towards the method, and at the same time raises exciting patterns in the behaviours. But this blog post is not about emergent behaviour. 🙂

Read more →

Chaos Engineering as management practice

Chaos Engineering is a practice that has its roots at Netflix. It born from the challenges of moving their workloads from the data centre to the cloud; the transient nature of the cloud affected the way that they build and operate a system at scale. The initial project was called Chaos Monkey, and it has almost 10 years.

Since then the community grew, fueled by Netflix practitioners. Today there are commercial and open-source tools, and we can see more initiatives in different communities. The technical practices had matured, and the knowledge started to spread in the IT world.

However, it is deemed perceived as a technical practice. Can we leverage Chaos Engineering as a management practice?

Read more →

Using Team Topologies to discover and improve reliability qualities

Team Topologies is the work of Matthew Skelton and Manuel Pais, and I use it as part of my job. From a sociotechnical perspective, a team-first approach is paramount for any organisation and helps to decrease the accidental complexity. As such, I’m often asked “How can we operate in DevOps?” or “How can I have a reliable service to deliver value to my customer?”.

Read more →

If something is too complex to understand, it must be wrong

Recently, I was invited for a podcast interview by my brilliant colleague João Rosa. It was my first podcast interview (yes I was excited and nervous), and it has been keeping my mind busy ever since I received that calendar invite. The idea was that we would discuss a heuristic and see where we’d end up after 30 minutes. The heuristic for my interview was ‘If something is too complex to understand, it must be wrong.’ 

My first reaction was “Yes! That’s actually a heuristic I regularly use myself; what a coincidence!”. As hours and days went by, I started to notice that something was changing in my convictions. After some careful consideration and hours of contemplating, I can now say that my expert opinion regarding this heuristic is: “It depends”. (Ha! Surprising answer for a consultant, right?)

Read more →