Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Factory Testing

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok: 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
}
}

Planning the Design

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.

Add the Test

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

Coding up DinosaurFactory

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!

Refactor!

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.

Leave a comment!

10
Login or Register to join the conversation
Steven L. Avatar
Steven L. Avatar Steven L. | posted 3 years ago | edited

<a href="https://github.com/sebastianbergmann/phpunit/issues/3369&quot;&gt;assertInternalType is deprecated in PHPUnit</a>.

In this case, use:
<br />$this->assertIsString($dinosaur->getGenus());<br />

Reply

Hey Steven L. thanks for sharing it!

Reply

I'm enjoying this course :D

Reply

Hey felipsmartins

Nice to hear it! Hope you will enjoy all our courses ;)

Cheers!

Reply

Hi,

There is a mistake in the name of function : public function testItGrowsAVelociraptor() in the text.
Thank for this good tutorial.

Reply

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!

Reply

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.

Reply

I have achieved the same result by manually marking 'tests' directory as a test source.

Reply

Ah, yes, you need to mark tests/ folder as "Tests". Thanks for sharing this with others!

Cheers!

1 Reply

no problem, hope it helps somebody!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice