Coding, Adding Features, Refactoring
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOk, let's code! And remember: don't over-think things: just focus on getting each test to pass. Let's start with the second test, the default values.
Attacking Test #1
Inside DinosaurFactory
, I'll paste a few default values: we'll use $codeName
as the genus, because these are experimental dinosaurs, set the $length
to be a small dinosaur, and create leaf-eating friends.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// defaults | |
$codeName = 'InG-' . random_int(1, 99999); | |
$length = random_int(1, Dinosaur::LARGE - 1); | |
$isCarnivorous = false; | |
// ... lines 20 - 23 | |
} | |
// ... lines 25 - 33 | |
} |
Yep, with these values, our second test should be happy. Finish the function: $dinosaur = $this->createDinosaur()
with $codeName
, $isCarnivorous
and $length
. Then, return $dinosaur
. Oh... and it doesn't really matter... but let's move this function up: I like to have my public functions above private ones.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 20 | |
$dinosaur = $this->createDinosaur($codeName, $isCarnivorous, $length); | |
return $dinosaur; | |
} | |
// ... lines 25 - 33 | |
} |
Ok, that should be enough to get one test to pass. Run 'em:
./vendor/bin/phpunit
Yes! Failure... dot... failure.
Attacking Test #2
Keep going! Let's work on the last test next: if the spec has the word large in it, it should be a large dinosaur. That's easy enough: inside the method: use stripos
to check if the $specification
contains large
. Because if it does... we need a bigger length! Generate a random number between the LARGE
constant and 100... which would be a horrifyingly big dinosaur.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 20 | |
if (stripos($specification, 'large') !== false) { | |
$length = random_int(Dinosaur::LARGE, 100); | |
} | |
// ... lines 24 - 27 | |
} | |
// ... lines 29 - 37 | |
} |
And just like that, another test passes!
Attack Test #3
This is fun! It's like, every time I write a line of code, Sebastian Bergmann is personally giving me a high five!
Ok, the last test is one where the spec includes the word carnivorous
. What's the quickest way to get this test to pass? Just copy the if
statement, paste it, change the string to carnivorous
and set isCarnivorous
to true
.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 24 | |
if (stripos($specification, 'carnivorous') !== false) { | |
$isCarnivorous = true; | |
} | |
// ... lines 28 - 31 | |
} | |
// ... lines 33 - 41 | |
} |
And now... thanks to the power of TDD... they all pass! That felt great.
We Want HUGE Dinosaurs
And management already loves this feature. But... they don't think the dinosaurs are big enough. Now, they want to use the word "huge" to grow mouth-gaping dinosaurs! They've gone mad!
No problem! Thanks to the power of data providers, we can just add more test cases! Or... if you feel like this method is already doing enough, you can create another test. Let's do that: testItGrowsAHugeDinosaur()
with only a $specification
argument. Grow the dino with $dinosaur = $this->factory->growFromSpecification()
. Then, check to make sure it's huge with $this->assertGreaterThanOrEqual()
.
// ... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
// ... lines 11 - 75 | |
public function testItGrowsAHugeDinosaur(string $specification) | |
{ | |
$dinosaur = $this->factory->growFromSpecification($specification); | |
$this->assertGreaterThanOrEqual(Dinosaur::HUGE, $dinosaur->getLength()); | |
} | |
// ... lines 82 - 90 |
Oh, but we need to define what huge means. Back in Dinosaur
, add const HUGE = 30
. And management decided to make the large dinosaurs a bit smaller - set LARGE
to 10.
// ... lines 1 - 10 | |
class Dinosaur | |
{ | |
const LARGE = 10; | |
const HUGE = 30; | |
// ... lines 15 - 67 | |
} |
Use the constant in the test and compare it with $dinosaur->getLength()
.
// ... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
// ... lines 11 - 75 | |
public function testItGrowsAHugeDinosaur(string $specification) | |
{ | |
// ... lines 78 - 79 | |
$this->assertGreaterThanOrEqual(Dinosaur::HUGE, $dinosaur->getLength()); | |
} | |
// ... lines 82 - 90 |
Huge Data Provider
With the test function done, create the data provider: getHugeDinosaurSpecTests()
. Just like before, make this return an array. Each individual test case will also be an array like last time, but now with only one item inside. Test for 'huge dinosaur
, then also huge dino
, just the word huge
and, of course, OMG
and... the scream Emoji!
// ... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
// ... lines 11 - 82 | |
public function getHugeDinosaurSpecTests() | |
{ | |
return [ | |
['huge dinosaur'], | |
['huge dino'], | |
['huge'], | |
['OMG'], | |
['?'], | |
]; | |
} | |
} |
Back on the test method, connect it to the provider: @dataProvider getHugeDinosaurSpecTests
.
// ... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
// ... lines 11 - 72 | |
/** | |
* @dataProvider getHugeDinosaurSpecTests | |
*/ | |
public function testItGrowsAHugeDinosaur(string $specification) | |
// ... lines 77 - 90 |
Ok, let's watch some tests fail! Go Sebastian go!
./vendor/bin/phpunit
Beautiful failures! Five new test cases and five new failures. Time to code!
In DinosaurFactory
, this method is going to start getting ugly... but I don't care! Remember, our main job is to get the tests to pass, not to write really fancy code. TDD helps keep us focused.
First, update the large
if statement to make sure it creates large, but not HUGE dinosaurs. We could have updated our test first before making this change.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 24 | |
if (stripos($specification, 'large') !== false) { | |
$length = random_int(Dinosaur::LARGE, Dinosaur::HUGE - 1); | |
} | |
// ... lines 28 - 35 | |
} | |
// ... lines 37 - 45 | |
} |
Now, let's handle the HUGE dinos. Copy the large
if statement, change the search text to huge
, and generate a length between HUGE
and 100.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 20 | |
if (stripos($specification, 'huge') !== false) { | |
$length = random_int(Dinosaur::HUGE, 100); | |
} | |
// ... lines 24 - 35 | |
} | |
// ... lines 37 - 45 | |
} |
Run the tests!
./vendor/bin/phpunit
Easy! 3 of the 5 already pass: just OMG and the screaming Emoji left! Copy the huge
if statement and paste two more times. Use OMG
on the first and the screaming Emoji for the second.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 24 | |
if (stripos($specification, 'OMG') !== false) { | |
$length = random_int(Dinosaur::HUGE, 100); | |
} | |
// ... line 28 | |
if (strpos($specification, ' |
I know... there's so much duplication! It's so ugly. But... I don't care! I love it because the tests do pass!
Refactoring our Ugly Code
And that means we've reached step 3 of TDD: refactor! I don't actually love ugly code - it's just that it wasn't time to worry about it yet. TDD helps you focus on writing your business logic correctly first, and then on improving the code.
So let's makes this better. Actually, if you downloaded the course code, then you should have a tutorial/
directory with a DinosaurFactory.php
file inside. Copy the private function
from that file, find our DinosaurFactory
, and paste at the bottom.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 38 | |
private function getLengthFromSpecification(string $specification): int | |
{ | |
$availableLengths = [ | |
'huge' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'omg' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'?' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'large' => ['min' => Dinosaur::LARGE, 'max' => Dinosaur::HUGE - 1], | |
]; | |
$minLength = 1; | |
$maxLength = Dinosaur::LARGE - 1; | |
foreach (explode(' ', $specification) as $keyword) { | |
$keyword = strtolower($keyword); | |
if (array_key_exists($keyword, $availableLengths)) { | |
$minLength = $availableLengths[$keyword]['min']; | |
$maxLength = $availableLengths[$keyword]['max']; | |
break; | |
} | |
} | |
return random_int($minLength, $maxLength); | |
} | |
} |
This is still a bit complex, but it removes the duplication and makes the length calculation more systematic. Copy the method name, scroll up, delete all that ugly length logic... and just say $length = $this->getLengthFromSpecification($specification)
.
// ... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
// ... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 16 - 17 | |
$length = $this->getLengthFromSpecification($specification); | |
// ... lines 19 - 27 | |
} | |
// ... lines 29 - 44 |
My new code probably doesn't contain any bugs... but you should totally not trust me! I mess up all the time! Just run the tests.
./vendor/bin/phpunit
Ha! It works! And you doubted me....
Next! What if you need to test that a method throws an exception under certain conditions? Like... if you try to put a T-Rex in the same enclosure as a nice, friendly Brontosaurus. Let's find out!
Declaring the dinosaur carnivorous if "carnivorous" is part of the string, doesn't really work. The dataProvider test would also succeed if the specification was "non-carnivorous".