Buy

Handling Object Dependencies

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

Login Subscribe

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().

... 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 (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().

... 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!

Leave a comment!

  • 2018-05-28 Diego Aguiar

    haha, yeah, we all will work on a legacy project eventually, but the intention is to be able to start new projects where the base code will not rot, or to improve old projects little by little.
    Once again, read that book, you will find a lot of tricks when working with legacy code :)

  • 2018-05-27 Coder

    I don't now, but I believe in future probably everyone need to work with legacy code over their carreers. Unless you are chnaging jobs once you get legacy project :)

  • 2018-05-24 Diego Aguiar

    Hey Coder

    If you constantly work with wild legacy code, I recommend you this book: https://www.amazon.com/Work...
    It's pretty good :)

    Cheers!

  • 2018-05-24 Coder

    ok.

  • 2018-05-21 weaverryan

    Hey Coder!

    Yea, we try to make things as realistic as possible, but there are always "uglier" situations out in the "wild". In your situation, if you have legacy code that needs refactoring, then this likely means that it does not have any tests yet. So, TDD is a bit different in this case. Really, you have a few options: (1) trying to add some tests to the existing code to make sure you don't break anything and then doing TDD with the new feature change or (2) trying to extract the complex part into a new class and do TDD there, leaving all of the other, old logic in its original location.

    It's hard to describe, but *when* I do TDD (which, as you know is far from always), it is in situations that feel very natural. What I mean is, it is in situations where the business logic requires a class/function that has various input and various output. So, I naturally *want* to do TDD in these situations: where I can test all the input / output before writing the code. In other situations, where the class/function I need to write does *not* have a lot of input/output variations (e.g. maybe a function that does 1 thing, saves something to a database, then does 1 other thing), I often won't test it directly. Honestly, a lot of our tests are ultimately functional tests, because the individual units are not that complex, but we want to make sure the feature works. We more often do TDD with functional tests. For legacy code, this would mean first writing a functional test to see that the existing feature works, then write a new functional test for the new feature, then coding that feature. TDD... but on the functional test level.

    Cheers!

  • 2018-05-21 Victor Bocharsky

    Hey Coder,

    It depends, but I think as far as your functions in entities simple and useful - it's ok to have them there. And yes, you need to think about mocking entities only when you really need it, i.e. when it's much simpler to mock entity instead of setting proper data to it, but in most cases you probably don't need to do that ;)

    Cheers!

  • 2018-05-20 Coder

    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.

  • 2018-05-20 Coder

    Interesting about mocking entities. I had heard that we should not mock it, but when entities used to have some function which is not getter or setter, I felt like I need to mock that. So I learned that we can mock them. I used to mock them, but I thought I was doing poor tests and code was poor in entiy because it was having function other than getter and setter. I thought problem is that it is not good for tests if entity has other than getter and setter function.

  • 2018-04-18 Diego Aguiar

    Ohh I see it!

    It's already fixed now, thanks for informing us about that bug.

    Cheers!

  • 2018-04-18 toporovvv

    I'm talking about the code below this text:
    "Now, above the $dinosaurs property, use @ORM\OneToMany with targetEntity="Dinosaur", mappedBy="enclosure" - we'll add that property in a moment - and cascade={"persist"}."
    There should be Enclosure entity, but there is a Dinosaur.

    Then, below this code and after the text block - "In Dinosaur, add the other side: private $enclosure with @ORM\ManyToOne. Point back to the Enclosure class with inversedBy="dinosaurs" - exactly the same code as above.

  • 2018-04-18 Diego Aguiar

    Hey toporovvv

    What annotations are you talking about? About the relationship between Dinosaur and Enclosures? If that's the case, we are just declaring the inverse side of the relationship.

    Have a nice day.

  • 2018-04-18 toporovvv

    There is a mistake in the code of this chapter: when we make annotations for enclosure and dinosaurs in the entities Dinosaur entity is covered two times.