Skip to content

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:

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

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

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

php
/**
 * @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 production

Why both? Each direction answers a different question:

FromYou want to knowLink 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.

Add @see tags pointing to tests:

php
class UserService
{
    /**
     * @see \Tests\UserServiceTest::test_creates_user
     * @see \Tests\UserServiceTest::test_validates_email
     */
    public function create(array $data): User
    {
        // ...
    }
}
php
/**
 * @see \App\Services\UserService::create
 */
test('creates user', function () {
    // ...
})->linksAndCovers(UserService::class.'::create');

Links break when code changes. TestLink catches this:

bash
$ ./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:

yaml
- run: ./vendor/bin/testlink validate

Don't maintain links by hand. Use sync:

bash
$ ./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 hereSync 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:

bash
$ ./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:

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

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

bash
$ ./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 immediately

When to Use

  • 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.

See Also

Released under the MIT License.