Handling Object Dependencies
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNow 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()
.
// ... 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
.
// ... 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.
// ... 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
!
// ... 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!
// ... 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.
// ... 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"}
.
// ... lines 1 - 8 | |
/** | |
* @ORM\Entity | |
* @ORM\Table(name="enclosures") | |
*/ | |
class Enclosure | |
{ | |
/** | |
* @var Collection | |
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Dinosaur", mappedBy="enclosure", cascade={"persist"}) | |
*/ | |
private $dinosaurs; | |
// ... lines 20 - 29 | |
} |
In Dinosaur
, add the other side: private $enclosure
with @ORM\ManyToOne
. Point back to the Enclosure
class with inversedBy="dinosaurs"
.
// ... 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.
// ... 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())
.
// ... 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 (likeDinosaur
), 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()
.
// ... 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:
// ... 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.
// ... 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!
Btw so far TDD looks easy with those simple code examlples. But to get a good tutorial, I think you should to take some class from real complex business project and add or modify some feature. I would like to see how its done there. And take some bit more legacy code which needs a bit of refactoring. Thats where I struggle with TDD.