Buy

Handling Object Dependencies

Now that we're building all these dinosaurs... we need a place to keep them! Right now they're running free! Terrorizing the guests! Eating all the ice cream! We need an Enclosure class that will hold a collection of dinosaurs.

You guys know the drill, start with the test! Create EnclosureTest.

We don't want any surprise dinosaurs inside!

Create the new Enclosure() and then check that $this->assertCount(0) matches $enclosure->getDinosaurs().

17 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 7
class EnclosureTest extends TestCase
{
public function testItHasNoDinosaursByDefault()
{
$enclosure = new Enclosure();
$this->assertCount(0, $enclosure->getDinosaurs());
}
}

Ok, good start! Next, inside Entity, create Enclosure. This will eventually be a Doctrine entity, but don't worry about the annotations yet. Add a private $dinosaurs property. And, like normal, add public function __construct() so that we can initialize that to a new ArrayCollection.

26 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 8
class Enclosure
{
... lines 11 - 13
private $dinosaurs;
... line 15
public function __construct()
{
$this->dinosaurs = new ArrayCollection();
}
... lines 20 - 24
}

Back on the property, I'll add @var Collection. That's the interface that ArrayCollection implements.

26 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 8
class Enclosure
{
/**
* @var Collection
*/
private $dinosaurs;
... lines 15 - 24
}

Now that the class exists, go back to the test and add the use statement. Oh... and PhpStorm doesn't like my assertCount() method... because I forgot to extend TestCase!

17 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 4
use AppBundle\Entity\Enclosure;
... lines 6 - 7
class EnclosureTest extends TestCase
{
... lines 10 - 15
}

If we run the test now, it - of course - fails:

./vendor/bin/phpunit

In Enclosure, finish the code by adding getDinosaurs(), which should return a Collection. Summon the tests!

26 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 8
class Enclosure
{
... lines 11 - 20
public function getDinosaurs(): Collection
{
return $this->dinosaurs;
}
}

./vendor/bin/phpunit

We are green! I know, this is simple so far... but stay tuned.

Adding the Annotations

Before we keep going, since the tests are green, let's add the missing Doctrine annotations. With my cursor inside Enclosure, I'll go to the Code->Generate menu - or Command+N on a mac - and select "ORM Class". That's just a shortcut to add the annotations above the class.

31 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 8
/**
* @ORM\Entity
* @ORM\Table(name="enclosures")
*/
class Enclosure
{
... lines 15 - 29
}

Now, above the $dinosaurs property, use @ORM\OneToMany with targetEntity="Dinosaur", mappedBy="enclosure" - we'll add that property in a moment - and cascade={"persist"}.

75 lines src/AppBundle/Entity/Dinosaur.php
... lines 1 - 10
class Dinosaur
{
... lines 13 - 32
/**
* @var Enclosure
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Enclosure", inversedBy="dinosaurs")
*/
private $enclosure;
... lines 38 - 73
}

In Dinosaur, add the other side: private $enclosure with @ORM\ManyToOne. Point back to the Enclosure class with inversedBy="dinosaurs".

75 lines src/AppBundle/Entity/Dinosaur.php
... lines 1 - 10
class Dinosaur
{
... lines 13 - 32
/**
* @var Enclosure
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Enclosure", inversedBy="dinosaurs")
*/
private $enclosure;
... lines 38 - 73
}

That should not have broken anything... but run the tests to be sure!

./vendor/bin/phpunit

Dependent Objects

Testing that the enclosure starts empty is great... but we need a way to add dinosaurs! Create a new method: testItAddsDinosaurs(). Then, instantiate a new Enclosure() object.

28 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 8
class EnclosureTest extends TestCase
{
... lines 11 - 17
public function testItAddsDinosaurs()
{
$enclosure = new Enclosure();
... lines 21 - 25
}
}

Design phase! How should we allow dinosaurs to be added to an Enclosure? Maybe... an addDinosaur() method. Brilliant! $enclosure->addDinosaur(new Dinosaur()).

28 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 8
class EnclosureTest extends TestCase
{
... lines 11 - 17
public function testItAddsDinosaurs()
{
... lines 20 - 21
$enclosure->addDinosaur(new Dinosaur());
... lines 23 - 25
}
}

And this is where things get interesting. For the first time, in order to test one class - Enclosure - we need an object of a different class - Dinosaur. A unit test is supposed to test one class in complete isolation from all other classes. We want to test the logic of Enclosure, not Dinosaur.

This is why mocking exists. With mocking, instead of instantiating and passing real objects - like a real Dinosaur object - you create a "fake" object that looks like a Dinosaur, but isn't. As you'll see in a few minutes, a mock object gives you a lot of control.

Mock the Dinosaur?

So... should we mock this Dinosaur object? Actually... no. I know we haven't even seen mocking yet, but let me give you a general rule to follow:

When you're testing an object (like Enclosure) and this requires you to create an object of a different class (like Dinosaur), only mock this object if it is a service. Mock services, but don't mock simple model objects.

Let me say it a different way: if you're organizing your code well, then all classes will fall into one of two types. The first type - a model class - is a class whose job is basically to hold data... but not do much work. Our entities are model classes. The second type - a service class - is a class whose main job is to do work, but it doesn't hold much data, other than maybe some configuration. DinosaurFactory is a service class.

As a rule, you will want to mock service classes, but you do not need to mock model classes. Why not? Well, you can... but usually it's overkill. Since model classes tend to be simple and just hold data, it's easy enough to create those objects and set their data to whatever you want.

If this does not make sense yet, don't worry. We're going to talk about mocking very soon.

Let's add one more dinosaur to the enclosure. And then check that $this->assertCount(2) equals $enclosure->getDinosaurs().

28 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 8
class EnclosureTest extends TestCase
{
... lines 11 - 17
public function testItAddsDinosaurs()
{
... lines 20 - 21
$enclosure->addDinosaur(new Dinosaur());
$enclosure->addDinosaur(new Dinosaur());
$this->assertCount(2, $enclosure->getDinosaurs());
}
}

Try the test!

./vendor/bin/phpunit

Of course, it fails due to the missing method. Open Enclosure and create public function addDinosaur() with a Dinosaur argument. When you finish, try the tests again:

36 lines src/AppBundle/Entity/Enclosure.php
... lines 1 - 12
class Enclosure
{
... lines 15 - 30
public function addDinosaur(Dinosaur $dinosaur)
{
$this->dinosaurs[] = $dinosaur;
}
}

./vendor/bin/phpunit

Oh, and one last thing! Instead of $this->assertCount(0), you can use $this->assertEmpty()... which just sounds cooler. It works the same.

28 lines tests/AppBundle/Entity/EnclosureTest.php
... lines 1 - 8
class EnclosureTest extends TestCase
{
public function testItHasNoDinosaursByDefault()
{
... lines 13 - 14
$this->assertEmpty($enclosure->getDinosaurs());
}
... lines 17 - 26
}

Ok, now let's talk exceptions!

Leave a comment!