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

TDD in Practice

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

Let's put TDD into practice!

I want to add a new getSpecification() method to the Dinosaur class that will return a string description - like:

Velociraptor carnivorous dinosaur is 5 meters long

TDD says: write that test first! Add public function testReturnsFullSpecificationOfDinosaur(). Create a new dinosaur, but don't set any data on it. Let's test the default string: Unknown non-carnivorous dinosaur is 0 meters long should equal $dinosaur->getSpecification().

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 29
public function testReturnsFullSpecificationOfDinosaur()
{
$dinosaur = new Dinosaur();
$this->assertSame(
'The Unknown non-carnivorous dinosaur is 0 meters long',
$dinosaur->getSpecification()
);
}
}

Test done! Step 2 is to write the minimum amount of code to get this test to pass. In Dinosaur, add public function getSpecification(). So... what's the smallest amount of code we can write? We can just return a hardcoded string!

... lines 1 - 10
class Dinosaur
{
... lines 13 - 27
public function getSpecification(): string
{
return 'The Unknown non-carnivorous dinosaur is 0 meters long';
}
}

Genius! Ok, try the test!

./vendor/bin/phpunit

Ha! It passes! The third step to TDD is refactor... which I don't think is needed in this case.

Wait, what? You don't like my hardcoded string? Looks like you're missing the last boat back to the mainland. Just kidding ... I know, returning a hardcoded string is silly... and I don't do this in real life. But it shows off an important thing with TDD: keep your code simple. Don't make it unnecessarily fancy or cover unnecessary use-cases. If you do have an edge-case that you want to cover, add the test first, and then code for it.

Adding another Case

Actually, let's do that now: add a new test method: testReturnsFullSpecificationForTyrannosaurus. I want each Dinosaur to have a genus - like Tyrannosaurus - and a flag that says whether or not it likes to eat people... I mean whether or not it's carnivorous. These will be used in getSpecification(). Create a new Dinosaur() and pass Tyrannosaurus, and true for carnivorous... because T-Rex's love to eat people.

Set its length to 12. This time, the specification should be:

Tyrannosaurus carnivorous dinosaur is 12 meters long

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 39
public function testReturnsFullSpecificationForTyrannosaurus()
{
$dinosaur = new Dinosaur('Tyrannosaurus', true);
$dinosaur->setLength(12);
$this->assertSame(
'The Tyrannosaurus carnivorous dinosaur is 12 meters long',
$dinosaur->getSpecification()
);
}
}

Finishing getSpecification()

Test done! Let's write some code! Start in Dinosaur: I'll add a __construct() method with a $genus = 'Unknown' argument and $isCarnivorous = false. Add these two properties to the class. I'll go to the Code menu and then to Generate - or press Command+N on a Mac - select "ORM Annotations" to add annotations above each method. We don't technically need those right now... but it'll save time later.

... lines 1 - 10
class Dinosaur
{
... lines 13 - 17
/**
* @var string
* @ORM\Column(type="string")
*/
private $genus;
/**
* @var bool
* @ORM\Column(type="boolean")
*/
private $isCarnivorous;
public function __construct(string $genus = 'Unknown', bool $isCarnivorous = false)
{
... lines 32 - 33
}
... lines 35 - 54
}

Down in the constructor, set both properties to their values. The default values for each argument match our first test.

... lines 1 - 10
class Dinosaur
{
... lines 13 - 29
public function __construct(string $genus = 'Unknown', bool $isCarnivorous = false)
{
$this->genus = $genus;
$this->isCarnivorous = $isCarnivorous;
}
... lines 35 - 54
}

In getSpecification(), we can't really fake things anymore. Return sprintf() and the original string, but replace the variable parts with %s, %s and %d.

Then pass $this->genus, $this->isCarnivorous to print carnivorous or non-carnivorous, and then $this->length.

... lines 1 - 10
class Dinosaur
{
... lines 13 - 45
public function getSpecification(): string
{
return sprintf(
'The %s %s dinosaur is %d meters long',
$this->genus,
$this->isCarnivorous ? 'carnivorous' : 'non-carnivorous',
$this->length
);
}
}

Perfect! Find your terminal and run the tests!

./vendor/bin/phpunit

Passing! Now to step 3... refactor! And this time... I will! Let's include the word carnivorous in the string. Then below, just print non- if needed. I don't even need to think about whether or not I made any mistakes. Just run the tests!

... lines 1 - 10
class Dinosaur
{
... lines 13 - 45
public function getSpecification(): string
{
return sprintf(
'The %s %scarnivorous dinosaur is %d meters long',
... line 50
$this->isCarnivorous ? '' : 'non-',
... lines 52 - 53
}
}
./vendor/bin/phpunit

I love it! Testing gives you confidence!

Next! Let's create a DinosaurFactory - because that sounds awesome.

Leave a comment!

4
Login or Register to join the conversation
Jose carlos C. Avatar
Jose carlos C. Avatar Jose carlos C. | posted 2 years ago

I don't understand TDD, really what it does is test, the only difference I see, is you finally have to think about refactoring. But from that I don't see any other difference. Because the first steps are the ones always used, use less code to complete the test. I guess I need more example to compare. You can help me understand better.

Reply

Hey Jose carlos C.

TDD is a technique to develop software. It helps with refactoring because it covers your back. After a refactoring you can run your tests and see if nothing broke. The essence of TDD is to be able to guarantee the behavior of your code, and to develop code that's easier to maintain, change and test.
I strongly recommend you to practice TDD all the time you can, the more you practice the more you will convince yourself about the benefits of it

Cheers!

Reply
Sumeet G. Avatar
Sumeet G. Avatar Sumeet G. | posted 2 years ago

I am getting this error:
ArgumentCountError: Too few arguments to function App\Entity\Dinosaur::__construct(), 0 passed in /Users/sumeet.gupta/Desktop/Symfony/my_project_name/tests/Entity/DinosaurTest.php on line 20 and exactly 2 expected

Reply

Hey Sumeet,

Looks like you forgot to pass a few required arguments to the Dinosaur entity when create it. Please, check your DinosaurTest.php on line 20.

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