Refactoring & Dependency Injection
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn DinosaurFactory
, we made a fancy private function getLengthFromSpecification
. It's a great function. So great, that now, I want to be able to use it from outside this class.
To do that... and of course... to help us get to mocking, let's refactor this method into its own class. Create a new Service
directory in AppBundle
. And then a new class called DinosaurLengthDeterminator
. That's a fun name.
Copy getLengthFromSpecification()
, remove it, and paste it here. Make the method public
and re-type Dinosaur
to get the use
statement.
// ... lines 1 - 4 | |
use AppBundle\Entity\Dinosaur; | |
// ... line 6 | |
class DinosaurLengthDeterminator | |
{ | |
public function getLengthFromSpecification(string $specification): int | |
{ | |
$availableLengths = [ | |
'huge' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'omg' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'?' => ['min' => Dinosaur::HUGE, 'max' => 100], | |
'large' => ['min' => Dinosaur::LARGE, 'max' => Dinosaur::HUGE - 1], | |
]; | |
$minLength = 1; | |
$maxLength = Dinosaur::LARGE - 1; | |
foreach (explode(' ', $specification) as $keyword) { | |
$keyword = strtolower($keyword); | |
if (array_key_exists($keyword, $availableLengths)) { | |
$minLength = $availableLengths[$keyword]['min']; | |
$maxLength = $availableLengths[$keyword]['max']; | |
break; | |
} | |
} | |
return random_int($minLength, $maxLength); | |
} | |
} |
We already have a bunch of tests in DinosaurFactoryTest
that make sure each specification string gives us the right length. In tests
, create that same Service
directory and a new DinosaurLengthDeterminatorTest
. We're going to migrate the existing length tests to this class.
Add public function testItReturnsCorrectLengthRange()
with $spec
, $minExpectedSize
and $maxExpectedSize
.
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 13 | |
public function testItReturnsCorrectLengthRange($spec, $minExpectedSize, $maxExpectedSize) | |
{ | |
// ... lines 16 - 20 | |
} | |
// ... lines 22 - 34 |
This test will be similar to the one in DinosaurFactoryTest
, but a bit simpler: it only needs to test the length. Create a new determinator. And then set $actualSize
to $determinator->getLengthFromSpecification($spec)
.
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 13 | |
public function testItReturnsCorrectLengthRange($spec, $minExpectedSize, $maxExpectedSize) | |
{ | |
$determinator = new DinosaurLengthDeterminator(); | |
$actualSize = $determinator->getLengthFromSpecification($spec); | |
// ... lines 18 - 20 | |
} | |
// ... lines 22 - 34 |
To make sure this is within the range, add $this->assertGreaterThanOrEqual()
. Oh wait! No auto-completion! Bah! Extend TestCase
!
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 34 |
Now add $this->assertGreaterThanOrEqual()
with $minExpectedSize
and $actualSize
. You need to read this... backwards: this asserts that $actualSize
is greater than or equal to $maxExpectedSize
.
Repeat that with $this->assertLessThanOrEqual()
and $maxExpectedSize, $actualSize
.
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 13 | |
public function testItReturnsCorrectLengthRange($spec, $minExpectedSize, $maxExpectedSize) | |
{ | |
// ... lines 16 - 18 | |
$this->assertGreaterThanOrEqual($minExpectedSize, $actualSize); | |
$this->assertLessThanOrEqual($maxExpectedSize, $actualSize); | |
} | |
// ... lines 22 - 34 |
Adding the Length Data Provider
The real work is done in the data provider. Add public function getSpecLengthTests()
. If you look at DinosaurFactoryTest
, the getSpecificationTests()
already has great examples. Copy those, go back to the new test, and paste. We need almost the same thing: just change the comments to specification, min length and max length.
Then, for a large dinosaur, it should be between Dinosaur::LARGE
and DINOSAUR::HUGE - 1
. For a small dinosaur, the range is 0 to Dinosaur::LARGE - 1
. Copy the large dino range and use that for the last one too,
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 22 | |
public function getSpecLengthTests() | |
{ | |
return [ | |
// specification, min length, max length | |
['large carnivorous dinosaur', Dinosaur::LARGE, Dinosaur::HUGE - 1], | |
'default response' => ['give me all the cookies!!!', 0, Dinosaur::LARGE - 1], | |
['large herbivore', Dinosaur::LARGE, Dinosaur::HUGE - 1], | |
// ... lines 30 - 34 | |
]; | |
} | |
} |
We can also move the huge dinosaur tests here. Copy them, move back, and paste! This time, the range should be Dinosaur::HUGE
to 100. Copy that and use it for all of them.
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
// ... lines 11 - 22 | |
public function getSpecLengthTests() | |
{ | |
return [ | |
// ... lines 26 - 29 | |
['huge dinosaur', Dinosaur::HUGE, 100], | |
['huge dino', Dinosaur::HUGE, 100], | |
['huge', Dinosaur::HUGE, 100], | |
['OMG', Dinosaur::HUGE, 100], | |
['?', Dinosaur::HUGE, 100], | |
]; | |
} | |
} |
And finally, hook this all up with @dataProvider getSpecLengthTests()
. I'll even fix my typo!
// ... lines 1 - 8 | |
class DinosaurLengthDeterminatorTest extends TestCase | |
{ | |
/** | |
* @dataProvider getSpecLengthTests | |
*/ | |
public function testItReturnsCorrectLengthRange($spec, $minExpectedSize, $maxExpectedSize) | |
{ | |
// ... lines 16 - 20 | |
} | |
// ... lines 22 - 34 |
Perfect! Because we deleted some code, the DinosaurFactory
is temporarily broken. So let's execute just this test:
./vendor/bin/phpunit tests/AppBundle/Service/DinosaurLengthDeterminatorTest.php
It passes!
Refactoring to use a Dependency
Time to fix the factory and get those dinosaurs growing again! The getLengthFromSpecification()
method is gone. To use the new determinator class, use dependency injection: add public function __construct()
with a DinosaurLengthDeterminator
argument. I'll press alt+enter and select "Initialize fields" as a shortcut to create the property and set it below.
// ... lines 1 - 7 | |
class DinosaurFactory | |
{ | |
private $lengthDeterminator; | |
public function __construct(DinosaurLengthDeterminator $lengthDeterminator) | |
{ | |
$this->lengthDeterminator = $lengthDeterminator; | |
} | |
// ... lines 16 - 45 | |
} |
Back in growFromSpecification()
, use $this->lengthDeterminator
to call the method.
// ... lines 1 - 7 | |
class DinosaurFactory | |
{ | |
// ... lines 10 - 21 | |
public function growFromSpecification(string $specification): Dinosaur | |
{ | |
// ... lines 24 - 25 | |
$length = $this->lengthDeterminator->getLengthFromSpecification($specification); | |
// ... lines 27 - 35 | |
} | |
// ... lines 37 - 45 | |
} |
And... that's it! We haven't run the tests yet, but this class now probably works again. You need to pass in a dependency, but as long as you do that, it's all basically the same.
Run all the tests:
./vendor/bin/phpunit
Woh! That is a lot of failures - all with the same message:
Too few arguments passed to
DinosaurFactory::__construct()
onDinosaurFactoryTest
line 18.
Scroll up to line 18. Of course: we're missing the constructor argument to the factory in our test.
For the second time, we have dependent objects. The object we're testing - DinosaurFactory
- is dependent on another object - DinosaurLengthDeterminator
. We're going to fix this with a mock.
Emojis are breaking the code display. See anything including the size specification function.