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 SubscribeOk first, we've learned a lot about phpspec so far. But... we've still only described one class - and a pretty simple one! It's time to dig deeper and add more complexity to our app.
Here's the deal: that new growVelociraptor()
factory method has made our life a lot easier because, in our pretend app, we constantly need to create new velociraptors. But now, we also need to be able to create a few other popular dinosaurs - like T-rexes and Stegosaurus! We could keep adding more static methods to Dinosaur
. But to keep things organized, I'd rather put all the logic into a new class - how about DinosaurFactory
. Or, we might choose to do this because creating a Dinosaur requires some other services - like a database object - and we can't access services from simple model classes like Dinosaur
.
So, hey! We need a new class! Well, to say it better, it's time for us to describe a new class: ./vendor/bin/phpspec describe
and, for the name, how about App/Factory/DinosaurFactory
.
./vendor/bin/phpspec describe App/Factory/DinosaurFactory
That creates one new file: DinosaurFactorySpec
. Let's go check it out! Like last time, we get one super basic example for free - asserting that $this
should be an instance of DinosaurFactory
. That's... kinda silly... but it's enough to force some code generation! Go run phpspec:
... lines 1 - 9 | |
class DinosaurFactorySpec extends ObjectBehavior | |
{ | |
function it_is_initializable() | |
{ | |
$this->shouldHaveType(DinosaurFactory::class); | |
} | |
} |
./vendor/bin/phpspec run
Why, yes! I would love for you to generate that class for us. Now, the spec passes.
Our first goal is to move the growVelociraptor()
method into DinosaurFactory
, but I want to follow the red, green, refactor cycle. So first, describe that functionality with a new example: function it_grows_a_large_velociraptor()
. Then, call the method: $dinosaur = $this->growVelociraptor(5)
.
... lines 1 - 16 | |
function it_grows_a_large_velociraptor() | |
{ | |
$dinosaur = $this->growVelociraptor(5); | |
... line 20 | |
} | |
... lines 22 - 23 |
Eventually, after coding all of this up, we know that the $dinosaur
variable should be a Dinosaur
object. But we also know that phpspec adds a lot of magic. Check this out: var_dump($dinosaur)
. Now, run phpspec:
... lines 1 - 16 | |
function it_grows_a_large_velociraptor() | |
{ | |
... line 19 | |
var_dump($dinosaur); | |
} | |
... lines 22 - 23 |
./vendor/bin/phpspec run
First, it notices that the growVelociraptor()
method is missing. Hit enter to generate that. Ok: scroll up to check out the dumped object. Cool! The $dinosaur
variable is actually a Subject
object! Right now, the underlying value is null
because the new growVelociraptor()
method doesn't return anything.
But more importantly, do you remember where we saw the Subject
object earlier? It was in DinosaurSpec
! When we call $this->getLength()
, that returns the length, but wrapped inside of a Subject
object. Why do we care? Because that was the magic layer that allowed us to call ->shouldReturn()
.
Inside DinosaurFactorySpec
, it's the same thing! growVelociraptor
will eventually return a Dinosaur
object, but phpspec wraps that inside a Subject
object. Thanks to that, we can call real methods on the Dinosaur
or matcher methods. In other words, the $dinosaur
in this class works pretty much exactly like the $this
variable in DinosaurSpec
. In fact, let's steal four lines of code from here. Paste these into the new example and change all of the $this
to $dinosaur
. Re-type the "r" in Dinosaur
and hit tab so PhpStorm adds its use
statement.
... lines 1 - 16 | |
function it_grows_a_large_velociraptor() | |
{ | |
... lines 19 - 20 | |
$dinosaur->shouldBeAnInstanceOf(Dinosaur::class); | |
$dinosaur->getGenus()->shouldBeString(); | |
$dinosaur->getGenus()->shouldBe('Velociraptor'); | |
$dinosaur->getLength()->shouldBe(5); | |
} | |
... lines 26 - 27 |
Ok! The growVelociraptor()
method is still empty, but let's see what phpspec thinks!
./vendor/bin/phpspec run
And the tests are red! Step 2: make this work with as little work as possible... or at least without over-engineering it. We can cheat: copy the code from the old growVelociraptor()
method. I'll keep this method here just as an example. Back in DinosaurFactory
, paste, change the new static
to new Dinosaur
, change the argument to int $length
and give this a Dinosaur
return type.
... lines 1 - 6 | |
class DinosaurFactory | |
{ | |
public function growVelociraptor(int $length): Dinosaur | |
{ | |
$dinosaur = new Dinosaur('Velociraptor', true); | |
$dinosaur->setLength($length); | |
return $dinosaur; | |
} | |
} |
Try it out:
./vendor/bin/phpspec run
Green! So now we get to step 3: refactor. This is our chance to remove duplication or improve things. For example, if I absolutely know that we will add other methods to this class - like growTyrannosaurus()
- it might make sense to refactor some logic into a new private
function called createDinosaur()
. Give this 3 arguments: string $genus
, bool $isCarnivorous
and int $length
. Copy the first two lines above and make each part dynamic.
... lines 1 - 13 | |
private function createDinosaur(string $genus, bool $isCarnivorous, int $length) | |
{ | |
$dinosaur = new Dinosaur($genus, $isCarnivorous); | |
$dinosaur->setLength($length); | |
} | |
... lines 19 - 20 |
Finally, the first method can be simplified to: return $this->createDinosaur()
, passing Velociraptor
, true
, and $length
. We could have wrote the code this way initially. But now we can refactor confidently because our tests will prove we didn't mess anything up:
... lines 1 - 8 | |
public function growVelociraptor(int $length): Dinosaur | |
{ | |
return $this->createDinosaur('Velociraptor', true, $length); | |
} | |
... lines 13 - 20 |
./vendor/bin/phpspec run
Oh. Except... I messed something up:
Return value of
DinosaurFactory::growVelociraptor()
must be an instance ofDinosaur
,null
returned.
Duh! I forgot my return
statement! And I should have added a return type too. Try it again:
... lines 1 - 13 | |
private function createDinosaur(string $genus, bool $isCarnivorous, int $length): Dinosaur | |
{ | |
... lines 16 - 18 | |
return $dinosaur; | |
} | |
... lines 21 - 22 |
./vendor/bin/phpspec run
Now we know it works. To be honest, I love the red, green, refactor cycle, but I also don't always do it. Heck, I don't even unit test all my code - only the parts that are complex enough to keep me up at night. But I do take one important lesson from it into everything I do: focus on accomplishing the behavior you need and nothing more. Keep things simple until they can't be. And when you get there, write a test first, then get crazy.
Next: we'll describe a new class that depends on another class. Is it finally time to talk about mocking in phpspec? Well... not so fast...
"Houston: no signs of life"
Start the conversation!