Buy

Coding, Adding Features, Refactoring

Ok, let's code! And remember: don't over-think things: just focus on getting each test to pass. Let's start with the second test, the default values.

Attacking Test #1

Inside DinosaurFactory, I'll paste a few default values: we'll use $codeName as the genus, because these are experimental dinosaurs, set the $length to be a small dinosaur, and create leaf-eating friends.

35 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
// defaults
$codeName = 'InG-' . random_int(1, 99999);
$length = random_int(1, Dinosaur::LARGE - 1);
$isCarnivorous = false;
... lines 20 - 23
}
... lines 25 - 33
}

Yep, with these values, our second test should be happy. Finish the function: $dinosaur = $this->createDinosaur() with $codeName, $isCarnivorous and $length. Then, return $dinosaur. Oh... and it doesn't really matter... but let's move this function up: I like to have my public functions above private ones.

35 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 20
$dinosaur = $this->createDinosaur($codeName, $isCarnivorous, $length);
return $dinosaur;
}
... lines 25 - 33
}

Ok, that should be enough to get one test to pass. Run 'em:

./vendor/bin/phpunit

Yes! Failure... dot... failure.

Attacking Test #2

Keep going! Let's work on the last test next: if the spec has the word large in it, it should be a large dinosaur. That's easy enough: inside the method: use stripos to check if the $specification contains large. Because if it does... we need a bigger length! Generate a random number between the LARGE constant and 100... which would be a horrifyingly big dinosaur.

39 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 20
if (stripos($specification, 'large') !== false) {
$length = random_int(Dinosaur::LARGE, 100);
}
... lines 24 - 27
}
... lines 29 - 37
}

And just like that, another test passes!

Attack Test #3

This is fun! It's like, every time I write a line of code, Sebastian Bergmann is personally giving me a high five!

Ok, the last test is one where the spec includes the word carnivorous. What's the quickest way to get this test to pass? Just copy the if statement, paste it, change the string to carnivorous and set isCarnivorous to true.

43 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 24
if (stripos($specification, 'carnivorous') !== false) {
$isCarnivorous = true;
}
... lines 28 - 31
}
... lines 33 - 41
}

And now... thanks to the power of TDD... they all pass! That felt great.

We Want HUGE Dinosaurs

And management already loves this feature. But... they don't think the dinosaurs are big enough. Now, they want to use the word "huge" to grow mouth-gaping dinosaurs! They've gone mad!

No problem! Thanks to the power of data providers, we can just add more test cases! Or... if you feel like this method is already doing enough, you can create another test. Let's do that: testItGrowsAHugeDinosaur() with only a $specification argument. Grow the dino with $dinosaur = $this->factory->growFromSpecification(). Then, check to make sure it's huge with $this->assertGreaterThanOrEqual().

94 lines tests/AppBundle/Factory/DinosaurFactoryTest.php
... lines 1 - 8
class DinosaurFactoryTest extends TestCase
{
... lines 11 - 75
public function testItGrowsAHugeDinosaur(string $specification)
{
$dinosaur = $this->factory->growFromSpecification($specification);
$this->assertGreaterThanOrEqual(Dinosaur::HUGE, $dinosaur->getLength());
}
... lines 82 - 92
}

Oh, but we need to define what huge means. Back in Dinosaur, add const HUGE = 30. And management decided to make the large dinosaurs a bit smaller - set LARGE to 10.

69 lines src/AppBundle/Entity/Dinosaur.php
... lines 1 - 10
class Dinosaur
{
const LARGE = 10;
const HUGE = 20;
... lines 15 - 67
}

Use the constant in the test and compare it with $dinosaur->getLength().

94 lines tests/AppBundle/Factory/DinosaurFactoryTest.php
... lines 1 - 8
class DinosaurFactoryTest extends TestCase
{
... lines 11 - 75
public function testItGrowsAHugeDinosaur(string $specification)
{
... lines 78 - 79
$this->assertGreaterThanOrEqual(Dinosaur::HUGE, $dinosaur->getLength());
}
... lines 82 - 92
}

Huge Data Provider

With the test function done, create the data provider: getHugeDinosaurSpecTests(). Just like before, make this return an array. Each individual test case will also be an array like last time, but now with only one item inside. Test for 'huge dinosaur, then also huge dino, just the word huge and, of course, OMG and... the scream Emoji!

94 lines tests/AppBundle/Factory/DinosaurFactoryTest.php
... lines 1 - 8
class DinosaurFactoryTest extends TestCase
{
... lines 11 - 82
public function getHugeDinosaurSpecTests()
{
return [
['huge dinosaur'],
['huge dino'],
['huge'],
['OMG'],
['😱'],
];
}
}

Back on the test method, connect it to the provider: @dataProvider getHugeDinosaurSpecTests.

94 lines tests/AppBundle/Factory/DinosaurFactoryTest.php
... lines 1 - 8
class DinosaurFactoryTest extends TestCase
{
... lines 11 - 72
/**
* @dataProvider getHugeDinosaurSpecTests
*/
public function testItGrowsAHugeDinosaur(string $specification)
... lines 77 - 92
}

Ok, let's watch some tests fail! Go Sebastian go!

./vendor/bin/phpunit

Beautiful failures! Five new test cases and five new failures. Time to code!

In DinosaurFactory, this method is going to start getting ugly... but I don't care! Remember, our main job is to get the tests to pass, not to write really fancy code. TDD helps keep us focused.

First, update the large if statement to make sure it creates large, but not HUGE dinosaurs. We could have updated our test first before making this change.

47 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 24
if (stripos($specification, 'large') !== false) {
$length = random_int(Dinosaur::LARGE, Dinosaur::HUGE - 1);
}
... lines 28 - 35
}
... lines 37 - 45
}

Now, let's handle the HUGE dinos. Copy the large if statement, change the search text to huge, and generate a length between HUGE and 100.

47 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 20
if (stripos($specification, 'huge') !== false) {
$length = random_int(Dinosaur::HUGE, 100);
}
... lines 24 - 35
}
... lines 37 - 45
}

Run the tests!

./vendor/bin/phpunit

Easy! 3 of the 5 already pass: just OMG and the screaming Emoji left! Copy the huge if statement and paste two more times. Use OMG on the first and the screaming Emoji for the second.

I know... there's so much duplication! It's so ugly. But... I don't care! I love it because the tests do pass!

Refactoring our Ugly Code

And that means we've reached step 3 of TDD: refactor! I don't actually love ugly code - it's just that it wasn't time to worry about it yet. TDD helps you focus on writing your business logic correctly first, and then on improving the code.

So let's makes this better. Actually, if you downloaded the course code, then you should have a tutorial/ directory with a DinosaurFactory.php file inside. Copy the private function from that file, find our DinosaurFactory, and paste at the bottom.

64 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 38
private 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);
}
}

This is still a bit complex, but it removes the duplication and makes the length calculation more systematic. Copy the method name, scroll up, delete all that ugly length logic... and just say $length = $this->getLengthFromSpecification($specification).

64 lines src/AppBundle/Factory/DinosaurFactory.php
... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 17
$length = $this->getLengthFromSpecification($specification);
... lines 19 - 27
}
... lines 29 - 62
}

My new code probably doesn't contain any bugs... but you should totally not trust me! I mess up all the time! Just run the tests.

./vendor/bin/phpunit

Ha! It works! And you doubted me....

Next! What if you need to test that a method throws an exception under certain conditions? Like... if you try to put a T-Rex in the same enclosure as a nice, friendly Brontosaurus. Let's find out!

Leave a comment!