Skip to content

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:

php
// 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:

php
// 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?

php
// The class doesn't exist yet!
->linksAndCovers(UserService::class.'::create')  // Error: Class not found

You're stuck:

  • Can't link without the class
  • Can't create the class without breaking TDD (writing test first)

Placeholders Solve This

php
// 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 pair

How Placeholders Work

1. Marking Phase

Use the same placeholder in test and production:

php
// tests/UserServiceTest.php
test('creates user', function () {
    // ...
})->linksAndCovers('@A');

// src/UserService.php
#[TestedBy('@A')]
public function create(): User

2. 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:

php
// tests/UserServiceTest.php - AFTER
test('creates user', function () {
    // ...
})->linksAndCovers(UserService::class.'::create');

// src/UserService.php - AFTER
#[TestedBy(UserServiceTest::class, 'creates user')]
public function create(): User

Placeholder Naming

Simple Placeholders

For quick, temporary markers:

php
->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:

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

PlaceholderValidNotes
@AMinimal
@userLowercase
@UserCreatePascalCase
@user-createKebab-case
@user_createSnake_case
@user123With numbers
@123Must start with letter after @
@-testMust start with letter after @
userMissing @

N:M Placeholder Matching

One placeholder can match multiple tests AND multiple methods:

php
// 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(): User

After testlink pair, ALL tests link to ALL methods:

php
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

php
// 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 pair

BDD Development

php
// 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(): void

Prototyping

php
// Quick iteration, resolve later
->linksAndCovers('@A')
->linksAndCovers('@B')
->linksAndCovers('@C')

When NOT to Use Placeholders

Existing Code

php
// Class already exists - use real reference
->linksAndCovers(ExistingService::class.'::method')

Stable Codebase

php
// No TDD workflow - use direct links
#[TestedBy(UserServiceTest::class, 'test_creates_user')]

Team Without Convention

php
// If team isn't using placeholders, don't introduce
// Use testlink sync instead

Placeholder 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

bash
./vendor/bin/testlink pair --dry-run

Shows what would change without modifying files.

Specific Placeholder

bash
./vendor/bin/testlink pair --placeholder=@user-create

Only resolve one placeholder.

Path Filtering

bash
./vendor/bin/testlink pair --path=src/Services

Only resolve in specific directory.

CI Integration

Ensure no placeholders in committed code:

yaml
# .github/workflows/ci.yml
- run: ./vendor/bin/testlink pair
  # Returns exit code 1 if unresolved placeholders exist

Or use validate:

yaml
- run: ./vendor/bin/testlink validate
  # Fails if placeholders remain

Comparison with Sync

FeaturePlaceholder (pair)Sync
Use caseTDD/BDD workflowExisting code
Requires markersYes (@placeholder)No
DirectionBoth simultaneouslyProduction → Test or Test → Production
WhenDuring developmentAfter 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:

  1. Write test code before production code exists
  2. Link them with temporary markers
  3. Resolve to real references when ready

They're a bridge between "test first" methodology and TestLink's requirement for real class references.

See Also

Released under the MIT License.