In this presentation, we discussed what Test-Driven Development(TDD) is, how to get started with TDD, work through an example, and discuss how to get started in your application.
3. What Are You Going to Learn?
1. What Is Test-Driven Development
2. Why You Should Be Using Test-
Driven Development
3. How To Do Test-Driven Development
4. How To Get Started On Your Project
5. Common Gotchas
5. What is Test-Driven
Development?
• Abbreviated TDD
• Test-first software development process
• Create suite of automated tests
• Short cycles of 5 phases
• < Minute
7. What is Test-Driven
Development?
• “Best” for greenfield
• Works well for brownfield
• Check out “Working Effectively
with Legacy Code” by Michael
Feathers for brownfield
25. PHPUnit
• Test Framework
• De Facto Testing Framework
for PHP
• Written by Sebastian
Bergmann
• All tests are written in PHP
Photo by RF._.studio from Pexels
26. PHPUnit
• Organization is easy
• All test files inside a tests
folder
• A test is a PHP class method
Photo by Anete Lusina from Pexels
28. PHPUnit
public function testAStringCausesIsEmptyToReturnFalse(): void
{
$item = new SuperString('Test Data');
$this->assertFalse($item->isEmpty());
}
/**
* @test
*/
public function aStringCausesIsEmptyToReturnFalse(): void
{
$item = new SuperString('Test Data');
29. PHPUnit
•Each test contains one or more asserts
$this->assertFalse($value);
$this->assertEquals($expectedValue, $value);
$this->assertGreaterThan($number, $value);
30. PHPUnit
• A PHP class groups similar tests together
• Ideally a test class for each application class with logic
• A test class for common initial conditions
• Each class ends with “Test”
31. PHPUnit
• Installed using composer
• Can install globally or per project
• Per project is the way to go
32. PHPUnit
• Command line tool
• Super powerful
• phpunit
• phpunit tests/Unit/SuperStringTest.php
• phpunit —filter User
• PHPUnit + IDE = More efficient
33. PHPUnit
• From the command line:
• ./vendor/bin/phpunit tests/Unit/SuperStringTest.php
39. isEmpty() - Add Test
<?php
namespace TestsUnit;
class SuperStringTest
{
40. isEmpty() - Add Test
<?php
namespace TestsUnit;
use PHPUnitFrameworkTestCase;
class SuperStringTest extends TestCase
{
41. isEmpty() - Add Test
<?php
namespace TestsUnit;
use PHPUnitFrameworkTestCase;
class SuperStringTest extends TestCase
{
public function testBlankStringCausesIsEmptyToReturnTrue(): void
{
42. isEmpty() - Add Test
<?php
namespace TestsUnit;
use AppSuperString;
use PHPUnitFrameworkTestCase;
class SuperStringTest extends TestCase
{
public function testBlankStringCausesIsEmptyToReturnTrue(): void
{
$item = new SuperString('');
43. isEmpty() - Add Test
<?php
namespace TestsUnit;
use AppSuperString;
use PHPUnitFrameworkTestCase;
class SuperStringTest extends TestCase
{
public function testBlankStringCausesIsEmptyToReturnTrue(): void
{
$item = new SuperString('');
46. isEmpty() - Make A Change
// in SuperString
public function isEmpty(): bool
{
return true;
}
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
47. isEmpty() - Make A Change
// in SuperString
public function isEmpty(): bool
{
return true;
}
51. !isEmpty() - Add Test
// in SuperStringTest
public function testBlankStringCausesIsEmptyToReturnTrue(): void
{
$item = new SuperString('');
$this->assertTrue($item->isEmpty());
}
52. !isEmpty() - Add Test
// in SuperStringTest
public function testBlankStringCausesIsEmptyToReturnTrue(): void
{
$item = new SuperString('');
$this->assertTrue($item->isEmpty());
}
public function testAStringCausesIsEmptyToReturnFalse(): void
{
$item = new SuperString('Test Data');
$this->assertFalse($item->isEmpty());
}
59. isNotEmpty() - Add Test
// in SuperStringTest
public function testBlankStringCausesIsNotEmptyToReturnFalse(): void
{
$item = new SuperString('');
$this->assertFalse($item->isNotEmpty());
}
64. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
public function isNotEmpty(): bool
{
return mb_strlen($this->string) != 0;
}
65. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
public function isNotEmpty(): bool
{
return mb_strlen($this->string) != 0;
}
66. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
public function isNotEmpty(): bool
{
return mb_strlen($this->string) != 0;
}
67. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return $this->length() == 0;
}
public function isNotEmpty(): bool
{
return $this->length() != 0;
}
68. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
public function isNotEmpty(): bool
{
return mb_strlen($this->string) != 0;
}
69. isNotEmpty() - Refactor
// in SuperString
public function isEmpty(): bool
{
return mb_strlen($this->string) == 0;
}
public function isNotEmpty(): bool
{
return !$this->isEmpty();
}
81. Getting Started With Your
Team
1. Get management buy in
2. Get everyone using TDD
Photo by Fauxels from Pexels
82. Getting Started With Your
Team
1. Book recommendations
1. “Test Driven
Development: By
Example” by Kent Beck
2. “Working Effectively with
Legacy Code” by Michael
Feathers
Photo by Liza Summer from Pexels
83. Getting Started With Your
Team
1. Basic Training on TDD with
your code base
2. Code Reviews with TDD as
the keystone
3. Pair Programming
Photo by Christina Morillo from Pexels
Professional PHP Developer for 11 years
// supervisor for 6 of those 11
Currently Director of Technology at WeCare Connect
Survey solutions to improve employee and resident retention at skilled nursing facilities
Use PHP for our backend
Host of PHP Developers TV on YouTube
Discuss topics helpful for PHP developers
Can get to our channel using phpdevelopers.tv
Could spend hours discussing automated testing with you all. It’s one of my favorite PHP adjacent topics but we only have 40 minutes.
My goals are to always have you leave one of my sessions with something you can use the next day you’re going to work and how you can implement this with your team
TDD is a test-first software development process that uses short development cycles to write very specific test cases and then modify our code so the tests pass.
TDD consists of five phases that we will repeat as we modify our code. Each of the phases happens very quickly and we might go through all five phases in less than a minute.
TDD was "rediscovered" by Kent Beck while working on the SmallTalk language and has been documented extensively in his book "Test Driven Development: By Example". The book is an excellent primer for working with TDD as it works through several examples of how to use TDD and also explains some techniques for improving code.
One of few physical books I have
While TDD works best for greenfield applications, it's a benefit to anyone working on a brownfield application as well.
Brownfield applications tend to require a little more finesse to get them set up for automated testing so don't get discouraged if it's frustrating at first.
“Working Effectively with Legacy Code” by Michael Feathers is a helpful book in this regard
Let's say it's 4 PM before a long weekend and your boss comes to you and asks you to make a small change to your codebase to meet a client requirement.
How confident are you that if you make the change you're not going to get a call on Sunday?
Because TDD forces you to create tests, you'll have the confidence that when you make the change
And the test pass you can push to production and know that your weekend isn't going to be interrupted with a new bug from your change.
TDD consists of 5 phases
Each phase should be small with, at most, ten new lines of code being modified. If we find ourselves doing more than that we're working on too large a change and we need to break it into smaller pieces.
1. Add a new test
The first thing we're going to do is write a failing test. We'll use this failing test to help determine when we've achieved our expected functionality.
It's important that the test is succinct and that it's looking at how a **single** change will affect our code.
2. Run all tests and see the new one fail
In this step, we're going to run the test to make sure our test fails before we move on to the next phase. It's very easy to write a test that doesn't fail so we **always** run our test to verify it's failing before moving to the next phase.
If it’s not failing we don’t know if our change actually did anything
As a small aside, the wording for this phase says "run all the tests" but as our test suite (a collection of tests) grows this will take an unproductively large amount of time. Current code base takes 25 minutes to run whole suite.
We'll want to short circuit this and only run the test file or just our new test. Many IDEs can run a single file or even a single test and it's worth spending time figuring out how to get this working as it will make us more productive.
Some are built in like PHP Storm but some require extensions like VS Code.
3. Make a little change
Now our goal is to change the smallest amount of code possible to get that test to pass.
We don't want to change any more than is necessary because that extra bit of change wasn't made using TDD and is potentially not tested. We don't need perfect code in this phase we just need code that makes the test pass. It's very easy to get caught up in making sure everything is perfect but that's not the goal here. Perfect comes later.
4. Run all tests and see them all succeed
Now that we've made our change we can run our test and see that it passes new test and any other tests.
If it doesn't then we just jump back to phase #3 and keep making small changes until it does.
5. Refactor to remove duplication
Now that we have our tests passing we're going to take a break and inspect both our test code and our code under test to see where we can make changes so it's easier for future developers to read, understand, and maintain.
This is called Refactoring and it’s: restructure code so as to improve operation without altering functionality.
We're using TDD so changes are painless because we can quickly run our unit tests again to make sure we didn't make a mistake.
Other things we should look for as we're doing this process:
Are the method/class/variables easy to read?
Do they express intent? -> We can name a function getTheValue but it doesn’t explain what fnt does and will cause confusion
Will future me understand them? A week? A Year? If I can’t how can anyone else who wasn’t in my head space
Other things we should look for as we're doing this process:
Can we move logic into the superclass so other classes can use it?
Can we move logic into a shared trait? Favorite from of reuse is when we can apply same function to multiple trees of inheritance
Now that we've completed a single TDD cycle we can start back at the beginning with a new test.
If you google TDD There’s also a three phase version
Called Red, green, refactor
Red and green come from colored messages we get from our testing tool
Basically group phases 1 and 2 and 3 and 4.
I like 5 phases for demos because it gives explicit steps
We’re going to work through some example TDD cycles.
Our examples uses PHPUnit to run our tests so we going to do a Quick intro on PHPUnit
In case you don’t know
Testing Framework
De Facto Testing Framework for PHP
Written by Sebastian Bergmann
All tests are written in PHP so we don’t need to learn anything new
Organization is easy
All test files are placed inside a tests folder at the root of the project
A test is a PHP class method
Each test function starts with “test”
or …
Each test function starts with “test”
or uses the @test annotation
This is a personal preference thing. I learned PHPUnit before annoation option so I default to prefixing tests with test. Also uses less vertical space which helpful for presentations
Each test function contains one or more asserts
Asserts tell PHPUnit to compare an expected value to the value we got from our code
$this->assertFalse($value);
$this->assertEquals($expectedValue, $value);
$this->assertGreaterThan($number, $value);
A PHP class groups similar tests together
Ideally a test class for each application class with logic. Don’t need tests for classes with no logic.
A test class for common initial conditions
Might have class for tests involving a standard user and another class for administrator users
Each class ends with “Test” super important and PHPunit won’t “autoload” them.
Installed using composer
Can install globally or per project
Some people on the internet say to install globally but it’s not great
Might have two projects that need different versions and won’t work with opposite version
Per project is the way to go
PHPUnit website even says not to even though it gives instructions
Our examples today mostly command line
Running using
./vendor/bin/phpunit tests/FileTest.php
Let’s talk about our example
One of the things I feel like PHP is missing is a string class
Always end up using a string class library and it’s a good example of something easy to understand that isn’t a blog so we can discuss it easily.
This is what the class looks like initially
Taking a string and storing it inside itself and then we have to add methods to act on that string
First thing check to see if a our string has no length
1. Add a new test
Start from the beginning assuming no tests for SuperString
First step is to create the test file Laravel places tests that operate on single classes in a unit folder
Want to use that here so create tests/Unit/SuperStringTest.php
Next fill in the file
Create class
Class name needs to match the file name -> new PHPunit doesn’t like mismatch
Extends PHPUnit\Framework\TestCase . TestCase class gives us asserts and setup logic we need
Create the test function. We’re going to name the function based on what we’re testing to it’s easier to understand when future us comes back to it.
Create Our Initial Conditions
Test our result from the function.
Calling our isEmpty function and asserting it’s returning true
Notice how small the actual 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 breaks.
2. 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 method so we get an "undefined method" error.
Red message -> in an error state like we saw in red/green/refactor
3. Make a little change
To reiterate, our goal in this phase is to make the smallest change we can to allow our tests to pass.
Two options here:
add in the obvious implementation
Obvious means is a few lines that we can’t possibility mess up the logic for
This case just call to mb_strlen
(click)
Make the smallest possible change by returning true always.
(click)
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.
We’re going to want to show another cycle of TDD so we’re going to just return true
4. Run all tests and see them all succeed
Now we run our test and verify that our test passes.
Notice green from red/green/refactor
5. Refactor to remove duplication
Our code currently doesn't contain any duplication but it's important not to get lazy and skip this step.
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 other cases where the string isn't empty.
As a general rule, it's a good idea to have tests for normal input, the extremes of inputs (very large or very small), and spots where we can think of oddities happening obviously very domain specific
We’re going to check for a case where the string isn’t empty to add an addition testing point
Add a new test
What we have currently
Add in the opposite
2. Run all tests and see the new one fail
This time we get a failure assertion that true is false
3. Make a little change
Look at our original code
Could change to return false but that would cause other tests to fail
Back to the obvious implementation we had before
4. Run all tests and see them all succeed
5. Refactor to remove duplication
Again due to the simple nature of our example there isn't any duplication in our code at this point.
Starting to get the hang of this
Adding a test now to check for a string that isn’t empty.
Could do !isEmpty() but isNotEmpty is actually easier for us to process as programmers so it’s nice to have
1. Add a new test
Again small
Again written so we know what’s going on quickly
2. Run all tests and see the new one fail
3. 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.
4. Run all tests and see them all succeed
Oh good we didn’t make any mistakes
5. 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.
2 options
Option 1 is to extract that duplication into a new function.
Extracting a new function is favorite refactor because it makes the code more readable and because we'll most likely need the same logic again.
Now The replace the calls with the new function
The second solution is to realize that `isNotEmpty()` returns the boolean opposite of `isEmpty()` and just use it
If I hadn’t been working through this example just to get to this who cares example I think I would have done this automatically.
The first option gives us the best flexibility for future expansion but second less code. Always a fond of less code
Finally, we need to run our tests again to verify that no accidents crept into our code as we made these changes.
In our example, we worked out an example of tests where we only tested a single class. This is know as a unit test.
Unit tests are exceptionally good for testing each piece of our application but what if we want to test how all of the units work together? For example, we may want to test a request against our API endpoints so we can make sure they meet our specs.
Integration tests allow us to do this.
But they come with a dark side
Integration tests tend to be slower because we’ll be initializing 100s of classes and might be interacting with a database which is generally the slowest part of the application stack.
Really going to depend on your application but Unit test might take tenth of a second and an integration might take a second. So it’s not a tenth of second vs 50 seconds
In the long run they add up but it’s so easy to throw additional resources at our testing servers that it can be worth it.
Because the integration tests are testing several components at once it may not be quickly apparent what caused the test to fail. For example, our integration test might hit a dashboard endpoint and one of the components fails because of a poorly written query. The result might just be that an error occurred and not tell us what the error was.
Personally feel they’re worth every bit of time spent on them. Every integration test that fails before it get’s to a customer is one less bug ticket. I use TDD to develop these tests and it can be a dream.
Several PHP frameworks provide facilities that allow tests to simulate an entire web request and validate the output.
This is a laravel example, request /categories page using an HTTP get request
Check to make sure we get a 200 based response code and not a redirect or error message.
I’m a fan of doing integration tests on any endpoint where we’re creating or updating data because they tend to be hard to manually test.
Now that you understand the basics of TDD you can start to use it in our daily work.
It’s one of those need to start using it to understand it. Start doing it. Spent years not fully understanding the benefit and it wasn’t until I committed myself that it really clicked.
Check with manager first - don’t want to get your fired
If we're working by ourselves this can be an easy process because we can easily develop and maintain the test suite.
Using TDD with your team can be more of a challenge
As we're getting started with TDD in our teams we must have management support. Without the organization believing that TDD will help improve the application it might look to the leaders in your organization that the time spent writing tests has been wasted when it could have been spent adding new features. I argue it’s always time well spent
We'll also want to make sure everyone on our team is coding using TDD. By having everyone invested in using TDD we'll quickly build up a huge test suite and give everyone the confidence to make changes to other people's code. It also allows everyone to feel ownership of the tests and keep up with the maintenance of them instead of it falling on the shoulders of a few people.
Recommend doing a group read of the two books on the screen.
I’ve done this a couple times with different groups of developers. It’s helpful to setup a schedule depending and then have a quick discussion about what people found interesting/hard to understand about the assigned reading.
This can be super quick or more slow. I’ve had a new developer read both books in two weeks and I’ve had a team read them over two quarters. It’s all going to depend on your needs.
There are several actions that our team can take to get everyone invested in TDD. The first is to have training on the basics of TDD and how to get started using our codebase. Pick two people to get TDD working well in your codebase and then have a group training to show people how to do it.
The second is to start having code reviews and make sure every code review starts with answering the question “Is this change driven by a test”. If not, the review stops until it is.
The final thing is to have developers that are more consistently using TDD pair program with the developers that aren’t. This will allow for more hands-on experience for the less experienced engineer.
As the suite of tests grows it starts to become part of the maintenance overhead of our project. To that end, we'll need to make sure we prevent running into some of these common gotchas to make our lives easier.
As the amount of time it takes to run the full suite of tests increases the likelihood that we don't run the whole suite also increases.
Because of this, it's very easy to get to a point where so many of the tests are broken that nobody feels confident in them.
It's a best practice to set up a Continuous Integration server like Jenkins, TravisCI, or CircleCI to automatically run all the tests and report back any errors. We can even set them up to prevent new code from reaching our servers if any of the tests fail.
Ideally, all of the automated tests would be run every time a new commit is created but we’ve really commonly combined them into a PR so
When we're setting up our tests it's very easy for us to create `testBlankStringCausesIsEmptyToReturnTrue()` and then immediately create `testValidStringCausesIsEmptyToReturnFalse()` without making sure the former passes before creating the later. This will make it harder for us to get back to the state where all of the tests pass and will make it harder for us to progress because we're trying to solve two problems at the same time. It's always best to write a single test and get it passing before moving on to another.
Individual tests should aspire to only have a single assert function in each test. There's nothing in PHPUnit that prevents us from setting up a single test with 100 assertions but that is an indication that the test is doing too much. We won't be able to quickly look at the test and see what we've done or how to fix it because the test is so large.
Small tests allow us to quickly determine why something has broken and quickly adjust to get it passing again.
Finally
One of the things new practitioners of TDD tend to do is to create tests for trivial functionality. For example, we'll see tests that are set up to test new getters and setters. These tests tend to not be beneficial to our test suite because trivial functions don't change frequently and because the trivial code is usually being run as part of another test.
1. Add a new test
2. Run all tests and see the new one fail
3. Make a little change
4. Run all tests and see them all succeed
5. Refactor to remove duplication