20. Agenda
1. What Is Test-Driven Development?
2. Why You Should Be Using Test-Driven
Development?
3. How To Do Test-Driven Development?
4. Unit and Feature Testing
5. What is Code Coverage?
22. TDD:
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.
23. What is Test-Driven Development?
• Short cycles of 5 phases
• Less Minute
• Dozens/hundreds of cycles each day
Add A
New Test
Run All
Tests to
See
Failures
Make a
change
Run Tests
to See
Success
Refactor
227. What You Need to Know
• TDD: test-first software development process
• Gives us confidence to make changes
• Five short phases
• Each of the phases happens very quickly
• Don’t neglect to refactor
228. What You Need to Know
Add A
New Test
Run All
Tests to
See
Failures
Make a
change
Run Tests
to See
Success
Refactor
229. What You Need to Know
• Use a testing framework like PHPUnit/Pest
• Write clear and concise tests
• Unit tests: tests code in isolation
• Feature tests: tests code as user interacts with it
• Code coverage determines if our code has enough tests
234. Thank Scott!
• Feedback on joind.in
• Follow me on my socials
• @scottkeckwarren@phpc.social
• scottkeckwarren on:
• TikTok/Twitch/X
• Subscribe to YouTube Channel
My grocery store changed layout and couldn’t find coffee for three weeks
As developers, we know that change breaks production
No mater how small the change
I’ve fixed a spelling error in a string and created a bug
That leads to…<click>
Whose heard this one before
<slide>
That’s way my mantra because I don’t want to have to boot my work computer up on Sunday
Maintained a module to set professional development goals
Process had seven steps <click>
Alternated between supervisor and direct <click>
Two “modes” <click>
Testing took a minimum of 10 minutes<click>
Manual so Error prone. Sometimes I would make a mistake in step 2 and not realize to step 6 and have to start over
God forbid I was on vacation and someone else made the change
Because there we so many moving parts:
<slide>
Someone would say can we add a new feature
I would say yes very reluctantly and then tell them 2 days minimum
Confidence that I could make changes
Not break anything
Hard to do with all the manual testing
Looked for a solution
How do we create automated tests
Lots of options
Cloud image in the back
TDD gave me that confidence
For those of you who haven’t met me my name is …
Professional PHP Developer for 15 years
// team lead/CTO role for 10 of those 15
Currently Director of Technology at WeCare Connect
Survey solutions to improve employee and resident retention at skilled nursing facilities
Use PHP for our backend
Also …
Create Educational Videos on PHPArchitect YouTube
Discuss topics helpful for PHP developers
If you want more content like this session follow me on social media and subscriber to our channel
Found at youtube.com/phparch
Could spend hours discussing automated testing with you all. It’s one of my favorite PHP adjacent topics but we only have 50 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 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.
Back to our goals module from before
Problem was on this side because it was manual
<click>
Wrote 200+ automated tests for Goals Module
Still had that complexity but I didn’t care
Was 10 minutes now less 2 minutes
Wasn’t even 2 active minutes I could step away from my computer
Could confidently make a changes
Didn’t forget steps
Cycle time dropped from days to hours
Cycle time dropped from days
To
Hours
Abstract then concrete
TDD consists of 5 SHORT phases
< 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.
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.
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.
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.
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 because it gives explicit steps
This has been all very abstract so lets make it more concrete
Fast
Good starting point for new classes
Been working on an open source project for the YouTube Channel
Value objects
Did a whole video on how awesome value objects are
Nee
Custom classes for each domain type in our application
Provide three things
Problem they solve is this
Have a function to recreate user
Need first, last, email
Later call the function
Made an Oops
<click>
Should be <comment order>
Did email first
Tricky kind of bug
Dare you to find this in code review
Now I can’t mess up calling this
And
Helper functions
before we go further
This is a contrived example
Feel free to roll your eyes as I’m doing stupid things to make my point
Don’t use this as evidence for a future job
I have this value object class to track a City
Only important thing is that we’re storing our information in $value property
Want to add more functionality using TDD
First thing we need is a <slide>
This example uses PHPUnit to run our tests as the project is already using phpunit
Talk about the other framework later
In case you don’t know
Testing Framework
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 …
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
Might notice assert function calls
Each test function contains one or more asserts
Asserts tell PHPUnit to compare an expected value to the value we got from our codeEach test contains one or more asserts
Compare output values <click>
Compare expected value vs output <click>
Check for greater than or equal <click>
Command line tool
Super powerful
Run a single file
Use filter to reduce runs
Example anything that involves users
Our examples today mostly command line
Running using
./vendor/bin/phpunit tests/FileTest.php
1. Add a new test
No tests for City because there wasn’t any logic to test
First step is to create the test file
<screen> is current file
Swap tests for src
And append test
That way can easily find tests for city class
Start with our blank 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
This is just boiler plate so far
Remember this when we talk about Pest
Let’s remember what we want to do because I’ve rambled
Not sure how I want to do this so let’s start coding and figure it out
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
Run the code we’re going to test
Assert the results
Don’t like the useless variable here so we’ll get rid of it
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: <click>
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
2 - Make the smallest possible change by returning true always.
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
Our simple implementation of `somefunction()` 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
1. Add a new test
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
We have working code that fulfills the requirements
Right?
Done?
Maybe?
Let’s look at how this looks in usage
Basic if block
I’m going to come across someFunction and have no idea what it is
Going to slow me down and that’s annoying
Excellent time to refactor and pick a new name
Lots of options
Don’t like this because its a verb so we could be performing an action
isEmpty sounds good to me
Sounds like a sentence
Back to our code now
After refactor
Subjectively better?
No happy accidents
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
Also contrived example
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.
Call it length and copy duplication into body
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 option 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.
That’s how we do unit tests
Until today this is where the “coding” piece ended
Someone on joind.in gave me feedback that it was too simple so
In our example, we worked out an example of tests where we only tested a single class.
Generally that’s where these presentations stop but we can do better right
Slower than unit tests
Working with local databases and services
Great for high coverage (discuss later)
Over on my Twitch stream slowly working on this
Tool to help with Pull Requests
Lots of ideas on what to do
Time to tackle another item off my backlog
Wanted to use all the newest/greatest projects in PHP space for this project so using Pest
Testing Framework
<click>Built on top of PHPUnit
Can use with existing PHPunit tests
There are some differences
Organization is easy
All test files are placed inside a tests folder at the root of the project
A test is a PHP function
Uses expectation API
Built on top of PHPUnit so we can still use assertion api
I love how expressive the expectation API is but tend to fall back to assertions
Command line tool
Super powerful
Again run with not parameters to run all tests
Run a single file
Use filter to reduce runs
Example anything that involves users
Maybe this is more complicated?
Let’s look at a flow diagram of what we’re doing
<Click through steps>
It’s a lot right?
Don’t feel like I can write an easy test for this
Tackle one small piece only
First test: Can access /organizations/all
Only thing that exists now:
GitHub service
Takes the current user’s OAuth token and creates a connection to GitHub using 3rd party package
Only thing that exists now:
GitHub service
Takes the current user’s OAuth token and creates a connection to GitHub
1. Add a new test
2. Run all tests and see the new one fail
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: <click>
It doesn’t do anything but again smallest amount of code possible
4. Run all tests and see them all succeed
5. Refactor to remove duplication
Kept it DRY to start so we’re good
Second test the rest?
1. Add a new test
2. Run all tests and see the new one fail
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: <click>
I’m going to ignore creating the blade file here because it’s not interesting and hard to read
getAllRepositoriesForCurrentUser
Break with process and see actually results
Uggg, look at all the abandoned side projects
<click> But do see ScottsValueObjects
External Dependency
Have zero control over it
Data might change
Worse it requires:
login
active connection to internet
Use a library
This case we’re using Mockery library
Allows us to specify return values from functions
Pest has support to override the service container for our mock
4. Run all tests and see them all succeed
Uggg, look at all the abandoned side projects
But do see ScottsValueObjects
5. Refactor to remove duplication
Again dry code so no refactoring
That’s a feature test
Small amount of testing code tests large amount of user land code
Allows for high code coverage
The "total number of lines of code" only includes lines that are performing operations and not declarations and white space.
As an example, this class
Single test to see that we can initialize our class
Let’s see what our coverage is
Only two lines are executable
Only the assignment is actually run during this test
Calcaluation on the board to help us
<result>
Manual isn’t going to work because our code is always changing
Thankfully
Need to also have XDEBUG or another tool installed
SET mode to coverage
Spits out same result
< 75% -> hard to feel confident making changes
> 90% -> fragile, expensive tests
Personally my team targets 80%
Missing critical stuff we target 95%
Want to make this part of our
Running at the command line also is a little rough so I like to include it in my CI/CD runs
Like to use Codecov but there’s a million of these SaaS companies that do this
Spits out results in our PR
Also shows lines that aren’t being covered by a 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