Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Coding, Adding Features, Refactoring

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

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

Login Subscribe

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.

... 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.

... 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.

... 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.

... 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().

... 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 - 90

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.

... lines 1 - 10
class Dinosaur
{
const LARGE = 10;
const HUGE = 30;
... lines 15 - 67
}

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

... 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 - 90

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!

... 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.

... lines 1 - 8
class DinosaurFactoryTest extends TestCase
{
... lines 11 - 72
/**
* @dataProvider getHugeDinosaurSpecTests
*/
public function testItGrowsAHugeDinosaur(string $specification)
... lines 77 - 90

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.

... 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.

... 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.

... lines 1 - 6
class DinosaurFactory
{
... lines 9 - 13
public function growFromSpecification(string $specification): Dinosaur
{
... lines 16 - 24
if (stripos($specification, 'OMG') !== false) {
$length = random_int(Dinosaur::HUGE, 100);
}
... line 28
if (strpos($specification, '

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.

... 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).

... 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 - 44

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!

9
Login or Register to join the conversation
Yard Avatar

Declaring the dinosaur carnivorous if "carnivorous" is part of the string, doesn't really work. The dataProvider test would also succeed if the specification was "non-carnivorous".

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Yard | posted 1 month ago

Howdy,

Good Catch! Yes, I do believe you're correct... It probably would have been better if we did something like:

if (str_starts_with(strtolower($specification), 'carnivorous') {
    $isCarnivorous = true;
}

And then also added another specification to the getSpecificationTests() data provider to ensure that a non-carnivorous specification was false when calling $dinosaur->isCarnivorous()

The interesting thing about testing, is this scenario is actually called an "edge case" and it happens all the time... If I ran into this situation in a real project, I would ask myself:

1) How likely is it that we will run into this scenario?
2) If we do run into this scenario, will it cause any undesired side effects?

Depending on those answer's, I would either write a test and make sure it passes, or let sleeping dogs lie and move onto bigger problems...

By the way, did you know we're releasing a new version of this tutorial? Feel free to check it out https://symfonycasts.com/screencast/phpunit and let us know what you think.

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 1 year ago

Hi there,

I 😍 this tutorial about TDD.

Thanks for bring it us!

Reply

You're welcome!

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 1 year ago

Hi there,
This code looks like a bit outdated, maybe? If so, will you plan to update it with SF5?

Best regards.

Reply

Hey there,

could you tell me what part of the chapter looks outdated? I don't think we will redo this tutorial but like to keep it up to date by adding notes or updating only a relevant part of the code. Anyways, all the concepts taught here are still relevant

Cheers!

Reply
Abelardo Avatar

It's not related to this tutorial but the Symfony version which is being used at the tutorial.
I think Symfony4/5 brings us more utilities for testing.

Cheers!

Reply

The code block after "and... the scream Emoji!" is broken (maybe because of this Emoji).
Idem for the block after "the screaming Emoji for the second." and the one after "and paste at the bottom.".
Etc...

It seems it is the same issue for all blocks with the Emoji.

Reply

Hey Capucine,

Wooops, our bad! I just updated those code blocks - now them have emoji! :)

Thank you for reporting this! If you noticed any missing emoji further in the course - please, let us know and we will be happy to fix them as well.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// 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
    }
}