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: our dinosaur park is going to be huge! Like, dinosaurs everywhere. So we can't keep creating dinosaurs by hand. Nope, we need a DinosaurFactory
!
You guys know the drill: start with the test. I think the class should eventually live in a Factory
directory, so create a Factory
folder inside tests
. Then, add a new PHP class. But, wait! PhpStorm isn't auto-filling the namespace. That's annoying!
Let's fix it! Go into the PhpStorm Preferences and search for Composer. Ah, find the Composer section. This is a really cool feature - I don't know why it's not enabled by default. Click to select your composer.json
path, and then make sure the "Synchronize IDE Settings" check box is checked.
Woh! The entire tests/
directory just turned green... like a dinosaur! PhpStorm reads the autoload-dev
section of composer.json
and now knows what namespace to use. Creepy.... Create a new PHP class again: DinosaurFactoryTest
.
Make it extend the usual TestCase
from PHPUnit. And add a new method: public function testItGrowsALargeVelociraptor()
.
... lines 1 - 7 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
public function testItGrowsALargeVelociraptor() | |
{ | |
... lines 12 - 18 | |
} | |
} |
Ok, let's think about the design of this new class. There are a few dinosaurs that people just love - like velociraptors, tyrannosaurus and triceratops. To grow those quickly, I think it would be cool to add methods on DinosaurFactory
like growVelociraptor()
. Each method would already know the correct genus name and isCarnivorous
value... so the only argument we need is $length
.
Awesome! Let's start using this imaginary class. First, create it: $factory = new DinosaurFactory()
. Then, $dinosaur = $factory->growVelociraptor()
and pass in the length.
... lines 1 - 7 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
public function testItGrowsALargeVelociraptor() | |
{ | |
$factory = new DinosaurFactory(); | |
$dinosaur = $factory->growVelociraptor(5); | |
... lines 14 - 18 | |
} | |
} |
Next, add some basic checks: $this->assertInstanceOf()
to make sure that this returns a Dinosaur::class
instance. We can also use $this->assertInternalType()
to make sure that a string
is what we get back from $dinosaur->getGenus()
.
Let's also check that value exactly - it should match Velociraptor
. And make sure that 5 is the length.
... lines 1 - 7 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
public function testItGrowsALargeVelociraptor() | |
{ | |
... lines 12 - 14 | |
$this->assertInstanceOf(Dinosaur::class, $dinosaur); | |
$this->assertInternalType('string', $dinosaur->getGenus()); | |
$this->assertSame('Velociraptor', $dinosaur->getGenus()); | |
$this->assertSame(5, $dinosaur->getLength()); | |
} | |
} |
Perfect! There's one cool thing you may not have noticed: we're now indirectly testing some of the methods on Dinosaur
. Yep, if we have a bug in getGenus()
or getLength()
, we'll discover that here... even if we don't have a test specifically for them. This is a good thing to keep in mind when trying to decide what to test. Sure, having a specific test for getLength()
is the strongest way to ensure it keeps working. But since that method is pretty simple... and since it's indirectly tested here, that's more than enough in a real project.
Ok, step 1 of TDD is done! Let's run the test to make sure it fails:
./vendor/bin/phpunit
Yes! Let's code! Create the new Factory
directory, then the class inside: DinosaurFactory
. With TDD, writing the code is easy: we already know the method's name, its arguments and exactly how it should behave. Add public function growVelociraptor
. We know this needs a $length
argument and that it will return a Dinosaur
object.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
public function growVelociraptor(int $length): Dinosaur | |
{ | |
... lines 11 - 15 | |
} | |
} |
Create the new Dinosaur
object inside and pass it Velociraptor
and true for the isCarnivorous
argument. Set the length to $length
and return that fresh, terrifying dinosaur!
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
public function growVelociraptor(int $length): Dinosaur | |
{ | |
$dinosaur = new Dinosaur('Velociraptor', true); | |
$dinosaur->setLength($length); | |
return $dinosaur; | |
} | |
} |
Back in DinosaurFactoryTest
, add the use
statement for the new class.
... lines 1 - 2 | |
namespace Tests\AppBundle\Factory; | |
... lines 4 - 5 | |
use AppBundle\Factory\DinosaurFactory; | |
... lines 7 - 8 | |
class DinosaurFactoryTest extends TestCase | |
{ | |
... lines 11 - 20 | |
} |
Ok, run the tests!
./vendor/bin/phpunit
Woh! It fails!
Undefined method Dinosaur::getGenus()
That makes sense! And PhpStorm was trying to warn us. This is actually really cool: TDD helps you to think about what methods you need and what methods you do not need. We could have automatically added getter and setter methods for every property in Dinosaur
. But, that's totally unnecessary! Less methods means less opportunity for bugs: only add methods when you need them.
Add the getGenus()
method and return that property. Try the tests again:
... lines 1 - 10 | |
class Dinosaur | |
{ | |
... lines 13 - 55 | |
public function getGenus(): string | |
{ | |
return $this->genus; | |
} | |
} |
./vendor/bin/phpunit
They pass!
And that means it's time to refactor! Since we're going to eventually create a lot of dinosaurs, let's create a new private function called createDinosaur()
with three arguments: $genus
, $isCarnivorous
and $length
.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
... lines 9 - 13 | |
private function createDinosaur(string $genus, bool $isCarnivorous, int $length) | |
{ | |
... lines 16 - 18 | |
} | |
} |
Use those on the new Dinosaur()
.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
... lines 9 - 13 | |
private function createDinosaur(string $genus, bool $isCarnivorous, int $length) | |
{ | |
$dinosaur = new Dinosaur($genus, $isCarnivorous); | |
$dinosaur->setLength($length); | |
} | |
} |
Above in growVelociraptor()
. return $this->createDinosaur()
and pass Velociraptor
, true
and the length.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
public function growVelociraptor(int $length): Dinosaur | |
{ | |
return $this->createDinosaur('Velociraptor', true, $length); | |
} | |
... lines 13 - 19 | |
} |
And because this has a test, it will tell us if we made any mistakes. But I doubt that... try the test!
./vendor/bin/phpunit
Explosion!
Return value of DinosaurFactory::growVelociraptor must be a dinosaur: null returned
Whooops. I forgot my return value. Let's even add a return-type for that method.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
... lines 9 - 13 | |
private function createDinosaur(string $genus, bool $isCarnivorous, int $length): Dinosaur | |
{ | |
... lines 16 - 19 | |
return $dinosaur; | |
} | |
} |
This is the power of test-driven development... and testing in general.
./vendor/bin/phpunit
Now the tests pass.
Next, let's talk about a few hooks in PHPUnit that we can use to organize our test setup.
Hi,
There is a mistake in the name of function : public function testItGrowsAVelociraptor() in the text.
Thank for this good tutorial.
Hey Stephane
Nice catch! thanks for letting us know :)
Oh, one more thing, if you find any other error like this, and you feel like you would like to fix it by yourself, you can do it!
There is a "Edit on Github" button at the right upper corner of the script content, or just ping us like you did
Cheers!
Hey, what version of PhpStorm are you using? Mine has slightly less options for "Composer":
https://www.dropbox.com/s/b...
composer.json path was set by default, but auto-namespace doesn't work.
Ah, yes, you need to mark tests/ folder as "Tests". Thanks for sharing this with others!
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
}
}
<a href="https://github.com/sebastianbergmann/phpunit/issues/3369">assertInternalType is deprecated in PHPUnit</a>.
In this case, use:
<br />$this->assertIsString($dinosaur->getGenus());<br />