Placeholder Strategy
Understanding the placeholder system: why it exists, how it works, and when to use it.
What Are Placeholders?
Placeholders are temporary markers that stand in for real class::method references:
// Instead of this (requires knowing the class name upfront)
->linksAndCovers(UserService::class.'::create')
// You can use this (decide the name later)
->linksAndCovers('@user-create')Placeholders:
- Start with
@followed by a letter - Can contain letters, numbers, hyphens, underscores
- Are resolved later with
testlink pair
Why Placeholders Exist
The TDD Timing Problem
In Test-Driven Development, you write the test first:
// Step 1: Write failing test
test('creates user with valid data', function () {
$service = new UserService();
$user = $service->create(['name' => 'John']);
expect($user)->toBeInstanceOf(User::class);
});Now you want to add the link... but to what?
// The class doesn't exist yet!
->linksAndCovers(UserService::class.'::create') // Error: Class not foundYou're stuck:
- Can't link without the class
- Can't create the class without breaking TDD (writing test first)
Placeholders Solve This
// Step 1: Write test with placeholder
test('creates user with valid data', function () {
// ...
})->linksAndCovers('@user-create'); // Works! No class needed yet
// Step 2: Write production code with same placeholder
#[TestedBy('@user-create')]
public function create(array $data): User
{
// ...
}
// Step 3: After both exist, resolve placeholders
./vendor/bin/testlink pairHow Placeholders Work
1. Marking Phase
Use the same placeholder in test and production:
// tests/UserServiceTest.php
test('creates user', function () {
// ...
})->linksAndCovers('@A');
// src/UserService.php
#[TestedBy('@A')]
public function create(): User2. Scanning Phase
testlink pair scans for all placeholders:
Found placeholders:
@A
Tests:
- tests/UserServiceTest.php :: "creates user"
Production:
- src/UserService.php :: create()3. Resolution Phase
Placeholders are replaced with real references:
// tests/UserServiceTest.php - AFTER
test('creates user', function () {
// ...
})->linksAndCovers(UserService::class.'::create');
// src/UserService.php - AFTER
#[TestedBy(UserServiceTest::class, 'creates user')]
public function create(): UserPlaceholder Naming
Simple Placeholders
For quick, temporary markers:
->linksAndCovers('@A')
->linksAndCovers('@B')
->linksAndCovers('@C')Good for:
- Single feature development
- Short-lived placeholders
- When you'll resolve immediately
Descriptive Placeholders
For longer-lived or team workflows:
->linksAndCovers('@user-create')
->linksAndCovers('@order-process')
->linksAndCovers('@payment-refund')Good for:
- Multiple developers
- Placeholders that live for a while
- Self-documenting code
Valid Placeholder Patterns
| Placeholder | Valid | Notes |
|---|---|---|
@A | ✓ | Minimal |
@user | ✓ | Lowercase |
@UserCreate | ✓ | PascalCase |
@user-create | ✓ | Kebab-case |
@user_create | ✓ | Snake_case |
@user123 | ✓ | With numbers |
@123 | ✗ | Must start with letter after @ |
@-test | ✗ | Must start with letter after @ |
user | ✗ | Missing @ |
N:M Placeholder Matching
One placeholder can match multiple tests AND multiple methods:
// Multiple tests with same placeholder
test('creates user with name', function () {
// ...
})->linksAndCovers('@user-create');
test('creates user with email', function () {
// ...
})->linksAndCovers('@user-create');
// Multiple methods with same placeholder
#[TestedBy('@user-create')]
public function create(): User
#[TestedBy('@user-create')]
public function createWithRole(): UserAfter testlink pair, ALL tests link to ALL methods:
test('creates user with name', function () {
// ...
})
->linksAndCovers(UserService::class.'::create')
->linksAndCovers(UserService::class.'::createWithRole');
test('creates user with email', function () {
// ...
})
->linksAndCovers(UserService::class.'::create')
->linksAndCovers(UserService::class.'::createWithRole');This is intentional for cases where multiple tests cover multiple related methods.
When to Use Placeholders
Ideal Scenarios
TDD Development
// RED: Write failing test
test('calculates discount', function () {
// ...
})->linksAndCovers('@discount-calc');
// GREEN: Implement
#[TestedBy('@discount-calc')]
public function calculate(): float
// REFACTOR + PAIR: Clean up and resolve
./vendor/bin/testlink pairBDD Development
// Acceptance test placeholder
test('user sees discount on cart', function () {
// ...
})->linksAndCovers('@cart-discount');
// Multiple implementations use same placeholder
#[TestedBy('@cart-discount')]
public function calculateDiscount(): float
#[TestedBy('@cart-discount')]
public function applyDiscount(): voidPrototyping
// Quick iteration, resolve later
->linksAndCovers('@A')
->linksAndCovers('@B')
->linksAndCovers('@C')When NOT to Use Placeholders
Existing Code
// Class already exists - use real reference
->linksAndCovers(ExistingService::class.'::method')Stable Codebase
// No TDD workflow - use direct links
#[TestedBy(UserServiceTest::class, 'test_creates_user')]Team Without Convention
// If team isn't using placeholders, don't introduce
// Use testlink sync insteadPlaceholder Lifecycle
┌─────────────────┐
│ Development │
│ Start │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Write Test │ test('...')->linksAndCovers('@A')
│ with @ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Write Prod │ #[TestedBy('@A')]
│ with @ │ public function method()
└────────┬────────┘
│
▼
┌─────────────────┐
│ Tests Pass │ Green phase complete
│ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ testlink pair │ Resolve @ → real references
│ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Commit │ No placeholders in repo
│ │
└─────────────────┘Pair Command Options
Preview Changes
./vendor/bin/testlink pair --dry-runShows what would change without modifying files.
Specific Placeholder
./vendor/bin/testlink pair --placeholder=@user-createOnly resolve one placeholder.
Path Filtering
./vendor/bin/testlink pair --path=src/ServicesOnly resolve in specific directory.
CI Integration
Ensure no placeholders in committed code:
# .github/workflows/ci.yml
- run: ./vendor/bin/testlink pair
# Returns exit code 1 if unresolved placeholders existOr use validate:
- run: ./vendor/bin/testlink validate
# Fails if placeholders remainComparison with Sync
| Feature | Placeholder (pair) | Sync |
|---|---|---|
| Use case | TDD/BDD workflow | Existing code |
| Requires markers | Yes (@placeholder) | No |
| Direction | Both simultaneously | Production → Test or Test → Production |
| When | During development | After development |
Use Placeholders When
- Following TDD/BDD
- Want to defer class naming
- Building test + production together
Use Sync When
- Adding links to existing code
- One-way synchronization
- No placeholder markers exist
Summary
Placeholders enable true test-first development by letting you:
- Write test code before production code exists
- Link them with temporary markers
- Resolve to real references when ready
They're a bridge between "test first" methodology and TestLink's requirement for real class references.