Links vs LinksAndCovers
Understanding when to use #[Links] versus #[LinksAndCovers] for optimal test organization.
Quick Comparison
| Attribute | Creates Link | Counts as Coverage |
|---|---|---|
#[LinksAndCovers] / ->linksAndCovers() | ✓ | ✓ |
#[Links] / ->links() | ✓ | ✗ |
Both create bidirectional links. The difference is whether the test claims coverage of the production method.
What is "Coverage"?
In TestLink, "coverage" means the test is the primary verification of that production method:
// This test IS the coverage for UserService::create
test('creates user with valid data', function () {
$service = new UserService();
$user = $service->create(['name' => 'John', 'email' => 'john@example.com']);
expect($user)->toBeInstanceOf(User::class);
expect($user->name)->toBe('John');
})->linksAndCovers(UserService::class.'::create');vs.
// This test USES UserService::create but doesn't claim to be its primary test
test('complete registration flow', function () {
$service = new UserService();
$user = $service->create(['name' => 'John']); // Used but not the focus
$emailService = new EmailService();
$emailService->sendWelcome($user); // This is what we're actually testing
expect($emailSent)->toBeTrue();
})->links(UserService::class.'::create')
->linksAndCovers(EmailService::class.'::sendWelcome');LinksAndCovers: Primary Coverage
Use linksAndCovers() (Pest) or #[LinksAndCovers] (PHPUnit) when:
1. Unit Tests
The test directly verifies the method's behavior:
test('adds two numbers', function () {
$calc = new Calculator();
expect($calc->add(2, 3))->toBe(5);
})->linksAndCovers(Calculator::class.'::add');
test('handles negative numbers', function () {
$calc = new Calculator();
expect($calc->add(-2, -3))->toBe(-5);
})->linksAndCovers(Calculator::class.'::add');2. The Test "Owns" the Method
If this test fails, the method is broken:
test('validates email format', function () {
$validator = new UserValidator();
expect($validator->validateEmail('valid@email.com'))->toBeTrue();
expect($validator->validateEmail('invalid'))->toBeFalse();
})->linksAndCovers(UserValidator::class.'::validateEmail');3. Coverage Reports Should Count It
You want this test-method relationship in coverage metrics:
./vendor/bin/testlink report
UserValidator
└── validateEmail()
└── UserValidatorTest::validates email format ← Counted as coverageLinks: Traceability Without Coverage
Use links() when:
1. Integration Tests
The test exercises the method but isn't its primary test:
test('checkout process creates order', function () {
// Many methods are called, but we're testing the flow
$cart = new Cart();
$cart->add($product);
$checkout = new CheckoutService();
$order = $checkout->process($cart);
expect($order)->toBeInstanceOf(Order::class);
})
->linksAndCovers(CheckoutService::class.'::process') // This IS what we're testing
->links(Cart::class.'::add'); // This is just setup2. E2E Tests
End-to-end tests touch many methods:
test('user can complete purchase', function () {
// Tests the entire flow
})
->links(UserService::class.'::create')
->links(CartService::class.'::add')
->links(PaymentService::class.'::charge')
->links(OrderService::class.'::create');
// All linked for traceability, none claiming primary coverage3. Avoiding Double Coverage
When unit tests already provide coverage:
// Unit test - provides coverage
test('charges credit card', function () {
$payment = new PaymentService();
expect($payment->charge(100))->toBeTrue();
})->linksAndCovers(PaymentService::class.'::charge');
// Integration test - just links (coverage already provided above)
test('order payment flow', function () {
// ...
})->links(PaymentService::class.'::charge');4. Setup/Teardown Methods
Methods used for test setup:
test('sends notification after user update', function () {
$user = UserFactory::create(); // Setup - not what we're testing
$service = new UserService();
$service->update($user, ['name' => 'New Name']);
expect($notificationSent)->toBeTrue();
})
->links(UserFactory::class.'::create') // Setup
->linksAndCovers(UserService::class.'::update'); // Actual test subjectThe Coverage Decision
Ask yourself: "If this test fails, does it mean THIS specific method is broken?"
- Yes →
linksAndCovers()- The test owns this method - No →
links()- The test uses this method but doesn't own it
Example Analysis
test('user registration sends welcome email', function () {
$userService = new UserService();
$user = $userService->create(['email' => 'test@example.com']);
$emailService = new EmailService();
$emailService->sendWelcome($user);
expect($emailSent)->toBeTrue();
});What should we link?
| Method | If test fails... | Use |
|---|---|---|
UserService::create | User creation might be broken, OR email sending might be broken | links() - not primary focus |
EmailService::sendWelcome | Email sending is broken | linksAndCovers() - primary focus |
test('user registration sends welcome email', function () {
// ...
})
->links(UserService::class.'::create')
->linksAndCovers(EmailService::class.'::sendWelcome');Validation Differences
LinksAndCovers Validation
testlink validate checks that #[TestedBy] exists on the production side:
// Test
->linksAndCovers(UserService::class.'::create')
// Production SHOULD have
#[TestedBy(UserServiceTest::class, 'creates user')]
public function create(): UserIf missing, validation reports it as an issue.
Links Validation
#[Links] doesn't require #[TestedBy]:
// Test
->links(CartService::class.'::add')
// Production - no TestedBy required
public function add(Product $product): voidThis makes sense: if a method is only used by integration tests (not primarily tested), it shouldn't need #[TestedBy].
Report Output
The report distinguishes between coverage types:
./vendor/bin/testlink report
UserService
└── create()
├── UserServiceTest::test_creates_user (covers)
└── RegistrationFlowTest::test_complete_flow (links)Common Patterns
Pattern 1: Unit + Integration
// Unit test covers the method
test('creates user', function () {
// Direct test of create()
})->linksAndCovers(UserService::class.'::create');
// Integration test links to it
test('registration flow', function () {
// Uses create() as part of flow
})->links(UserService::class.'::create');Pattern 2: One Focus, Many Dependencies
test('processes refund', function () {
// Primary focus is refund
})->linksAndCovers(PaymentService::class.'::refund')
->links(PaymentService::class.'::getTransaction')
->links(PaymentService::class.'::updateBalance')
->links(NotificationService::class.'::notify');Pattern 3: Feature Tests
test('user can update profile', function () {
// Feature test - links to all touched code
})->links(UserController::class.'::update')
->links(UserService::class.'::update')
->links(UserRepository::class.'::save');
// No linksAndCovers - unit tests provide coverageSummary
| Scenario | Use | Why |
|---|---|---|
| Unit test for a method | linksAndCovers | Primary coverage |
| Integration test | links | Traceability only |
| E2E test | links | Too broad for coverage |
| Setup code | links | Not the test focus |
| Already covered elsewhere | links | Avoid double counting |
Rule of thumb: One method should have one or few tests claiming linksAndCovers coverage, but can have many tests using links for traceability.