Buy Access to Course
19.

When Existing Tests Break & Exceptions in __construct()

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

The new example we just added is passing... but we totally broke a bunch of our original examples! Lame! For example, it should be able to add dinosaurs is getting the DinosaursAreRunningRampantException and that makes sense. Find it_should_be_able_to_add_dinosaurs. It's testing to make sure that if we add two dinosaurs, then we should have... 2 dinosaurs.

Existing Tests Broke... now what?

So, hmm: we made a change to our app and then an existing example started to fail. That's... the beauty of tests! Now we can take in all this information about which tests are failing and why they're failing and determine the best way forward. For example, we may have just accidentally introduced a bug and we want to fix that bug. Or we may discover that the tests that are failing are no longer relevant and should be removed. Or, most likely you're in a situation like this one: where you simply need to update existing tests for some new change.

Describing adding Security

For this example, the enclosure needs some security before we starting adding dinosaurs. To do that, well... we need some way to add Security to an Enclosure! And, ya know what? We should probably make it pretty easy to add security - maybe via a new constructor arg.

Let me show you what I mean - in an example! Instead of creating a totally new example function for this, I'm going to need to use this new functionality in the example that's current failing. Here's the idea: to add some basic security, say $this->beConstructedWith(true). Yep, I want there to be a constructor arg that easily allows you to activate some type of security.

54 lines | spec/Entity/EnclosureSpec.php
// ... lines 1 - 11
class EnclosureSpec extends ObjectBehavior
{
// ... lines 14 - 23
function it_should_be_able_to_add_dinosaurs()
{
$this->beConstructedWith(true);
// ... lines 27 - 31
}
// ... lines 33 - 52
}

Let's also add this to the other example that's failing - it's around line 32. Paste! And for the newest example we've been working on, I'll instantiate with false.

54 lines | spec/Entity/EnclosureSpec.php
// ... lines 1 - 33
function it_should_not_allow_to_add_carnivorous_dinosaurs_to_non_carnivorous_enclosure()
{
$this->beConstructedWith(true);
// ... lines 37 - 42
}
// ... line 44
function it_should_not_allow_to_add_dinosaurs_to_unsecure_enclosures()
{
$this->beConstructedWith(false);
// ... lines 48 - 51
}
// ... lines 53 - 54

Ok, let's try the tests:

./vendor/bin/phpspec run

Nice! It asks us to generate the __construct method - yes please! A whole bunch of examples are failing - but, pfff - that's probably fine. Find Enclosure. Perfect!

Change the argument to bool $withBasicSecurity and... I don't need to, but let's give this a default value: if you pass nothing, there is no security. Next, if ($withBasicSecurity), let's... add some security! I'll call a new method we haven't created yet: $this->addSecurity() with new Security() passing that fence... or I guess "fency", whatever that is... true to make it active and $this because it will be attached to this Enclosure.

62 lines | src/Entity/Enclosure.php
// ... lines 1 - 7
class Enclosure
{
// ... lines 10 - 15
public function __construct(bool $withBasicSecurity = false)
{
if ($withBasicSecurity) {
$this->addSecurity(new Security('Fence', true, $this));
}
}
// ... lines 22 - 60
}

For the addSecurity() method, because we're not using this method anywhere outside of this class, we should technically create it as private. But, I already know that I will need to use it outside this class, so let's make it public: public function addSecurity(Security $security). Inside, $this->securities[] = $security.

62 lines | src/Entity/Enclosure.php
// ... lines 1 - 40
public function addSecurity(Security $security)
{
$this->securities[] = $security;
}
// ... lines 45 - 62

Phew! Okay, find your terminal and let's try this!

./vendor/bin/phpspec run

Hmm, not passing yet:

Class App\Entity\Security not found.

Let's see, what did I mess up? It's not clear where that error is. To get more info, use the --verbose flag:

./vendor/bin/phpspec run --verbose

And... ah! There it is: Enclosure at line 19. Find that and... ah again! Our spec/ and src/ directories look so much alike that I copied the Security class into the wrong spot! Move that into src/Entity - good job tests!

Run 'em again:

./vendor/bin/phpspec run

Now they pass.

Exception during Construction???

We're on a roll! Shall we add one more enhancement to Enclosure? It's now easy to create an Enclosure with basic security. But I also want the ability to pass some initial dinosaurs into the constructor. That's cool - but another programmer tried to do this last week and... oof, things got ugly. They did allow for initial dinosaurs to be added, but they forgot to check first to see if any security was active. Oof. Anyways, that programmer is... "unavailable" now.

Let's not make the same mistake: create a new example function: it_should_fail_if_providing_initial_dinosaurs_without_security(). Start with $this->beConstructedWith(false) and an array with one new Dinosaur().

63 lines | spec/Entity/EnclosureSpec.php
// ... lines 1 - 53
function it_should_fail_if_providing_initial_dinosaurs_without_security()
{
$this->beConstructedWith(false, [new Dinosaur()]);
// ... lines 57 - 60
}
// ... lines 62 - 63

Then, we just need to tell phpspec what method will cause the exception. So... wait! This is a bit different: the exception will be thrown during instantiation! Not when we call some other method.

How can we tell phpspec about that? It's almost the same: $this->shouldThrow() with DinosaursAreRunningRampantException::class - I don't care about testing the exact message. Then, ->duringInstantiation().

63 lines | spec/Entity/EnclosureSpec.php
// ... lines 1 - 53
function it_should_fail_if_providing_initial_dinosaurs_without_security()
{
// ... lines 56 - 57
$this
->shouldThrow(DinosaursAreRunningRampantException::class)
->duringInstantiation();
}
// ... lines 62 - 63

That's it. Let's make sure things are failing!

./vendor/bin/phpspec run

Nice and broken: there was no exception thrown. Oh, but notice one thing: this is the first time that we've added an example where we are passing a second argument to the constructor. But, because that method already exists, phpspec is not quite smart enough to automatically generate a second argument for us. Ok, then, I guess we'll do it by hand: add array $initialDinosaurs = [].

66 lines | src/Entity/Enclosure.php
// ... lines 1 - 7
class Enclosure
{
// ... lines 10 - 15
public function __construct(bool $withBasicSecurity = false, array $initialDinosaurs = [])
{
// ... lines 18 - 24
}
// ... lines 26 - 64
}

Next, foreach over $initialDinosaurs as $dinosaurs and say, this->addDinosaur($dinosaur). That was the mistake that other programmer made: I'm using addDinosaur() instead of just setting the $dinosaurs property directly because that method contains the security checks.

66 lines | src/Entity/Enclosure.php
// ... lines 1 - 15
public function __construct(bool $withBasicSecurity = false, array $initialDinosaurs = [])
{
// ... lines 18 - 21
foreach ($initialDinosaurs as $dinosaur) {
$this->addDinosaur($dinosaur);
}
}
// ... lines 26 - 66

So... that should be it! Let's try phpspec:

./vendor/bin/phpspec run

Got it. Next... it's time! It's time to talk about mocking, test doubles and all that fun, testing magic.