Red-Green-Refactor with TestLink
This tutorial walks you through the classic TDD cycle—Red, Green, Refactor—while integrating TestLink at each phase.
What You'll Build
We'll build a StringCalculator class that adds numbers from a string input. This is a classic TDD kata.
Prerequisites
- Completed Getting Started
- TestLink installed in your project
The TDD Cycle with Links
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌───────────┐ │
│ │ RED │ ───→ │ GREEN │ ───→ │ REFACTOR │ │
│ │ │ │ │ │ │ │
│ │ + test │ │ + code │ │ + cleanup │ │
│ │ + link │ │ + link │ │ ± links │ │
│ └─────────┘ └─────────┘ └───────────┘ │
│ ↑ │ │
│ └───────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Iteration 1: Empty String Returns Zero
Red Phase: Write a Failing Test
Create the test file and write your first test:
<?php
// tests/StringCalculatorTest.php
use App\StringCalculator;
test('returns zero for empty string', function () {
$calculator = new StringCalculator();
expect($calculator->add(''))->toBe(0);
})->linksAndCovers(StringCalculator::class.'::add');Run the test—it fails because StringCalculator doesn't exist:
./vendor/bin/pest
# Error: Class "App\StringCalculator" not foundGreen Phase: Make It Pass
Create the minimal production code:
<?php
// src/StringCalculator.php
namespace App;
use TestFlowLabs\TestingAttributes\TestedBy;
class StringCalculator
{
#[TestedBy('Tests\StringCalculatorTest', 'returns zero for empty string')]
public function add(string $numbers): int
{
return 0; // Minimal implementation
}
}Run the test—it passes:
./vendor/bin/pest
# ✓ returns zero for empty stringValidate the links:
./vendor/bin/testlink validate
# ✓ All links are valid!Refactor Phase
No refactoring needed yet. The code is minimal.
Iteration 2: Single Number Returns Itself
Red Phase
Add a new test:
test('returns the number for single number', function () {
$calculator = new StringCalculator();
expect($calculator->add('5'))->toBe(5);
})->linksAndCovers(StringCalculator::class.'::add');Run tests—the new one fails:
./vendor/bin/pest
# ✓ returns zero for empty string
# ✗ returns the number for single numberGreen Phase
Update the production code:
<?php
// src/StringCalculator.php
namespace App;
use TestFlowLabs\TestingAttributes\TestedBy;
class StringCalculator
{
#[TestedBy('Tests\StringCalculatorTest', 'returns zero for empty string')]
#[TestedBy('Tests\StringCalculatorTest', 'returns the number for single number')]
public function add(string $numbers): int
{
if ($numbers === '') {
return 0;
}
return (int) $numbers;
}
}Run tests—both pass:
./vendor/bin/pest
# ✓ returns zero for empty string
# ✓ returns the number for single numberRefactor Phase
No refactoring needed yet.
Iteration 3: Two Numbers
Red Phase
test('returns sum of two numbers', function () {
$calculator = new StringCalculator();
expect($calculator->add('1,2'))->toBe(3);
})->linksAndCovers(StringCalculator::class.'::add');Green Phase
<?php
// src/StringCalculator.php
namespace App;
use TestFlowLabs\TestingAttributes\TestedBy;
class StringCalculator
{
#[TestedBy('Tests\StringCalculatorTest', 'returns zero for empty string')]
#[TestedBy('Tests\StringCalculatorTest', 'returns the number for single number')]
#[TestedBy('Tests\StringCalculatorTest', 'returns sum of two numbers')]
public function add(string $numbers): int
{
if ($numbers === '') {
return 0;
}
$parts = explode(',', $numbers);
return array_sum(array_map('intval', $parts));
}
}Refactor Phase
The code now handles the general case. Previous implementations were stepping stones.
Iteration 4: Multiple Numbers
Red Phase
test('returns sum of multiple numbers', function () {
$calculator = new StringCalculator();
expect($calculator->add('1,2,3,4,5'))->toBe(15);
})->linksAndCovers(StringCalculator::class.'::add');Green Phase
The test passes immediately! Our implementation already handles this case.
./vendor/bin/pest
# ✓ returns zero for empty string
# ✓ returns the number for single number
# ✓ returns sum of two numbers
# ✓ returns sum of multiple numbersAdd the #[TestedBy] attribute anyway:
#[TestedBy('Tests\StringCalculatorTest', 'returns zero for empty string')]
#[TestedBy('Tests\StringCalculatorTest', 'returns the number for single number')]
#[TestedBy('Tests\StringCalculatorTest', 'returns sum of two numbers')]
#[TestedBy('Tests\StringCalculatorTest', 'returns sum of multiple numbers')]
public function add(string $numbers): intFinal Validation
Run a complete validation:
./vendor/bin/testlink validateValidation Report
─────────────────
Link Summary
PHPUnit attribute links: 4
Pest method chain links: 0
Total links: 4
TestedBy Summary
TestedBy attributes found: 4
Synchronized: 4
✓ All links are valid!View the Report
./vendor/bin/testlink reportCoverage Links Report
─────────────────────
App\StringCalculator
add()
→ Tests\StringCalculatorTest::returns zero for empty string
→ Tests\StringCalculatorTest::returns the number for single number
→ Tests\StringCalculatorTest::returns sum of two numbers
→ Tests\StringCalculatorTest::returns sum of multiple numbers
Summary
Methods with tests: 1
Total test links: 4Key Takeaways
- Add test-side links in Red phase - When writing the failing test
- Add production-side links in Green phase - When making the test pass
- Update links in Refactor phase - If you rename or restructure
- Validate frequently - Catch broken links early
What's Next?
- Placeholders - Use placeholders for faster iteration
- Complete Example - A more comprehensive TDD walkthrough