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 SubscribeDinosaurs, check! Enclosures, check! But... we forgot to add security to the enclosures! Ya know, like electric fences! Dang it, I knew we forgot something. The dinosaurs have been escaping their enclosure and... of course, terrorizing the guests. The investors are not going to like this...
Inside Enclosure
, add a new securities
property: this will be a collection of Security
objects - like "fence" security or "watch tower". We'll create that class in a minute. Anyways, if this collection is empty, there's no security! So we cannot let people put dinosaurs here.
... lines 1 - 13 | |
class Enclosure | |
{ | |
... lines 16 - 21 | |
private $securities; | |
... lines 23 - 47 | |
} |
Let's create another custom exception class: DinosaursAreRunningRampantException
. This time, make sure it extends \Exception
.
... lines 1 - 4 | |
class DinosaursAreRunningRampantException extends \Exception | |
{ | |
} |
Perfect!
Inside EnclosureTest
, add a new method: testItDoesNotAllowToAddDinosToUnsecureEnclosures
. And yea... this is pretty simple: just create the new Enclosure()
, and then add the dinosaur. But first, this time, I want to test for the exception class and exception message. You can do both via annotations, or right here with $this->expectException(DinosaursAreRunningRamptantException::class)
and $this->expectExceptionMessage('Are you craaazy?!?')
.
Below that, add a new Dinosaur()
.
... lines 1 - 10 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 13 - 51 | |
public function testItDoesNotAllowToAddDinosToUnsecureEnclosures() | |
{ | |
$enclosure = new Enclosure(); | |
$this->expectException(DinosaursAreRunningRampantException::class); | |
$this->expectExceptionMessage('Are you craaazy?!?'); | |
$enclosure->addDinosaur(new Dinosaur()); | |
} | |
} |
Nice! Except... yea... this is going to make all of our tests fail: none of those Enclosures have security. Let's worry about that later: focus on this test.
In fact, copy the method name and find your terminal. To avoid noise, I want to run only this one test. You can do that with ./vendor/bin/phpunit --filter
and then the method:
./vendor/bin/phpunit --filter testItDoesNotAllowToAddDinosToUnsecureEnclosures
Awesome! One test and one failure. We'll talk more about --filter
soon!
Ok, step 2 of TDD: code! First, we need a Security
class. If you downloaded the course code, you should have a tutorial/
directory with a Security
class inside. Paste that into our Entity
directory.
... lines 1 - 2 | |
namespace AppBundle\Entity; | |
... line 4 | |
use Doctrine\ORM\Mapping as ORM; | |
... line 6 | |
/** | |
* @ORM\Entity | |
* @ORM\Table(name="securities") | |
*/ | |
class Security | |
{ | |
/** | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="AUTO") | |
* @ORM\Column(type="integer") | |
*/ | |
private $id; | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $name; | |
/** | |
* @ORM\Column(type="boolean") | |
*/ | |
private $isActive; | |
/** | |
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\Enclosure", inversedBy="securities") | |
*/ | |
private $enclosure; | |
public function __construct(string $name, bool $isActive, Enclosure $enclosure) | |
{ | |
$this->name = $name; | |
$this->isActive = $isActive; | |
$this->enclosure = $enclosure; | |
} | |
public function getIsActive(): bool | |
{ | |
return $this->isActive; | |
} | |
} |
It's pretty simple: it has a name
, an isActive
boolean and a reference to the Enclosure
it's attached to.
Speaking of Enclosure
, initialize its $securities
property to a new ArrayCollection
. Oh, and on the property, add @var Collection|Security[]
.
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 22 | |
/** | |
* @var Collection | |
... line 25 | |
*/ | |
private $securities; | |
public function __construct(bool $withBasicSecurity = false) | |
{ | |
... line 31 | |
$this->securities = new ArrayCollection(); | |
... lines 33 - 36 | |
} | |
... lines 38 - 77 | |
} |
Really, this will be a Collection
instance. But the Security[]
part tells our editor that this is a collection of Security objects. And that will give us better auto-completion. Which we can only enjoy if we get this dino security going, so let's get to it!
Down in addDinosaur()
, we need to know if this Enclosure
has at least one active security. Add a method to help with that: public function isSecurityActive()
. I'm making this public only because I already know I'm going to use it later outside of this class.
Set this to return a bool
and then loop! Iterate over $this->securities
as $security
. And if $security->getIsActive()
, return true
. If there are no active securities, run for your life! And also return false
at the bottom.
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 67 | |
public function isSecurityActive(): bool | |
{ | |
foreach ($this->securities as $security) { | |
if ($security->getIsActive()) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
Finish things in addDinosaur()
: if not $this->isSecurityActive()
, throw a new DinosaursAreRunningRampantException()
. And remember, we're checking for an exact message: so use the string from the test.
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 43 | |
public function addDinosaur(Dinosaur $dinosaur) | |
{ | |
if (!$this->isSecurityActive()) { | |
throw new DinosaursAreRunningRampantException('Are you craaazy?!?'); | |
} | |
... lines 49 - 54 | |
} | |
... lines 56 - 77 | |
} |
Ok, I think we're done! Go tests go!
./vendor/bin/phpunit --filter testItDoesNotAllowToAddDinosToUnsecureEnclosures
Yes! It's now impossible to add a Dinosaur
to an Enclosure... unless there's some security to keep it inside.
We've reached the third step of TDD once again: refactor. Actually, I don't need to refactor, but now is a great time to add the missing Doctrine annotations. Above the $securities
property, add @ORM\OneToMany()
with targetEntity="Security"
and mappedBy="enclosure"
. enclosure
is the name of the property on the other side of the relation. Finish it with cascade={"persist"}
.
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 22 | |
/** | |
... line 24 | |
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Security", mappedBy="enclosure", cascade={"persist"}) | |
*/ | |
private $securities; | |
... lines 28 - 77 | |
} |
Ok, this one test still passes... but what about the rest? Run them:
./vendor/bin/phpunit
Ah! We have so many DinosaursAreRunningRampantException
errors! Yep, we knew this was coming: our existing tests need security.
To make this easy, inside Enclosure
, add a bool $withBasicSecurity
argument. Then, if this is true, let's add some basic security! $this->addSecurity()
- we'll create this method next - new Security('Fence', true)
- for isActive
- and then $this
for the Enclosure.
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 28 | |
public function __construct(bool $withBasicSecurity = false) | |
{ | |
... lines 31 - 33 | |
if ($withBasicSecurity) { | |
$this->addSecurity(new Security('Fence', true, $this)); | |
} | |
} | |
... lines 38 - 77 | |
} |
Add the missing method public function addSecurity()
, append the securities array and... we're done!
... lines 1 - 14 | |
class Enclosure | |
{ | |
... lines 17 - 56 | |
public function addSecurity(Security $security) | |
{ | |
$this->securities[] = $security; | |
} | |
... lines 61 - 77 | |
} |
Inside EnclosureTest
, the first test method does not need security: it never adds any dinosaurs. But the next three do: pass true
, true
and true
.
... lines 1 - 10 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 13 - 19 | |
public function testItAddsDinosaurs() | |
{ | |
$enclosure = new Enclosure(true); | |
... lines 23 - 27 | |
} | |
... line 29 | |
public function testItDoesNotAllowCarnivorousDinosToMixWithHerbivores() | |
{ | |
$enclosure = new Enclosure(true); | |
... lines 33 - 38 | |
} | |
... lines 40 - 43 | |
public function testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure() | |
{ | |
$enclosure = new Enclosure(true); | |
... lines 47 - 49 | |
} | |
... lines 51 - 60 | |
} |
Let's do the tests!
./vendor/bin/phpunit
Yes! This is amazing! We just created a dinosaur park with security. What a novel idea!
I have a little question, if i cant execute phpUnit with .vendor/bin/phpUnit, what can i do it?
Hey Sebastian R.
Seems like your local machine is not executing the phpunit binary. Have you tried running it like this:php vendor/bin/phpunit
(without ./ at the begining)
Another thing that may be the problem is the file permissions. Check that your user can execute such file
Cheers!
Is this good practice to add such check in the constructor? I never add such, unless somebody forces me to do so.
Hey Lijana Z.
Do you mean the `$withBasicSecurity` check? If that's the case, I would say yes because it is part of the initialization process and it's pretty simple
Cheers!
I would go with that approach, but if in the future that security check necessitates more logic, then you can refactor it into it's own service, or depends on your requirements, you may want to apply a design pattern, like the "visitor" or "builder" pattern.
// 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
}
}
I have tried to execute phpUnit through > php ./vendor/bin/simple-phpunit tests/appBundle/Controllerbut i recive:
`
if [ -d /proc/cygdrive ]; then
fi
"${dir}/simple-phpunit" "$@"
C:\xampp\htdocs\doki\dev2>php ./vendor/bin/simple-phpunit tests/appBundle/Controller
dir=$(cd "${0%[/\]*}" > /dev/null; cd "../symfony/phpunit-bridge/bin" && pwd)
if [ -d /proc/cygdrive ]; then
fi
"${dir}/simple-phpunit" "$@"
C:\xampp\htdocs\doki\dev2>php ./vendor/bin/simple-phpunit tests/appBundle/Controller/SeoControllerTest.php
dir=$(cd "${0%[/\]*}" > /dev/null; cd "../symfony/phpunit-bridge/bin" && pwd)
if [ -d /proc/cygdrive ]; then
fi
"${dir}/simple-phpunit" "$@"`