skills/testing/testing-anti-patterns/SKILL.md
Use to avoid critical testing mistakes. Five Iron Laws: Never test mock behavior, Never add test-only methods, Never mock without understanding, Always integration test, Always test error paths.
npx skillsauth add liauw-media/codeassist testing-anti-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Avoid the five critical testing mistakes that undermine test value and create false confidence.
If your test only verifies mock interactions, it tests nothing.
// ❌ BAD: Testing the mock, not the system
public function test_user_service_calls_repository()
{
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->expects($this->once())
->method('save')
->with($this->isInstanceOf(User::class));
$service = new UserService($mockRepo);
$service->createUser(['name' => 'John']);
// This test passes if mock is called
// But does the system actually work? Unknown!
}
// ✅ GOOD: Test actual behavior
public function test_user_service_creates_user()
{
$service = new UserService(new UserRepository());
$user = $service->createUser(['name' => 'John']);
$this->assertDatabaseHas('users', ['name' => 'John']);
$this->assertEquals('John', $user->name);
// Tests actual system behavior, not mocks
}
If code only exists for testing, your test is wrong.
// ❌ BAD: Test-only method
class UserService
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
// This method ONLY exists for testing!
public function getRepositoryForTesting()
{
return $this->repository;
}
}
public function test_user_service_uses_repository()
{
$service = new UserService($repo);
$this->assertSame($repo, $service->getRepositoryForTesting());
// Testing implementation, not behavior
}
// ✅ GOOD: Test observable behavior
public function test_user_service_saves_user()
{
$service = new UserService(new UserRepository());
$user = $service->createUser(['name' => 'John']);
// Test through public API, not internals
$this->assertTrue($user->exists);
}
If you don't understand what you're mocking, your test is worthless.
// ❌ BAD: Mocking without understanding
public function test_payment_processing()
{
// What does StripeClient actually do? Unknown!
$mockStripe = $this->createMock(StripeClient::class);
$mockStripe->method('charge')->willReturn(true);
$service = new PaymentService($mockStripe);
$result = $service->processPayment($order);
$this->assertTrue($result);
// But does this match real Stripe behavior? Unknown!
}
// ✅ GOOD: Understand what you're mocking
public function test_payment_processing()
{
// I understand Stripe returns PaymentIntent object
// with specific structure and states
$mockStripe = $this->createMock(StripeClient::class);
$mockStripe->method('charge')
->willReturn(new PaymentIntent([
'id' => 'pi_123',
'status' => 'succeeded',
'amount' => 1000,
]));
$service = new PaymentService($mockStripe);
$result = $service->processPayment($order);
// Test that we handle PaymentIntent correctly
$this->assertEquals('pi_123', $result->transaction_id);
$this->assertEquals('succeeded', $result->status);
}
Integration tests should be written ALONGSIDE unit tests.
// ❌ BAD: Only unit tests
// unit/UserServiceTest.php
public function test_create_user()
{
$mockRepo = $this->createMock(UserRepository::class);
$service = new UserService($mockRepo);
// ... test with mocks only
}
// Seems fine, but does the REAL system work?
// Does UserService work with REAL UserRepository?
// Does UserRepository work with REAL database?
// Unknown until production!
// ✅ GOOD: Unit AND integration tests
// unit/UserServiceTest.php
public function test_user_creation_logic()
{
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->method('save')->willReturn(true);
$service = new UserService($mockRepo);
$user = $service->createUser(['name' => 'John']);
$this->assertEquals('John', $user->name);
}
// integration/UserServiceIntegrationTest.php
public function test_user_creation_with_real_database()
{
$repo = new UserRepository();
$service = new UserService($repo);
$user = $service->createUser(['name' => 'John']);
// Tests entire stack
$this->assertDatabaseHas('users', ['name' => 'John']);
$found = User::where('name', 'John')->first();
$this->assertNotNull($found);
}
The happy path is 10% of your code. The other 90% is error handling.
// ❌ BAD: Only testing success
public function test_user_registration()
{
$response = $this->postJson('/api/register', [
'email' => '[email protected]',
'password' => 'secret123',
]);
$response->assertStatus(200);
// What about validation errors?
// What about duplicate emails?
// What about database failures?
}
// ✅ GOOD: Test happy path AND error paths
public function test_user_registration_success()
{
$response = $this->postJson('/api/register', [
'email' => '[email protected]',
'password' => 'secret123',
]);
$response->assertStatus(200);
}
public function test_registration_requires_email()
{
$response = $this->postJson('/api/register', [
'password' => 'secret123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors('email');
}
public function test_registration_rejects_duplicate_email()
{
User::factory()->create(['email' => '[email protected]']);
$response = $this->postJson('/api/register', [
'email' => '[email protected]',
'password' => 'secret123',
]);
$response->assertStatus(422)
->assertJsonValidationErrors('email');
}
public function test_registration_handles_database_failure()
{
// Simulate database down
DB::shouldReceive('transaction')
->andThrow(new QueryException());
$response = $this->postJson('/api/register', [
'email' => '[email protected]',
'password' => 'secret123',
]);
$response->assertStatus(500);
}
What it looks like:
- Tests verify mock method calls
- Tests check mock expectations
- No assertions on actual behavior
- Tests pass but code doesn't work
Why it happens:
- Misunderstanding of mocking purpose
- Following bad examples
- Cargo cult testing
- Not understanding what to test
The fix:
1. Mock only external dependencies
2. Assert on observable behavior
3. Verify actual outcomes
4. Test through public API
Example:
```php
// ❌ TESTS MOCK
public function test_email_is_sent()
{
$mockMailer = $this->createMock(Mailer::class);
$mockMailer->expects($this->once())
->method('send');
$service = new NotificationService($mockMailer);
$service->notifyUser($user);
// Test passes if mock.send() was called
// But was email actually sent? Unknown!
}
// ✅ TESTS BEHAVIOR
public function test_email_is_sent()
{
Mail::fake();
$service = new NotificationService();
$service->notifyUser($user);
Mail::assertSent(NotificationEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
// Tests that email was actually sent
}
### Anti-Pattern 2: Test-Only Methods and Properties
What it looks like:
Why it happens:
The fix:
Example:
// ❌ TEST-ONLY METHOD
class OrderProcessor
{
private $validator;
// This method ONLY for testing!
public function getValidatorForTesting()
{
return $this->validator;
}
}
public function test_processor_has_validator()
{
$processor = new OrderProcessor();
$this->assertInstanceOf(
Validator::class,
$processor->getValidatorForTesting()
);
}
// ✅ TEST BEHAVIOR
class OrderProcessor
{
private $validator;
public function process(Order $order): bool
{
if (!$this->validator->isValid($order)) {
return false;
}
// ... process order
return true;
}
}
public function test_processor_rejects_invalid_order()
{
$processor = new OrderProcessor();
$invalidOrder = new Order(['total' => -100]);
$result = $processor->process($invalidOrder);
$this->assertFalse($result);
// Tests validation through behavior, not internals
}
### Anti-Pattern 3: Incomplete or Incorrect Mocks
What it looks like:
Why it happens:
The fix:
Example:
// ❌ INCORRECT MOCK
public function test_api_client_handles_response()
{
$mockClient = $this->createMock(HttpClient::class);
$mockClient->method('get')->willReturn([
'data' => 'something'
]);
// Real API returns Response object, not array!
$service = new ApiService($mockClient);
$result = $service->fetchData();
// Test passes but production will fail!
}
// ✅ CORRECT MOCK
public function test_api_client_handles_response()
{
// I understand HttpClient returns Response object
$mockResponse = new Response(200, [], json_encode([
'data' => 'something'
]));
$mockClient = $this->createMock(HttpClient::class);
$mockClient->method('get')->willReturn($mockResponse);
// Mock matches real behavior
$service = new ApiService($mockClient);
$result = $service->fetchData();
$this->assertEquals('something', $result);
}
// ✅ EVEN BETTER: Integration test too
public function test_api_service_with_real_client()
{
// Use real HTTP client with test endpoint
$client = new HttpClient();
$service = new ApiService($client);
$result = $service->fetchData();
// Tests with real HTTP client
$this->assertNotNull($result);
}
### Anti-Pattern 4: Integration Testing as Afterthought
What it looks like:
Why it happens:
The fix:
Example:
// ❌ ONLY UNIT TESTS
class OrderServiceTest extends TestCase
{
public function test_creates_order()
{
$mockRepo = $this->createMock(OrderRepository::class);
$mockPayment = $this->createMock(PaymentService::class);
$mockInventory = $this->createMock(InventoryService::class);
$service = new OrderService($mockRepo, $mockPayment, $mockInventory);
$order = $service->createOrder($items);
// Everything mocked, does real system work? Unknown!
}
}
// ✅ UNIT AND INTEGRATION TESTS
// unit/OrderServiceTest.php
class OrderServiceTest extends TestCase
{
public function test_order_creation_logic()
{
// Mock external dependencies only
$mockPayment = $this->createMock(PaymentService::class);
$mockPayment->method('charge')->willReturn(true);
$service = new OrderService(
new OrderRepository(),
$mockPayment,
new InventoryService()
);
$order = $service->createOrder($items);
$this->assertNotNull($order->id);
$this->assertEquals('pending', $order->status);
}
}
// integration/OrderServiceIntegrationTest.php
class OrderServiceIntegrationTest extends TestCase
{
public function test_full_order_flow()
{
// No mocks, test entire stack
$service = new OrderService(
new OrderRepository(),
new PaymentService(),
new InventoryService()
);
$order = $service->createOrder($items);
// Verify database
$this->assertDatabaseHas('orders', ['id' => $order->id]);
// Verify inventory reduced
$this->assertDatabaseHas('inventory', [
'product_id' => $items[0]->id,
'quantity' => $originalQuantity - $items[0]->quantity
]);
// Verify payment recorded
$this->assertDatabaseHas('payments', [
'order_id' => $order->id,
'status' => 'completed'
]);
// Tests REAL system integration
}
}
## Recognizing Anti-Patterns in the Wild
### Red Flag 1: High Mock-to-Assertion Ratio
```php
// 🚩 RED FLAG
public function test_something()
{
// 20 lines of mock setup
$mock1->expects($this->once())->method('foo');
$mock2->expects($this->once())->method('bar');
$mock3->expects($this->once())->method('baz');
// ... 17 more lines ...
$service->doSomething();
// 1 assertion
$this->assertTrue(true);
// Ratio: 20:1 mock:assertion
// Probably testing mocks, not behavior!
}
// 🚩 RED FLAG
public function test_user_service()
{
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->expects($this->once())->method('save');
$service = new UserService($mockRepo);
$service->createUser(['name' => 'John']);
}
// Refactor: Rename save() to persist()
class UserRepository
{
public function persist(User $user) { } // Renamed!
}
// Test breaks! Even though behavior unchanged
// This indicates testing implementation, not behavior
// 🚩 RED FLAGS
public function test_calls_repository() { }
public function test_uses_correct_method() { }
public function test_mocks_are_called() { }
// These describe implementation, not behavior!
// ✅ GOOD
public function test_creates_user_in_database() { }
public function test_rejects_invalid_email() { }
public function test_sends_welcome_email() { }
// These describe observable behavior
// 🚩 RED FLAG
public function test_step_1_creates_user()
{
$this->user = User::create(['name' => 'John']);
$this->assertNotNull($this->user->id);
}
public function test_step_2_updates_user()
{
// Depends on test_step_1 running first!
$this->user->update(['name' => 'Jane']);
$this->assertEquals('Jane', $this->user->name);
}
// Tests must be independent!
// This is test pollution
// BEFORE: Testing mocks
public function test_notification_sent()
{
$mockMailer->expects($this->once())->method('send');
$service->notifyUser($user);
}
// AFTER: Testing behavior
public function test_notification_sent()
{
Mail::fake();
$service->notifyUser($user);
Mail::assertSent(NotificationEmail::class);
}
// BEFORE: Test-only method
class Service
{
private $dep;
public function getDepForTesting() { return $this->dep; }
}
// AFTER: Test through behavior
class Service
{
private $dep;
// No test-only methods
public function process() {
return $this->dep->doSomething();
}
}
// Test the process() result, not internal dep
// BEFORE: Only unit tests with mocks
public function test_with_mocks()
{
$mockDb = $this->createMock(Database::class);
// ... test with mocks only
}
// AFTER: Add integration test
public function test_with_real_database()
{
// Use real database
$service = new Service(new Database());
$result = $service->process();
$this->assertDatabaseHas('results', ['data' => $result]);
}
// BEFORE: Only happy path
public function test_success_case() { }
// AFTER: Happy path AND error paths
public function test_success_case() { }
public function test_validation_errors() { }
public function test_database_failure() { }
public function test_network_timeout() { }
public function test_invalid_input() { }
Use with:
test-driven-development - Avoid anti-patterns from startcode-review - Catch anti-patterns in reviewsystematic-debugging - Debug test issuesPrevents:
When writing tests:
When reviewing tests:
This skill is based on:
Research: Studies show proper testing reduces bugs by 40-80%, but anti-patterns reduce effectiveness to near zero.
Social Proof: All mature engineering teams avoid these anti-patterns.
When writing tests:
Bottom Line: Five Iron Laws prevent bad tests. Don't test mocks. Don't add test-only methods. Understand mocks. Write integration tests. Test error paths. Follow these, and your tests will actually protect you.
development
Use when decomposing complex work. Dispatch fresh subagent per task, review between tasks. Flow: Load plan → Dispatch task → Review output → Apply feedback → Mark complete → Next task. No skipping reviews, no parallel dispatch.
development
# Server Documentation System Set up a documentation system that tracks changes and maintains server/project documentation with Claude Code hooks. ## When to Use - Setting up a new server or development environment - Need to track configuration changes over time - Want automatic documentation of work sessions - Maintaining changelog for infrastructure ## Directory Structure ``` ~/docs/ # User home directory (cross-platform) ├── changelog.md # Global over
development
Delegate tasks to remote Claude Code agent containers for parallel execution, long-running analysis, or resource-intensive operations.
development
Use when working on multiple features simultaneously. Creates isolated workspaces without branch switching, enabling parallel development.