Bidirectional Linking
The goal: Cmd+Click to navigate between tests and production code.
Bidirectional linking makes this possible by creating explicit, clickable connections in both directions.
Why It Matters
The Navigation Problem
In most codebases, finding related tests requires searching:
class UserService
{
public function create(array $data): User
{
// Which tests verify this method?
// Is it tested at all?
// Time to search through test files...
}
}With bidirectional links, just Cmd+Click:
class UserService
{
/**
* @see \Tests\UserServiceTest::test_creates_user ← Click!
* @see \Tests\UserServiceTest::test_validates_email ← Click!
*/
public function create(array $data): User
{
// Two tests verify this. Click to jump directly.
}
}The same works in reverse—from tests to production:
/**
* @see \App\Services\UserService::create ← Click!
*/
public function test_creates_user(): void
{
// This test covers UserService::create. Click to jump.
}See All Relationships at a Glance
Without links, you have to hunt for relationships. With links, they're visible instantly:
/**
* @see \Tests\Unit\OrderServiceTest::test_creates_order
* @see \Tests\Unit\OrderServiceTest::test_validates_items
* @see \Tests\Unit\OrderServiceTest::test_calculates_total
* @see \Tests\Feature\OrderFlowTest::test_complete_checkout
*/
public function create(array $items): Order
{
// Four tests verify this method.
// Two are unit tests, two are feature tests.
// All visible here, all clickable.
}How It Works
Both Directions Required
Bidirectional means links exist on both sides:
Production → Test: @see tags point to tests
Test → Production: @see tags (or attributes) point to productionWhy both? Each direction answers a different question:
| From | You want to know | Link direction |
|---|---|---|
| Production code | "What tests verify this?" | Prod → Test |
| Test code | "What does this test cover?" | Test → Prod |
With both directions, navigation works from anywhere.
Production Side Links
Add @see tags pointing to tests:
class UserService
{
/**
* @see \Tests\UserServiceTest::test_creates_user
* @see \Tests\UserServiceTest::test_validates_email
*/
public function create(array $data): User
{
// ...
}
}Test Side Links
/**
* @see \App\Services\UserService::create
*/
test('creates user', function () {
// ...
})->linksAndCovers(UserService::class.'::create');Keeping Links Valid
Links break when code changes. TestLink catches this:
$ ./vendor/bin/testlink validate
✗ Broken link
UserService::create
→ UserServiceTest::test_old_name (test not found)
✗ Missing link
UserServiceTest::test_creates_user
→ UserService::create (no @see in production)Run validation in CI/CD to ensure navigation links stay accurate:
- run: ./vendor/bin/testlink validateGenerating Links Automatically
Don't maintain links by hand. Use sync:
$ ./vendor/bin/testlink sync
Syncing Coverage Links
──────────────────────
Modified Files
✓ src/Services/UserService.php (1 change)
+ #[TestedBy(UserServiceTest::class, 'test_creates_user')]
✓ tests/Unit/UserServiceTest.php (1 change)
+ linksAndCovers(OrderService::class.'::process')
Sync complete. Modified 2 file(s).Sync works bidirectionally:
| You add link here | Sync adds to |
|---|---|
Production: #[TestedBy(...)] | Test: linksAndCovers() or #[LinksAndCovers] |
Test: linksAndCovers() or #[LinksAndCovers] | Production: #[TestedBy(...)] |
Add a link on either side, run sync, and the other side gets updated automatically.
Additional Benefits
Refactoring Confidence
When you rename a method, validation tells you exactly what breaks:
$ ./vendor/bin/testlink validate
✗ UserServiceTest::test_creates_user
→ linksAndCovers(UserService::create) - method not found
Did you rename UserService::create?Living Documentation
The links document your test coverage:
/**
* @see \Tests\UserServiceTest::test_creates_user
* @see \Tests\UserServiceTest::test_creates_user_validates_email
* @see \Tests\UserServiceTest::test_creates_user_hashes_password
* @see \Tests\UserFlowTest::test_registration_flow
*/
public function create(array $data): UserReading the @see tags tells you:
- The method is tested
- What aspects are tested (email validation, password hashing)
- It's part of a larger flow (registration)
Coverage Reports
See all relationships at once:
$ ./vendor/bin/testlink report
UserService
create()
→ UserServiceTest::test_creates_user
→ UserServiceTest::test_validates_email
update()
→ UserServiceTest::test_updates_user
delete()
→ (no tests linked) ← Gap visible immediatelyWhen to Use
Always Link
- Unit tests — Direct method-to-test relationships
- Core business logic — Critical code that must be tested
- Public APIs — Methods others depend on
Consider Linking
- Integration tests — Use
#[Links]for secondary coverage - Helper methods — May not need individual links
Skip Linking
- Trivial code — Getters/setters without logic
- Generated code — Auto-generated files
Summary
Bidirectional linking exists for one primary purpose: Cmd+Click navigation between tests and production code.
Everything else—validation, sync, reports—supports this by keeping your navigation links accurate and up-to-date.
The result: No more searching for tests. Just click.