Sync Links Automatically
This guide shows how to use the sync command to automatically generate and maintain bidirectional links between production code and tests.
What Sync Does
The sync command maintains bidirectional links:
Forward sync (Production → Test):
- Reads
#[TestedBy]attributes from production code - Adds
->linksAndCovers()(Pest) or#[LinksAndCovers](PHPUnit) to tests - Adds
@seetags to production for IDE navigation
Reverse sync (Test → Production):
- Reads
->linksAndCovers()or#[LinksAndCovers]from tests - Adds
#[TestedBy]attributes to production methods
Basic Usage
Step 1: Run sync with dry-run
Always preview changes first:
./vendor/bin/testlink sync --dry-runOutput:
Syncing Coverage Links
──────────────────────
Running in dry-run mode. No files will be modified.
Would modify test files
✓ tests/Unit/UserServiceTest.php
+ linksAndCovers(UserService::class.'::create')
Would add #[TestedBy] to production
✓ OrderService::process
+ #[TestedBy(OrderServiceTest::class, 'test_processes_order')]
Dry run complete. Would modify 2 file(s).Step 2: Apply changes
If the preview looks correct:
./vendor/bin/testlink syncOutput:
Syncing Coverage Links
──────────────────────
Modified Files
✓ tests/UserServiceTest.php (1 change)
✓ src/Services/OrderService.php (1 change)
Sync complete. Modified 2 file(s).Sync Options
Sync specific directory
./vendor/bin/testlink sync --path=src/ServicesLink-only mode
Add test-side links without coverage (uses ->links() instead of ->linksAndCovers()):
./vendor/bin/testlink sync --link-onlyWith pruning
Remove orphaned links:
./vendor/bin/testlink sync --prune --forceWhat Gets Synced
Forward: Production to Tests
When production code has:
class UserService
{
#[TestedBy(UserServiceTest::class, 'test_creates_user')]
public function create(array $data): User
{
// ...
}
}Sync adds to test:
test('creates user', function () {
// ...
})->linksAndCovers(UserService::class.'::create');Reverse: Tests to Production
When test has a link but production doesn't have #[TestedBy]:
test('creates user', function () {
// ...
})->linksAndCovers(UserService::class.'::create');Sync adds #[TestedBy] to production:
use TestFlowLabs\TestingAttributes\TestedBy;
class UserService
{
#[TestedBy(UserServiceTest::class, 'test_creates_user')]
public function create(array $data): User
{
// ...
}
}Sync Workflow
Recommended workflow
# 1. Check current state
./vendor/bin/testlink validate
# 2. Preview sync changes
./vendor/bin/testlink sync --dry-run
# 3. Apply sync
./vendor/bin/testlink sync
# 4. Verify result
./vendor/bin/testlink validateStart from Production
Add #[TestedBy] to production, then sync:
# 1. Add #[TestedBy] to production method
# 2. Sync to add link to test
./vendor/bin/testlink syncStart from Tests
Add linksAndCovers() to test, then sync:
# 1. Write test with linksAndCovers()
# 2. Sync to add #[TestedBy] to production
./vendor/bin/testlink syncAfter refactoring
# 1. Sync with prune to clean up
./vendor/bin/testlink sync --prune --force
# 2. Check for issues
./vendor/bin/testlink validateBidirectional Sync Explained
See the sync command reference for details on how bidirectional sync works in both directions.
Combining Options
# Preview sync with pruning in specific directory
./vendor/bin/testlink sync --dry-run --prune --force --path=src/Services
# Sync with link-only (no coverage)
./vendor/bin/testlink sync --link-onlySync Behavior
What sync adds
| Source | Target | What's Added |
|---|---|---|
#[TestedBy] on production | Test file | linksAndCovers() or #[LinksAndCovers] |
linksAndCovers()/#[LinksAndCovers] in test | Production file | #[TestedBy] attribute |
| All links | Both sides | @see tags for navigation |
What sync doesn't do
- Doesn't remove
#[TestedBy]attributes (use--prunefor cleanup) - Doesn't modify test logic
- Doesn't create new test files
- Doesn't guess test names
Handling Conflicts
When sync finds existing links
By default, sync skips links that already exist.
When method names don't match
If the test name in #[TestedBy] doesn't match the actual test:
Warning: Test 'test_old_name' not found in Tests\UserServiceTestFix the #[TestedBy] attribute first:
// Wrong
#[TestedBy(UserServiceTest::class, 'test_old_name')]
// Correct
#[TestedBy(UserServiceTest::class, 'test_creates_user')]Best Practices
- Always dry-run first - Preview before modifying files
- Commit before sync - Easy to revert if needed
- Run after refactoring - Keep links up to date
- Use in CI - Ensure links stay synchronized
- Prune regularly - Remove stale links with
--prune --force