A lot of us have heard about unit testing. Out of all the different types of testing (and there are a few!), it’s probably the better-known one. It’s also the most common form of testing used with WordPress.
In the past, we’ve looked at how unit testing works with WordPress. That said, knowing how unit testing works is one thing. This knowledge doesn’t necessarily make it any easier to get started with it in your projects.
Some of us work with existing code bases. It could be a plugin, a theme or a whole WordPress site. How do you take the fundamentals of unit testing and apply them in that context?
This is a tricky question to answer, but it’s also an important one. Most of us will not start using unit testing in a vacuum. We’re going to start using it with one of those existing code bases.
This brings with it its own unique sets of challenges. It also means that you need to have a strategy for them. This will help make this transition smoother for you.
Your code probably isn’t ready for unit tests
This is a bit of a guess, but there’s a good chance that it’s true. There could be technical reasons for it. For example, it’s a good idea (or even mandatory) to have your code in namespaces.
That said, technical issues like namespaces aren’t the biggest reason why your code isn’t ready for unit tests. The main reason why you’re going to have a tough time with your transition to unit tests is complexity. (If you’re not familiar with the concept, you should consider reading this introduction on it.) But why is that an issue?
What does complexity have to do with unit testing?
One way to measure complexity is to think about paths through your code. The more paths there are, the more complex it is. Unit testing is all about testing these different paths to ensure that they lead where you want them to.
function insert_default_value($mixed) { if ($mixed instanceof ToStringInterface) { $mixed = $mixed->to_string(); } if (!is_string($mixed) || empty($mixed)) { $mixed = 'value'; } return $mixed; }
Here’s a code example from the introduction to complexity article that I just linked. There are six different paths through it. In an ideal scenario, you would have unit tests for all six of those paths. (Whether you should is another question and part of a much larger debate.)
But let’s be honest, there’s a good chance that your code is a lot more complex than this function. And that’s ok too! Even by complexity standards, you’re allowed to make functions that are more complex than that one.
However, it’s a lot more likely that your code is quite complex. It’s more common to see large functions in WordPress code than small ones. This makes them hard to unit test.
Yet even if they’re not too complex, your functions can still be hard to unit test. For example, do they call a lot of other WordPress functions? Then you have to mock all of those calls to external functions.
You’ll need to break things up
The most effective solution to complexity is to break up your large functions or methods into smaller ones. This is something that you’ll have to consider doing as you start unit testing your code. Otherwise, you might find that writing unit tests for your code to be near impossible.
Now, the goal isn’t to tell you to refactor all your code and break everything down right away. It’s just to prepare you for that eventuality. As you work to add unit tests to your code, it’s likely that you’ll need to break your functions and methods down into smaller ones.
This is a good thing since you’ll not only be making your code stronger by unit testing it. You’ll also make it less complex which is always a good thing. It’s just that it’ll require more work from you up front.
You can’t test everything at once
This is the biggest hurdle of all. You could sit down and write unit tests for all your code at once. But you’re probably looking at dozens of hours of work. That’s on top of whatever other code changes you need to do to support unit tests.
Most people don’t have that amount of hours to invest upfront like that. So it’s not realistic to expect anyone to just sit down and write a full test suite from scratch. Instead, you need to approach it in a pragmatic way.
Start by unit testing bugs
One way is to create unit tests for every bug that you fix. This is a common recommendation by a lot of developers that promote unit testing. They see it as a great way to get into unit testing for two reasons.
First, bugs tend to be small and localized to a specific area of your code. (As, with all things, there are exceptions to this, but it’s an overall trend.) This makes it easy to isolate the code that caused the bug. It also makes straightforward for you to write a test for it. (Although, you might have to refactor some of your code first as we discussed earlier.)
Second, it lets you actually experience a test-driven development approach. That’s because, in an ideal scenario, you start by writing a failing unit test exposing the bug in question. Then you fix the bug so that your unit test passes.
There’s nothing that quite reinforces the value of unit tests as seeing this process happen. You can reproduce the bug using your test. And then you can see the actual fix because your test went from failing to passing.
The result of this process is a test suite that focuses on regressions. We create tests for bugs so that they don’t show up again in the future. We also call that regression testing.
Test new features
If you’re working on an active code base, you’re probably working on new features. New features are also a great opportunity to add unit testing to your workflow. That’s because new features often involve writing new code that’s pretty self-contained.
Sure, you might need to integrate that feature with other parts of your code. That said, a lot of the code for that feature will also be standalone and specific to it. This is what makes it a good target for unit testing.
Writing this new code for this new feature also has another unintended benefit. It forces you to write the code that’s easy to unit test. This is something that’s useful to practice if you’re not comfortable doing writing code that way yet. It’s also easier to do that than to refactor old code to make it unit testable.
Keeping the ball rolling
Unit testing is also a habit and, like most habits, it’s hard to stay consistent with it. There’s always an excuse close at hand to convince you not to unit test your code. You have to find ways to stay consistent with it.
That’s part of the reason why the two suggestions to help you start unit testing your code. They try to limit possible objections that you might have for writing unit tests for your code. But that might still not be enough. So what else can you do to help build that habit especially within a team of developers?
Mandatory code reviews
Mandatory code reviews are an interesting tool for a few reasons. They allow the entire team to take ownership of adding unit tests to a code base. Someone can still commit code without unit tests. But, if someone approves that code, they’re also in some other way responsible for the lack of unit tests.
Mandatory code reviews are also great if you have someone with unit testing experience on your team. These experienced team members can help teach and improve your unit testing skills. For example, that team member might spot a missing test that you should’ve added.
Enforce code coverage
Another way to help you and your team start unit testing your code is to require a certain level of code coverage. Code coverage is a measurement used to calculate how much of your code your unit test suite really tests. If your test suite has 100% code coverage, it means that your tests run through every line of code in your code base.
How do you enforce code coverage when you have little to none?
That said, if you’re starting to write unit tests for your code, you can’t expect your tests to have 100% code coverage. That’s why enforcing code coverage can be a bit of a drastic way of forcing your team to write unit tests. But let’s say that you wanted to do it anyways, how could you do it in that scenario?
Well, first you would need a continuous integration workflow that supports code coverage verification. (Don’t have continuous integration workflow? This article will help you get started with continuous integration and WordPress.) This is necessary because evaluating code coverage is something that’s done as part of an automatic process. It’s not something that developers do by hand. (Although you could if you wanted.)
Once you have your continuous integration workflow, you need to figure out what your current code coverage is. Let’s imagine that you only have 12% coverage right now. (This might seem like nothing, but it’s ok! We have to start somewhere.) You would then start by setting your minimum code coverage threshold to 12%.
By doing this, you’d prevent a developer from committing code without tests. That’s because this commit would cause the code coverage would fall below 12%. That would then trigger an alert from your continuous integration platform.
That said, you can’t leave your minimum code coverage threshold to 12% forever. Otherwise, a developer would eventually be able to commit code without tests again. (For example, if you have 15% code coverage and the commit brings it to 14%, nothing would happen.) So you want to increase that minimum code coverage threshold as your code coverage improves.
How much code coverage is enough?
We mentioned 100% code coverage earlier. Should you aim to have your test suite cover 100% of your code base? Is there any benefit to that at all?
Well, it’s a matter of much debate whether 100% coverage is even necessary or useful. The reality is that few developers have worked on a code base with 100% code coverage. It’s only realistic for small code bases. And most of us don’t work with small code bases.
That said, this isn’t even the situation that we’re in. We’re trying to add code coverage to an existing code base. In that situation, it’s definitely not realistic to aim for 100% code coverage.
That’s why how much code coverage to have is so subjective. There’s no perfect number. You might be ok with 42%, or you might need 73%.
But, as a general rule, you should have more code coverage on critical parts of your code. For example, if you have code to handle payments, there’s a good chance that you want that code to be well tested.
But not every piece of code is as critical as the code handling payments. For that code, there’s less of a reason to have perfect code coverage. In the end, what’s important is that your code coverage makes you confident in the code that you’re delivering to clients or customers.
It’s normal to find this challenging
Starting a new habit is always hard. Very few of us can start running or eating well just like that. There’s always a struggle, and unit testing isn’t any different.
People that are successful in overcoming these hurdles often develop personal strategies to help them. That’s what you have to do with unit testing as well. Only by developing strategies that work well for you and your team will you be able to succeed at it.
What we’ve seen in this article are just suggestions. You’ll have to do some trial and error to figure out what works best for you and your team. And then you can iterate from there.
But like any habit, there’s value there if you persevere. (It’s a bit cliche, but it’s also true!) Unit testing is a great coding habit to have, and you’ll be grateful for the effort that you put into developing it.