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.
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.
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!
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.
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 |
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!
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!
Howdy,
Good Catch! Yes, I do believe you're correct... It probably would have been better if we did something like:
if (str_starts_with(strtolower($specification), 'carnivorous') {
$isCarnivorous = true;
}
And then also added another specification to the getSpecificationTests()
data provider to ensure that a non-carnivorous
specification was false
when calling $dinosaur->isCarnivorous()
The interesting thing about testing, is this scenario is actually called an "edge case" and it happens all the time... If I ran into this situation in a real project, I would ask myself:
1) How likely is it that we will run into this scenario?
2) If we do run into this scenario, will it cause any undesired side effects?
Depending on those answer's, I would either write a test and make sure it passes, or let sleeping dogs lie and move onto bigger problems...
By the way, did you know we're releasing a new version of this tutorial? Feel free to check it out https://symfonycasts.com/screencast/phpunit and let us know what you think.
Hi there,
This code looks like a bit outdated, maybe? If so, will you plan to update it with SF5?
Best regards.
Hey there,
could you tell me what part of the chapter looks outdated? I don't think we will redo this tutorial but like to keep it up to date by adding notes or updating only a relevant part of the code. Anyways, all the concepts taught here are still relevant
Cheers!
It's not related to this tutorial but the Symfony version which is being used at the tutorial.
I think Symfony4/5 brings us more utilities for testing.
Cheers!
The code block after "and... the scream Emoji!" is broken (maybe because of this Emoji).
Idem for the block after "the screaming Emoji for the second." and the one after "and paste at the bottom.".
Etc...
It seems it is the same issue for all blocks with the Emoji.
Hey Capucine,
Wooops, our bad! I just updated those code blocks - now them have emoji! :)
Thank you for reporting this! If you noticed any missing emoji further in the course - please, let us know and we will be happy to fix them as well.
Cheers!
// composer.json
{
"require": {
"php": "^7.0, <7.4",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/orm": "^2.5", // v2.7.2
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"sensio/distribution-bundle": "^5.0.19", // v5.0.21
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
"symfony/monolog-bundle": "^3.1.0", // v3.1.2
"symfony/polyfill-apcu": "^1.0", // v1.6.0
"symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
"symfony/symfony": "3.3.*", // v3.3.13
"twig/twig": "^1.0||^2.0" // v2.4.4
},
"require-dev": {
"doctrine/data-fixtures": "^1.3", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
"liip/functional-test-bundle": "^1.8", // 1.8.0
"phpunit/phpunit": "^6.3", // 6.5.2
"sensio/generator-bundle": "^3.0", // v3.1.6
"symfony/phpunit-bridge": "^3.0" // v3.4.30
}
}
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".