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 SubscribePark management has just dreamt up a new, fancy feature: they want to be able to create a new dinosaur... just by describing it. They want to be able to say:
Large friendly carnivorous dinosaur
and then, poof! Our DinosaurFactory
will figure out exactly what type of dino to grow and make it.
This means we need a new method in DinosaurFactory
and that means we need a test. How about: itGrowsADinosaurFromASpecification()
, where "specification" is the word we'll use for this "dinosaur description".
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 46 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
... lines 49 - 53 | |
} | |
} |
The API for this future method will be simple: $dinosaur = $this->growFromSpecification()
and pass it some string, like large carnivorous dinosaur
. Maybe we should have included the word 'friendly'....meh, probably not necessary.
Now, add some assertions! Like, $this->assertGreaterThanOrEqual()
that the dinosaur is 20 meters or longer. Actually, instead of hardcoding that value, open up the Dinosaur
class and add a const LARGE = 20
. Use that inside the test.
... lines 1 - 10 | |
class Dinosaur | |
{ | |
const LARGE = 20; | |
... lines 14 - 66 | |
} |
Then, assertTrue
that $dinosaur->isCarnivorous()
and give that a custom failure message.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 46 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
... lines 49 - 50 | |
$this->assertGreaterThanOrEqual(Dinosaur::LARGE, $dinosaur->getLength()); | |
$this->assertTrue($dinosaur->isCarnivorous(), 'Diets do not match'); | |
} | |
} |
Ok! That's a nice-looking test! Before I even try it, let's start the code. In DinosaurFactory
, add public function growFromSpecification()
with a string argument. This will return a Dinosaur
.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
... lines 9 - 13 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
} | |
... lines 17 - 25 | |
} |
Now, our test looks happier... except apparently we do not have a Dinosaur::isCarnivorous()
method. That's awesome! Another example of not adding a method until we need it... which is now. At the bottom of Dinosaur
, add public function isCarnivorous()
and return the property.
... lines 1 - 10 | |
class Dinosaur | |
{ | |
... lines 13 - 62 | |
public function isCarnivorous(): bool | |
{ | |
return $this->isCarnivorous; | |
} | |
} |
Perfect! Our test - and even some of our code - is ready. Run it:
./vendor/bin/phpunit
Whoo! It fails! So... time to code, right?
Well... hold on! Because... this is going to be painful! So far, we're just testing one string. But this is going to be a complex function... we're going to need to test a lot of specifications, like "small herbivore", "huge dinosaur" or maybe even "tiny, adorable, flesh-eating carnivorous dino". To do that, we're going to need to copy and paste this method, over and over again.
Unfortunately... there's no better option. Pff, kidding! Of course there is! Data providers!
Data providers let you run the same test in a loop: passing different arguments each time.
First, create a new public function called getSpecificationTests()
. Yep, this method does not start with the word test
. That's because its job is not to be a test: it's to provide the different test cases that we want to try.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 62 | |
public function getSpecificationTests() | |
{ | |
... lines 65 - 70 | |
} | |
} |
Let's code this first: it will make more sense when you see all the pieces working together. Return an array. Then, I'll add some comments: specification
, is large
and is carnivorous
.
Copy the specification from above. Then, inside the array, create another array with that string, then true
and true
, because we expect this dinosaur to be large and carnivorous. This will be the first test case: we want to test that this spec will create a large, carnivorous dinosaur.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 62 | |
public function getSpecificationTests() | |
{ | |
return [ | |
// specification, is large, is carnivorous | |
['large carnivorous dinosaur', true, true], | |
... lines 68 - 69 | |
]; | |
} | |
} |
Add another item to the array with a completely ridiculous spec: give me all the cookies!!!
. This time, use false
and false
. Management told us that if they say something crazy, the DinosaurFactory
should default to creating small, herbivore dinosaurs... which seems like a pretty safe idea.
Add one last test case: large herbivore
, which we expect to be large true
and carnivorous false
.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 62 | |
public function getSpecificationTests() | |
{ | |
return [ | |
... lines 66 - 67 | |
['give me all the cookies!!!', false, false], | |
['large herbivore', true, false], | |
]; | |
} | |
} |
Ok! We're not done yet... but once we are, PHPUnit will call our test method one time for each item in the array... so three times in total. On each call, it will pass the values as the arguments. So add three arguments: $spec
, $expectedIsLarge
and $expectedIsCarnivorous
.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 49 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsLarge, bool $expectedIsCarnivorous) | |
{ | |
... lines 52 - 60 | |
} | |
... lines 62 - 71 | |
} |
Pass the dynamic spec to growFromSpecification()
. The greaterThanOrEqual
assert will now need to vary, depending on whether or not we're expecting a large or small dinosaur. Add if $expectedIsLarge
. In that case, use the same assert. Else, copy that line, but use assertLessThan()
.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 49 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsLarge, bool $expectedIsCarnivorous) | |
{ | |
... lines 52 - 53 | |
if ($expectedIsLarge) { | |
$this->assertGreaterThanOrEqual(Dinosaur::LARGE, $dinosaur->getLength()); | |
} else { | |
$this->assertLessThan(Dinosaur::LARGE, $dinosaur->getLength()); | |
} | |
... lines 59 - 60 | |
} | |
... lines 62 - 71 | |
} |
Finally, use assertSame()
at the bottom to assert that $expectedIsCarnivorous()
matches $dinosaur->isCarnivorous()
.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 49 | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsLarge, bool $expectedIsCarnivorous) | |
{ | |
... lines 52 - 59 | |
$this->assertSame($expectedIsCarnivorous, $dinosaur->isCarnivorous(), 'Diets do not match'); | |
} | |
... lines 62 - 71 | |
} |
Phew! Ok, the test will not pass yet... but I like errors! Try the tests:
./vendor/bin/phpunit
Too few arguments to testItGrowsADinosaurFromASpecification(): 0 passed and 3 expected.
That makes sense: normally, you are not allowed to have any arguments to your test methods. But with a data provider, you can! We just need to hook it up. How? Above the test method, add @dataProvider getSpecificationTests
.
... lines 1 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 46 | |
/** | |
* @dataProvider getSpecificationTests | |
*/ | |
public function testItGrowsADinosaurFromSpecification(string $spec, bool $expectedIsLarge, bool $expectedIsCarnivorous) | |
{ | |
... lines 52 - 60 | |
} | |
... lines 62 - 71 | |
} |
Woo! Now, it will call the test three times: once for the first data set, passing these as the first, second and third arguments, then a second time, and a third. Check it out:
./vendor/bin/phpunit
Yay! Failures! But, 3 failures! And they're labeled as with data set #0
, #1
and #2
... because us programmers like to count from 0.
This is awesome. Oh, and if you want to, you can assign an array key to any test, like default response
for the second test. Now when you run the test, the second failure is called default response
. I actually do this a lot - it helps figure out which test is actually failing.
Now that we have our three test cases, it's time to move on to Step 2 of TDD and make these pass one-by-one.
"Houston: no signs of life"
Start the conversation!
// 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
}
}