Sitemap

What is Red, Green, Refactor?

4 min readNov 20, 2024

--

Test-Driven Development (TDD) is a powerful methodology for building robust, maintainable software. At its core lies the Red, Green, Refactor cycle, which provides a structured approach to writing tests and code. We’ll explore how to implement this methodology.

What is Red, Green, Refactor?

  1. Red: Write a failing test that defines a desired behavior or requirement.
  2. Green: Write the minimal code needed to pass the test.
  3. Refactor: Clean up the code while ensuring the test still passes.

This iterative approach ensures the code is both functional and maintainable. However, there’s always room for refinement. Drawing inspiration from James Shore’s insightful article from November 29, 2005, we can enhance the previous steps as follows:

Think: Figure out what test will best move your code towards completion. (Take as much time as you need. This is the hardest step for beginners.)

Red: Write a very small amount of test code. Only a few lines… usually no more than five. Run the tests and watch the new test fail: the test bar should turn red. (This should only take about 30 seconds.)

Green: Write a very small amount of production code. Again, usually no more than five lines of code. Don’t worry about design purity or conceptual elegance. Sometimes you can just hardcode the answer. This is okay because you’ll be refactoring in a moment. Run the tests and watch them pass: the test bar will turn green. (This should only take about 30 seconds, too.)

Refactor: Now that your tests are passing, you can make changes without worrying about breaking anything. Pause for a moment. Take a deep breath if you need to. Then look at the code you’ve written, and ask yourself if you can improve it. Look for duplication and other “code smells.” If you see something that doesn’t look right, but you’re not sure how to fix it, that’s okay. Take a look at it again after you’ve gone through the cycle a few more times. (Take as much time as you need on this step.) After each little refactoring, run the tests and make sure they still pass.

Repeat: Do it again. You’ll repeat this cycle dozens of times in an hour. Typically, you’ll run through several cycles (three to five) very quickly, then find yourself slowing down and spending more time on refactoring. Than you’ll speed up again. 20–40 cycles in an hour is not unreasonable.

Let’s see how this works in practice.

Example 1: Building a String Reverser

Red: Writing a Failing Test

We want to write a method that reverses a string. First, we write a PHPUnit test.

// tests/StringReverserTest.php
use PHPUnit\Framework\TestCase;

class StringReverserTest extends TestCase
{
public function testReverseString(): void
{
$reverser = new StringReverser();
$this->assertEquals('olleh', $reverser->reverse('hello'));
}
}

Run the test:

PHP Fatal error:  Class 'StringReverser' not found

This is expected since we haven’t implemented the StringReverser class yet.

Green: Implementing Minimal Code

Create the class and the reverse method:

// src/StringReverser.php
class StringReverser
{
public function reverse(string $input): string
{
return strrev($input);
}
}

Run the test:

Get Yeremi Loli’s stories in your inbox

Join Medium for free to get updates from this writer.

OK (1 test, 1 assertion)

Refactor: Cleaning the Code

In this case, there’s minimal code to refactor, but we might decide to add type declarations or move the class into a namespace for better organization.

// src/Utility/StringReverser.php
namespace App\Utility;

class StringReverser
{
public function reverse(string $input): string
{
return strrev($input);
}
}

Update the test accordingly to import the namespace:

use App\Utility\StringReverser;

Example 2: Implementing a Caching Layer with the Repository Pattern

Scenario

You want to implement a repository to fetch data from an API and cache the results locally. The repository should:

  • Use a caching layer to minimize API calls.
  • Fallback to the API if the cache is unavailable.

Red: Writing a Failing Test

Start by writing a PHPUnit test for the UserRepository.

// tests/Repository/UserRepositoryTest.php

use PHPUnit\Framework\TestCase;
use App\Repository\UserRepository;
use Psr\SimpleCache\CacheInterface;

class UserRepositoryTest extends TestCase
{
public function testGetUserReturnsCachedData(): void
{
$cacheMock = $this->createMock(CacheInterface::class);
$cacheMock->expects($this->once())
->method('get')
->with('user_1')
->willReturn(['id' => 1, 'name' => 'John Doe']);
$apiMock = $this->createMock(ApiClient::class);
$apiMock->expects($this->never())
->method('fetchUser');

$repository = new UserRepository($cacheMock, $apiMock);
$this->assertEquals(['id' => 1, 'name' => 'John Doe'], $repository->getUser(1));
}
}

Run the test:

Class "UserRepository" not found

Green: Implementing Minimal Code

Implement a basic UserRepository class.

// src/Repository/UserRepository.php

namespace App\Repository;

use Psr\SimpleCache\CacheInterface;

class UserRepository
{
private CacheInterface $cache;

public function __construct(CacheInterface $cache, $apiClient)
{
$this->cache = $cache;
$this->apiClient = $apiClient;
}

public function getUser(int $id): array
{
return $this->cache->get("user_$id");
}
}

Re-run the test:

OK (1 test, 1 assertion)

Refactor: Expanding Functionality

Now, enhance the repository to fetch data from the API if not cached, and cache the result.

public function getUser(int $id): array
{
$key = "user_$id";
$user = $this->cache->get($key);

if ($user === null) {
$user = $this->apiClient->fetchUser($id);
$this->cache->set($key, $user, 3600);
}

return $user;
}

Update the test to cover the fallback mechanism:

public function testGetUserFetchesFromApiWhenCacheMisses(): void
{
$cacheMock = $this->createMock(CacheInterface::class);
$cacheMock->method('get')->willReturn(null);
$apiMock = $this->createMock(ApiClient::class);
$apiMock->expects($this->once())
->method('fetchUser')
->with(1)
->willReturn(['id' => 1, 'name' => 'John Doe']);

$repository = new UserRepository($cacheMock, $apiMock);
$this->assertEquals(['id' => 1, 'name' => 'John Doe'], $repository->getUser(1));
}

--

--

Yeremi Loli
Yeremi Loli

Written by Yeremi Loli

PHP OOP, Javascript, AWS, MySQL, Magento2, Wordpress, Git, Javascript, RedBeanPHP, Slim and Symfony.