| name | testing-anti-patterns |
| description | 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. |
Testing Anti-Patterns
Core Principle
Avoid the five critical testing mistakes that undermine test value and create false confidence.
When to Use This Skill
- Writing new tests
- Reviewing test code
- Debugging failing tests
- Test suite feels fragile
- Tests pass but bugs still occur
- Refactoring breaks many tests
- Unsure how to test something
The Five Iron Laws
1. NEVER TEST MOCK BEHAVIOR
If your test only verifies mock interactions, it tests nothing.
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']);
}
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);
}
2. NEVER ADD TEST-ONLY METHODS
If code only exists for testing, your test is wrong.
class UserService
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function getRepositoryForTesting()
{
return $this->repository;
}
}
public function test_user_service_uses_repository()
{
$service = new UserService($repo);
$this->assertSame($repo, $service->getRepositoryForTesting());
}
public function test_user_service_saves_user()
{
$service = new UserService(new UserRepository());
$user = $service->createUser([ => ]);
->(->exists);
}
3. NEVER MOCK WITHOUT UNDERSTANDING
If you don't understand what you're mocking, your test is worthless.
public function test_payment_processing()
{
$mockStripe = $this->createMock(StripeClient::class);
$mockStripe->method('charge')->willReturn(true);
$service = new PaymentService($mockStripe);
$result = $service->processPayment($order);
$this->assertTrue($result);
}
public function test_payment_processing()
{
$mockStripe = $this->createMock(StripeClient::class);
$mockStripe->method('charge')
->willReturn(new ([
=> ,
=> ,
=> ,
]));
= ();
= ->();
->(, ->transaction_id);
->(, ->status);
}
4. INTEGRATION TESTS ARE NOT AN AFTERTHOUGHT
Integration tests should be written ALONGSIDE unit tests.
public function test_create_user()
{
$mockRepo = $this->createMock(UserRepository::class);
$service = new UserService($mockRepo);
}
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->(, ->name);
}
{
= ();
= ();
= ->([ => ]);
->(, [ => ]);
= ::(, )->();
->();
}
5. ALWAYS TEST ERROR PATHS
The happy path is 10% of your code. The other 90% is error handling.
public function test_user_registration()
{
$response = $this->postJson('/api/register', [
'email' => 'john@example.com',
'password' => 'secret123',
]);
$response->assertStatus(200);
}
public function test_user_registration_success()
{
$response = $this->postJson('/api/register', [
'email' => 'john@example.com',
'password' => 'secret123',
]);
$response->assertStatus(200);
}
public function test_registration_requires_email()
{
$response = $this->postJson('/api/register', [
'password' => 'secret123',
]);
$response->()
->();
}
{
::()->([ => ]);
= ->(, [
=> ,
=> ,
]);
->()
->();
}
{
DB::()
->( ());
= ->(, [
=> ,
=> ,
]);
->();
}
The Four Major Anti-Patterns
Anti-Pattern 1: Testing Mocks Instead of Behavior
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:
- Methods named "...ForTesting()"
- Public methods only used by tests
- Properties made public for testing
- Protected changed to public for tests
Why it happens:
- Can't figure out how to test properly
- Testing implementation instead of behavior
- Not using dependency injection
- Over-mocking
The fix:
- Test through public API only
- If you can't test it, redesign it
- Use proper dependency injection
- Test behavior, not implementation
Example:
class OrderProcessor
{
private $validator;
public function getValidatorForTesting()
{
return $this->validator;
}
}
public function test_processor_has_validator()
{
$processor = new OrderProcessor();
$this->assertInstanceOf(
Validator::class,
$processor->getValidatorForTesting()
);
}
class OrderProcessor
{
private $validator;
public function process(Order $order): bool
{
if (!$this->validator->isValid($order)) {
return false;
}
return true;
}
}
public {
= ();
= ([ => -]);
= ->();
->();
}
### Anti-Pattern 3: Incomplete or Incorrect Mocks
What it looks like:
- Mock returns wrong data types
- Mock behavior doesn't match real object
- Mock is missing key behaviors
- Tests pass but production fails
Why it happens:
- Don't understand the mocked dependency
- Copy/paste mock setup
- Mock created before understanding real behavior
- No integration tests to catch mismatches
The fix:
- Understand what you're mocking FIRST
- Make mock behavior match reality
- Write integration tests alongside unit tests
- Consider using real object instead
Example:
public function test_api_client_handles_response()
{
$mockClient = $this->createMock(HttpClient::class);
$mockClient->method('get')->willReturn([
'data' => 'something'
]);
$service = new ApiService($mockClient);
$result = $service->fetchData();
}
public function test_api_client_handles_response()
{
$mockResponse = new Response(200, [], json_encode([
'data' => 'something'
]));
$mockClient = $this->createMock(HttpClient::class);
$mockClient->method('get')->();
= ();
= ->();
->(, );
}
{
= ();
= ();
= ->();
->();
}
### Anti-Pattern 4: Integration Testing as Afterthought
What it looks like:
- Only unit tests with mocks
- Integration tests added "later" (never)
- No end-to-end tests
- Bugs found in production
Why it happens:
- "Unit tests are enough" mentality
- Integration tests seen as slower/harder
- Prioritizing coverage over confidence
- Not understanding test pyramid
The fix:
- Write integration tests alongside unit tests
- Test full stack for critical paths
- Use test-driven-development for both
- Balance unit and integration tests
Example:
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);
}
}
class OrderServiceTest extends TestCase
{
public function test_order_creation_logic()
{
= ->(::);
->()->();
= (
(),
,
()
);
= ->();
->(->id);
->(, ->status);
}
}
{
{
= (
(),
(),
()
);
= ->();
->(, [ => ->id]);
->(, [
=> []->id,
=> - []->quantity
]);
->(, [
=> ->id,
=>
]);
}
}
## 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 2: Tests Break on Refactoring
public function test_user_service()
{
$mockRepo = $this->createMock(UserRepository::class);
$mockRepo->expects($this->once())->method('save');
$service = new UserService($mockRepo);
$service->createUser(['name' => 'John']);
}
class UserRepository
{
public function persist(User $user) { }
}
Red Flag 3: Test Names Don't Describe Behavior
public function test_calls_repository() { }
public function test_uses_correct_method() { }
public function test_mocks_are_called() { }
public function test_creates_user_in_database() { }
public function test_rejects_invalid_email() { }
public function test_sends_welcome_email() { }
Red Flag 4: Can't Run Tests Independently
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()
{
$this->user->update(['name' => 'Jane']);
$this->assertEquals('Jane', $this->user->name);
}
How to Fix Anti-Patterns
Fix 1: Convert Mock Tests to Behavior Tests
public function test_notification_sent()
{
$mockMailer->expects($this->once())->method('send');
$service->notifyUser($user);
}
public function test_notification_sent()
{
Mail::fake();
$service->notifyUser($user);
Mail::assertSent(NotificationEmail::class);
}
Fix 2: Remove Test-Only Methods
class Service
{
private $dep;
public function getDepForTesting() { return $this->dep; }
}
class Service
{
private $dep;
public function process() {
return $this->dep->doSomething();
}
}
Fix 3: Add Integration Tests
public function test_with_mocks()
{
$mockDb = $this->createMock(Database::class);
}
public function test_with_real_database()
{
$service = new Service(new Database());
$result = $service->process();
$this->assertDatabaseHas('results', ['data' => $result]);
}
Fix 4: Test Error Paths
public function test_success_case() { }
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() { }
Integration with Skills
Use with:
test-driven-development - Avoid anti-patterns from start
code-review - Catch anti-patterns in review
systematic-debugging - Debug test issues
Prevents:
- False confidence from bad tests
- Tests that pass but code fails
- Brittle tests that break on refactoring
Checklist
When writing tests:
When reviewing tests:
Authority
This skill is based on:
- "Growing Object-Oriented Software, Guided by Tests" (Freeman & Pryce)
- Martin Fowler's testing patterns
- Research on test effectiveness
- Professional testing practices
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.
Your Commitment
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.