Greeter TDD kata

Test Driven Development (TDD) is a skill that needs practice. While we can practice and improve our skills in our work projects, a kata can be useful improve a specific skill. Today we will focus on the Greeter kata. In the Greeter kata we will build an application that greets the user based on the time of the day: good evening, good night, good morning, etc. The goal of this kata is to practice delegation and refactoring. To make our tests repeatable we need to make them independent of time. We can do that by delegating the handling of time to a different object that we can control from the test.

The rules of the greeter kata are:

  1. Write a Greeter class with a greet method that receives a name and returns Hello <name>.
  2. You aren't allowed to change the method signature during the kata
  3. Greet removes spaces at the begin and end of a name
  4. Greet capitalises the name
  5. Greet returns Good morning <name> between 06.00 and 12.00
  6. Greet returns Good evening <name> between 18.00 and 22.00
  7. Greet returns Good night <name> between 22.00 and 06.00

I would recommend to try the kata for yourself before you continue reading. In the rest of the article I will give a step by step guide how to walk through this kata. If you don't have any experience with TDD or katas, I would recommend to read my introduction article: Test Driven Development: Red, Green, Refactor.

Red, Green, Refactor

We will use the red, green, refactor technique for this kata. First we write a failing test: red. We write the simplest production code to bring our tests to green. We refactor if necessary and we repeat the cycle. We continue until we have all rules of the kata implemented.

The first test

The kata already gives us an order in which we can implement the functionality. The base case, which needs the least code is checking if the greet method returns Hello <name>.

class GreeterTest extends TestCase
{
    /** @test */
    public function it_returns_hello_name()
    {
        $greeter = new Greeter();
        $this->assertEquals(
            'Hello Mark',
            $greeter->greet('Mark')
        );
    }
}

This test fails:

Error : Class "Webdevils\Katas\Greeter\Greeter" not found

Let's create the Greeter class

class Greeter
{

}

And run the test again.

Error : Call to undefined method Webdevils\Katas\Greeter\Greeter::greet()

We need to add the greet method.

public function greet(string $name) : string
{
    return '';
}

To make sure the test fails I returned an empty string from the greet method. This leads to our first test-error instead of coding error:

Failed asserting that two strings are equal.
Expected :'Hello Mark'
Actual   :''

Now we can make our first implementation:

public function greet(string $name) : string
{
    return 'Hello Mark';
}

The code is stupid, but we are green. Do we need to refactor? Nothing much to refactor right now, but we need to make sure the code will become a bit smarter. Let's add a second assertion to our test:

/** @test */
public function it_returns_hello_name()
{
    $greeter = new Greeter();

    $this->assertEquals(
        'Hello Mark',
        $greeter->greet('Mark')
    );

    $this->assertEquals(
        'Hello John',
        $greeter->greet('John')
    );
}

And we are red again:

Failed asserting that two strings are equal.
Expected :'Hello John'
Actual   :'Hello Mark'

The easiest way to go back to green is by using the $name parameter. Exactly as we want to:

public function greet(string $name) : string
{
    return 'Hello ' . $name;
}

Nothing to refactor for now.

Handling weird input names

Let's continue with our next test. We want to trim the name of spaces:

/** @test */
public function it_trims_the_name()
{
    $greeter = new Greeter();

    $this->assertEquals(
        'Hello Mark',
        $greeter->greet('   Mark    ')
    );
}

We are back to red:

Failed asserting that two strings are equal.
Expected :'Hello Mark'
Actual   :'Hello    Mark    '

PHP has a trim function that does exactly what we want. Let's use the trim function in our code to go back to green:

public function greet(string $name) : string
{
    return 'Hello ' . trim($name);
}

Nothing to refactor in our code under test. Our tests start to feel a bit repetitive though. We are doing similar assertions and I don't feel like writing them multiple times. We can get rid of the duplicate assertion by introducing a dataProvider:

public function simple_tests() : array
{
    return [
        ['Mark', 'Hello Mark'],
        ['John', 'Hello John'],
        ['   Mark   ', 'Hello Mark'],
    ];
}

/**
 * @test
 * @dataProvider simple_tests
 */
public function it_returns_hello_name($name, $expected)
{
    $greeter = new Greeter();

    $this->assertEquals(
        $expected,
        $greeter->greet($name)
    );
}

Nice! On to our next failing test. We want to capitalise the name:

public function simple_tests() : array
{
    return [
        ['Mark', 'Hello Mark'],
        ['John', 'Hello John'],
        ['   Mark   ', 'Hello Mark'],
        ['mark', 'Hello Mark'],
    ];
}

The error to proof that we have a failing test:

Failed asserting that two strings are equal.
Expected :'Hello Mark'
Actual   :'Hello mark'

PHP has a ucfirst function that capitalises the first letter of a string. The ucfirst function will help us go back to green:

public function greet(string $name) : string
{
    return 'Hello ' . ucfirst(trim($name));
}

Anything to refactor? Maybe. The greet method starts to look a bit cluttered. Currently it is still alright and readable, but for a new reader of our code it can be unclear what we are trying to do. To make our intention more clear we can extract the preparation of the $name to its own method:

public function greet(string $name) : string
{
    return 'Hello ' . $this->prepareName($name);
}

private function prepareName(string $name): string
{
    return ucfirst(
        trim(
            $name
        )
    );
}

Good morning

Our next failing test is a bit more complicated. We want to test that the greet method will return good morning in the morning. Our test needs to be repeatable and work at any time of the day, so we cannot rely on the current time in our method. We need a way to provide the current time to our greet method. However, we aren't allowed to change the method signature.

The greet method is part of a Greeter class. Which means we can also use another method or the constructor to provide the current time. It means that we need to change our current tests to provide a time between 12.00 and 15.00 and for our good morning greeting a time between 06.00 and 12.00.

/**
 * @test
 * @dataProvider simple_tests
 */
public function it_returns_hello_name_during_the_day($name, $expected)
{
    $greeter = new Greeter(
        new \DateTimeImmutable('T12:00')
    );

    $this->assertEquals(
        $expected,
        $greeter->greet($name)
    );
}

/** @test */
public function it_returns_good_morning_in_the_morning()
{
    $greeter = new Greeter(
        new \DateTimeImmutable('T06:00')
    );

    $this->assertEquals(
        'Good morning Mark',
        $greeter->greet('Mark')
    );
}

We are red:

Failed asserting that two strings are equal.
Expected :'Good morning Mark'
Actual   :'Hello Mark'

We can go back to green by storing the current time in a class attribute and adding a conditional to our greet method:

private \DateTimeImmutable $currentTime;

public function __construct(\DateTimeImmutable $currentTime)
{
    $this->currentTime = $currentTime;
}

public function greet(string $name) : string
{
    if ($this->currentTime < (new \DateTimeImmutable('T12:00'))) {
        return 'Good morning ' . $this->prepareName($name);
    }

    return 'Hello ' . $this->prepareName($name);
}

Green! Time to refactor. The code under test can be improved again. If you read the condition you can figure out what we try to achieve, but the intention isn't immediately clear. We can solve that by extracting the condition to its own isMorning() method:

public function greet(string $name) : string
{
    if ($this->isMorning()) {
        return 'Good morning ' . $this->prepareName($name);
    }

    return 'Hello ' . $this->prepareName($name);
}

private function isMorning(): bool
{
    return $this->currentTime < (new \DateTimeImmutable('T12:00'));
}

We are also repeating ourselves in the greet method. We prepare the name on two different places. We can reduce duplication by introducing a $greeting variable for the greeting:

public function greet(string $name) : string
{
    $greeting = 'Hello';

    if ($this->isMorning()) {
        $greeting = 'Good morning';
    }

    return $greeting . ' ' . $this->prepareName($name);
}

We could even extract the creation of the greeting completely to its own method:

public function greet(string $name) : string
{
    return $this->createGreeting() . ' ' . $this->prepareName($name);
}

private function createGreeting(): string
{
    if ($this->isMorning()) {
        return 'Good morning';
    }
    
    return 'Hello';
}

This makes the greet method very easy to understand. To greet someone we create a greeting and add a prepared name after it. The greeting is Good morning in the morning or just Hello on other times of the day. The name always has a capital letter and a is trimmed of spaces.

Good evening

The next failing test is about greeting in the evening. When the it is 18.00 it is evening:

/** @test */
public function it_returns_good_evening_in_the_evening() 
{
    $greeter = new Greeter(
        new \DateTimeImmutable('T18:00')
    );
    
    $this->assertEquals(
        'Good evening Mark',
        $greeter->greet('Mark')
    );
}

Again, our test fails.

Failed asserting that two strings are equal.
Expected :'Good evening Mark'
Actual   :'Hello Mark'

To make the test pass, we add a condition if the time is 18.00 or later.

private function createGreeting(): string
{
    if($this->currentTime >= (new \DateTimeImmutable('T18:00'))) {
        return 'Good evening';
    }
    if ($this->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

We are green again. And we can refactor our code again. This time we extract the condition for evening to its own method.

private function createGreeting(): string
{
    if($this->isEvening()) {
        return 'Good evening';
    }
    if ($this->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

private function isEvening(): bool
{
    return $this->currentTime >= (new \DateTimeImmutable('T18:00'));
}

Looks good, let's continue with the next failing test.

Good night

And we have our last business rule. We want to greet someone good night during the night. Our first step is to add a test for a greeting after 22:00 in the night.

/** @test */
public function it_returns_good_night_in_the_night()
{
    $greeter = new Greeter(
        new \DateTimeImmutable('T22:00')
    );

    $this->assertEquals(
        'Good night Mark',
        $greeter->greet('Mark')
    );
}

We go to red.

Failed asserting that two strings are equal.
Expected :'Good night Mark'
Actual   :'Good evening Mark'

We can add a condition to check if the current time is after 22:00 in the night.

private function createGreeting(): string
{
    if($this->currentTime >= (new \DateTimeImmutable('T22:00'))) {
        return 'Good night';
    }

    if($this->isEvening()) {
        return 'Good evening';
    }

    if ($this->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

As we are green, we can refactor again. Same as previous two times, we can extract the condition to an isNight method.

private function createGreeting(): string
{
    if($this->isNight()) {
        return 'Good night';
    }

    if($this->isEvening()) {
        return 'Good evening';
    }

    if ($this->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

private function isNight(): bool
{
    return $this->currentTime >= (new \DateTimeImmutable('T22:00'));
}

Another refactoring we can do is change all if-statements in the createGreeting method to elseif statements. This clearly communicates to the reader that only one of the conditions needs to be true.

private function createGreeting(): string
{
    if($this->isNight()) {
        return 'Good night';
    } elseif($this->isEvening()) {
        return 'Good evening';
    } elseif ($this->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

We are still green, which means we refactored successfully. We are almost done, only one failing test to write. We stated that it's night until 06:00 in the morning, but if we greet at 05:59 in the morning the code will greet us with good morning. To prove this bug we can add another failing assertion to our good night test:

/** @test */
public function it_returns_good_night_in_the_night()
{
    $greeter = new Greeter(
        new \DateTimeImmutable('T22:00')
    );

    $this->assertEquals(
        'Good night Mark',
        $greeter->greet('Mark')
    );

    $greeter = new Greeter(
        new \DateTimeImmutable('T05:59')
    );

    $this->assertEquals(
        'Good night Mark',
        $greeter->greet('Mark')
    );
}

As expected our test fails because we greet with good morning instead of good night:

Failed asserting that two strings are equal.
Expected :'Good night Mark'
Actual   :'Good morning Mark'

The greater than (>) operator treats 01:00 as smaller than 23:00, because we are using a DateTimeImmutable. The DateTimeImmutable stores the time + the date. Even though we are only using the time part of it currently. We can take care of this by adding another condition to our isNight() method to check for smaller than 06:00 in the morning:

private function isNight(): bool
{
    return $this->currentTime >= (new \DateTimeImmutable('T22:00'))
        || $this->currentTime < (new \DateTimeImmutable('T06.00'));
}

We are green again, time for refactoring. Our code actually looks pretty good so we could keep it like this, which would mean that we finished our kata for today.

Some fun with refactoring

However, we can have some fun with refactoring. As we have a working test-suite we can be quite confident that we don't break anything. Which gives us the opportunity to go as crazy as we want.

For now, I want to run through one refactoring to proof the power of TDD.

If you look at our current Greeter class, you see it has two responsibilities:

  1. Greeting the user
  2. Determining which time of day it is

For our code and this kata that is perfectly fine, but just for fun, let's take the steps to extract the time code to a separate class.

First we introduce a TimeOfDay class:

class TimeOfDay
{
    private \DateTimeImmutable $currentTime;

    public function __construct(\DateTimeImmutable $currentTime)
    {
        $this->currentTime = $currentTime;
    }
}

The TimeOfDay takes the currentTime as a parameter. Next we make sure our Greeter class will construct a TimeOfDay object in its constructor:

private TimeOfDay $timeOfDay;

public function __construct(\DateTimeImmutable $currentTime)
{
    $this->currentTime = $currentTime;
    $this->timeOfDay = new TimeOfDay($currentTime);
}

Then we can move the isMorning, isEvening and isNight methods to our new class:

private function createGreeting(): string
{
    if($this->timeOfDay->isNight()) {
        return 'Good night';
    } elseif($this->timeOfDay->isEvening()) {
        return 'Good evening';
    } elseif ($this->timeOfDay->isMorning()) {
        return 'Good morning';
    }

    return 'Hello';
}

And the TimeOfDay class:

class TimeOfDay
{
    private \DateTimeImmutable $currentTime;

    public function __construct(\DateTimeImmutable $currentTime)
    {
        $this->currentTime = $currentTime;
    }

    public function isNight(): bool
    {
        return $this->currentTime >= (new \DateTimeImmutable('T22:00'))
            || $this->currentTime < (new \DateTimeImmutable('T06.00'));
    }

    public function isEvening(): bool
    {
        return $this->currentTime >= (new \DateTimeImmutable('T18:00'));
    }

    public function isMorning(): bool
    {
        return $this->currentTime < (new \DateTimeImmutable('T12:00'));
    }
}

Finally we can remove every mention of $currentTime in our Greeter class.

Note that after every step above we could run the tests and would still be green. Even though the structure of our code is completely different, we still have working code and a test suite to make sure that our code actually works.

Of course, some structural changes will have an effect on our test code. Another refactoring we could make is to change the signature of the Greeter class. Instead of requiring a DateTimeImmutable, we want the user of the class to give us a TimeOfDay object.

private TimeOfDay $timeOfDay;

public function __construct(TimeOfDay $timeOfDay)
{
    $this->timeOfDay = $timeOfDay;
}

Our tests will fail, but we can easily make this change to our tests. As this is only a small change, we can make sure that the tests still make sense. And after changing the test to work with this new constructor, we have the tests again to check if everything else still works:

/**
 * @test
 * @dataProvider simple_tests
 */
public function it_returns_hello_name_during_the_day($name, $expected)
{
    $greeter = new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable('T12:00')
        )
    );

    $this->assertEquals(
        $expected,
        $greeter->greet($name)
    );
}

/** @test */
public function it_returns_good_morning_in_the_morning()
{
    $greeter = new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable('T06:00')
        )
    );

    $this->assertEquals(
        'Good morning Mark',
        $greeter->greet('Mark')
    );
}

/** @test */
public function it_returns_good_evening_in_the_evening()
{
    $greeter = new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable('T18:00')
        )
    );

    $this->assertEquals(
        'Good evening Mark',
        $greeter->greet('Mark')
    );
}

/** @test */
public function it_returns_good_night_in_the_night()
{
    $greeter = new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable('T22:00')
        )
    );

    $this->assertEquals(
        'Good night Mark',
        $greeter->greet('Mark')
    );

    $greeter = new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable('T05:59')
        )
    );

    $this->assertEquals(
        'Good night Mark',
        $greeter->greet('Mark')
    );
}

While making this change, I realised that we construct a new Greeter objectseveral times in our test. When we changed the constructor, we had to change the test at all those places. We can refactor our test by introducing a factory method that takes care of constructing a new Greet object.

private function createGreeter(string $currentTime): Greeter
{
    return new Greeter(
        new TimeOfDay(
            new \DateTimeImmutable($currentTime)
        )
    );
}

This allows us to make another change to the constructor of our TimeOfDay class without having to make multiple changes to our tests. We use a DateTimeImmutable to keep track of time, but that is an irrelevant detail for the user of this class. We can encapsulate that in the TimeOfDay class, by just requesting a string time and create the DateTimeImmutable internally:

public function __construct(string $time)
{
    $this->currentTime = new \DateTimeImmutable($time);
}

To make this work with our tests, we only need one change in the createGreeter method of our tests:

private function createGreeter(string $currentTime): Greeter
{
    return new Greeter(
        new TimeOfDay(
            $currentTime
        )
    );
}

The code we have now is completely different from what we had before refactoring, but we didn't change any functionality. We have the test suite to proof that. That is the fun part of TDD. We could continue even further refactoring if we want, but for me it is enough. Feel free to try other changes to the code and see how it interacts with the tests and where you end up with the production code.

Source code

For anyone that followed along or is interested in the source code, I added it to the Kata Github project that I created for Webdevils. You can see the code of all Katas we discuss on this website, including the Greeter kata we just performed.

Author

Mark Kazemier's avatar
Mark Kazemier

Hi, my name is Mark. I'm the founder of webdevils.nl and love developing websites and other web applications. Through Webdevils.nl I want to spread my enthousiasm about the web and PHP. In my professional live I'm a security expert specialised in security monitoring.

View all posts