Test-Driven Development with PHPUnit
In our previous 2 articles (“What is Test Driven Development?” and “Introduction to PHPUnit”), we discussed what Test-Driven Development is and why you should use it and we gave a very basic intro to PHPUnit. In this article, we’ll work through how to use PHPUnit to develop some new features using TDD.
A Brief Recap of TDD
TDD consists of 5 phases that we’ll be cycling through during the course of this article. They include:
- Quickly add a new test
- Run all tests and see the new one fail
- Make a little change
- Run all tests and see them all succeed
- Refactor to remove duplication
If these phases are still a little fuzzy make sure to check out “What is Test-Driven Development?” for a full overview.
This example assumes we have PHPUnit > 8.0 installed in the ./vendor/bin directory of our project directory.
Example - SuperString::isEmpty is true
Let’s say our application has a SuperString
class to enhance PHP’s string
type and we need to add some functionality to check if the string we have is empty (""
).
This is what the SuperString
class looks like initially.
Quickly add a new test
The first thing we’re going to do is create a new test to check the result of one input to SuperString
.
Notice how small the test is. We’re giving the test a very specific functionality to test and we’re only asserting one thing. If we have more than one assert per test we run the risk of making it difficult to debug later when something does eventually breaks.
Also, note that we’ve named the test function so we can easily understand what the test is doing. testIsEmptyTrue
is much shorter but is ultimately not as understandable.
Run all tests and see the new one fail
Now we’ll run PHPUnit to see that we do indeed get a failing test. In this case, we haven’t yet defined the function so we get an “undefined method” error.
Make a little change
Again, our goal in this phase is to make the smallest change we can to allow our tests to pass. To that end, we’re going to create our new isEmpty()
function and just have it return true
. It doesn’t cover all the possible inputs but the goal in this step isn’t to cover all the inputs it’s to get our test to pass. We’ll cover more inputs later.
Run all tests and see them all succeed
Now we run our test and verify that our test passes.
Refactor to remove duplication
Our code currently doesn’t contain any duplication but it’s important not to get lazy and skip this step.
Example - SuperString::isEmpty is false
Our simple implementation of isEmpty()
is going to be wrong most of the time because of its current implementation. Now we need to add another test that checks for opposite cases where the string isn’t ""
. As a general rule, it’s a good idea to have tests for what we would consider normal input, the extremes of inputs (very large or very small), and spots where we can think of oddities happening.
Quickly add a new test
Here is our test. It’s essentially the inverse of our testBlankStringCausesIsEmptyToReturnTrue()
function.
Run all tests and see the new one fail
Make a little change
Now we make a small change to our isEmpty()
function so it passes all the tests.
Run all tests and see them all succeed
Refactor to remove duplication
Again due to the simple nature of our example there isn’t any duplication in our code at this point.
Example - SuperString::isNotEmpty
Now we have another feature that requires us to check to see if the string isn’t empty. To that end, we’re going to create an isNotEmpty()
function that will complement our isEmpty()
function.
Quickly add a new test
Run all tests and see the new one fail
Make a little change
In this case, instead of returning false
and then creating another test so we can write the functionality by going through all the TDD steps, we’re just going to trust ourselves and create the obvious implementation of the isNotEmpty()
function.
Run all tests and see them all succeed
Refactor to remove duplication
Now here is where it gets interesting. The last two times we’ve hit this step we haven’t had anything to do but now look at our isEmpty()
and isNotEmpty()
functions.
We can see some minor duplication in the two calls to mb_strlen($this->string)
. Now we just need to determine how we want to resolve this.
The first solution is to extract that duplication into a new function because we’ll most likely need the same logic again.
The second solution is to realize that isNotEmpty()
returns the boolean opposite of isEmpty()
.
In the end, the first solution gives us the best flexibility for future expansion so we’ll stick with that.
Finally, we need to run our tests again to verify that no errors crept into our code as we made these changes.
Thanks for Reading!
Are you using PHPUnit and TDD? How is it going? Let us know in the comments if it’s going great or if you’re running into problems.
Scott Keck-Warren
Scott is the Director of Technology at WeCare Connect where he strives to provide solutions for his customers needs. He's the father of two and can be found most weekends working on projects around the house with his loving partner.
Top Posts
- Working With Soft Deletes in Laravel (By Example)
- Fixing CMake was unable to find a build program corresponding to "Unix Makefiles"
- Upgrading to Laravel 8.x
- Get The Count of the Number of Users in an AD Group
- Multiple Vagrant VMs in One Vagrantfile
- Fixing the "this is larger than GitHub's recommended maximum file size of 50.00 MB" error
- Changing the Directory Vagrant Stores the VMs In
- Accepting Android SDK Licenses From The OSX Command Line
- Fixing the 'Target class [config] does not exist' Error
- Using Rectangle to Manage MacOS Windows