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

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.

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

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!

21
Login or Register to join the conversation
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 5 years ago

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.

2 Reply
Abelardo Avatar

Yes, you are right!

Reply

Hey Lijana Z.!

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!

Reply

Hey Lijana Z.

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

Cheers!

Reply
Lijana Z. Avatar

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 :)

Reply

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 :)

1 Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 2 years ago

When we test the functionality to add dinos into our enclosure, no parameters are passed into the Dinosaur's constructor...but it has parameters!
How didn't the tests fail when they were executed?🤔

Reply

Hey Abelardo

You're watching closely ;) - That's because both arguments of the Dinosaur's constructor are optional, so if you don't pass any, it will use the default values instead

Cheers!

1 Reply
Abelardo Avatar

Yes, I watched the video where both parameters were initialized by default with "Unknown" and false respectively.
Thanks!

Best regards.

Reply
Abelardo Avatar

I'm going to rewatch the prior videos because I didn't realize when those parameters were set as optionals.
Thanks for your reply.

Cheers!

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | posted 3 years ago

About mocking model-objects (in my case Doctrine entities): The service which I'm testing makes use of the auto-generated ID of the provided entity. However, the Doctrine entity does not have a setId()-method, so setting an ID just for testing is not possible. That's a problem, because when I feed the entity to my service, the service can't read the entity ID and explodes...

Would you mock the entity, in such a case? Or would you apply some other best practice, here? And if I should create a mock, is it possible to create a mock based on an instance of my entity-class, so I don't have to put a lot of ->willReturn()'s on my mock?

Reply

Hey Thijs-jan V.

That's a good question and there some options you can take

1) Just add a setId() method to the entity but add a PHPDoc @internal flag, so you indicate that such method is only for testing purposes
2) Use Reflection to set whatever value you want on the id property
3) You can mock an entity class, although it's not recommended, it's still valid when you have a complex situation and using a mock just make life easier
4) Refactor your code so it works with the ID value instead of the entity object

You can pick whichever fits better your needs. Cheers!

1 Reply
_zippp Avatar
_zippp Avatar _zippp | posted 3 years ago | edited

it seems like something has changed with a newer library versions, code below (EnclosureTest, line 25) does not pass the test:

<br />$this->assertEquals(2, $enclosure->getDinosaurs());<br />
with the following error:
<i>1) Tests\AppBundle\Entity\EnclosureTest::testItAddsDinosaurs Doctrine\Common\Collections\ArrayCollection Object (...) does not match expected type "integer".
</i>
although it works with explicit integer value:
<br />$this->assertEquals(2, $enclosure->getDinosaurs()->count());<br />

Reply

Hey _zippp

Looks like your line 25 is a little bit different. In course code Line 25 is:

$this->assertCount(2, $enclosure->getDinosaurs());

It's assertCount() not assertEquals(), so probably code you got some outdated code somehow. You can just fix this line, or download course code again to be sure that everything is in sync.

Cheers!

Reply
Default user avatar
Default user avatar toporovvv | posted 5 years ago

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.

Reply

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.

Reply
Default user avatar

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.

Reply

Ohh I see it!

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

Cheers!

1 Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 5 years ago

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.

Reply

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!

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