Skip to content
acc-detect-test-smells logo

Test Smell Detection

acc-detect-test-smells

Detects test antipatterns and code smells in PHP test suites. Identifies 15 smells (Logic in Test, Mock Overuse, Fragile Tests, Mystery Guest, etc.) with fix recommendations and refactoring patterns for testability.

SKILL.md

Full skill instructions

Test Smell Detection

Identifies antipatterns and code smells in PHP test suites.

15 Test Smells

1. Logic in Test

Problem: Test contains conditional logic (if/​for/​while).

Detection:

Grep: "if \(" --glob "tests/​**/​*Test.php"
Grep: "for \(" --glob "tests/​**/​*Test.php"
Grep: "while \(" --glob "tests/​**/​*Test.php"
Grep: "foreach \(" --glob "tests/​**/​*Test.php"

Example (Bad):

public function test_calculates_total(): void
{
    $items = [10, 20, 30];
    $total = 0;
    foreach ($items as $item) {  // ❌ Logic in test
        $total += $item;
    }
    self::assertEquals($total, $this->calculator->sum($items));
}

Fix: Use data providers or inline expected values.

public function test_calculates_total(): void
{
    $items = [10, 20, 30];

    self::assertEquals(60, $this->calculator->sum($items));  // ✅
}

2. Mock Overuse

Problem: More than 3 mocks in a single test.

Detection:

Grep: "createMock\|createStub" --glob "tests/​**/​*Test.php" -C 20
# Count mocks per test method

Example (Bad):

public function test_process_order(): void
{
    $repository = $this->createMock(OrderRepository::class);      // 1
    $mailer = $this->createMock(MailerInterface::class);          // 2
    $logger = $this->createMock(LoggerInterface::class);          // 3
    $eventDispatcher = $this->createMock(EventDispatcher::class); // 4
    $validator = $this->createMock(ValidatorInterface::class);    // 5 ❌
    // ...
}

Fix: Use Fakes, refactor design, or split test.

public function test_process_order(): void
{
    $repository = new InMemoryOrderRepository();  // Fake
    $mailer = new CollectingMailer();             // Fake
    $eventDispatcher = new CollectingEventDispatcher(); // Fake
    // ...
}

3. Test Interdependence

Problem: Tests depend on execution order or shared state.

Detection:

Grep: "static \$" --glob "tests/​**/​*Test.php"
Grep: "self::\$[a-z]" --glob "tests/​**/​*Test.php"
# Check for @depends annotation
Grep: "@depends" --glob "tests/​**/​*Test.php"

Example (Bad):

private static array $createdUsers = [];  // ❌ Shared state

public function test_creates_user(): void
{
    $user = $this->service->create('[email protected]');
    self::$createdUsers[] = $user;
}

public function test_finds_created_user(): void
{
    $user = $this->service->find(self::$createdUsers[0]->id);  // ❌ Depends on previous
}

Fix: Each test creates its own data.

public function test_finds_user(): void
{
    $user = UserMother::default();
    $this->repository->save($user);

    $found = $this->service->find($user->id);

    self::assertEquals($user->id, $found->id);
}

4. Fragile Test

Problem: Test breaks when implementation changes (not behavior).

Detection:

# Method call order verification
Grep: "expects\(.*exactly\|expects\(.*at\(" --glob "tests/​**/​*Test.php"
# Internal method testing
Grep: "->method\('_" --glob "tests/​**/​*Test.php"

Example (Bad):

$mock->expects($this->exactly(3))->method('process');  // ❌ Verifies HOW, not WHAT
$mock->expects($this->at(0))->method('first');
$mock->expects($this->at(1))->method('second');

Fix: Test outcomes, not call sequences.

$this->service->processAll($items);

self::assertCount(3, $this->repository->findProcessed());  // ✅ Verifies WHAT

5. Mystery Guest

Problem: Test uses external files or hidden data sources.

Detection:

Grep: "file_get_contents\|fopen\|include\|require" --glob "tests/​**/​*Test.php"
Grep: "getenv\|_ENV\|_SERVER" --glob "tests/​**/​*Test.php"

Example (Bad):

public function test_imports_products(): void
{
    $data = json_decode(file_get_contents('fixtures/​products.json'));  // ❌ Hidden
    // Where does this file come from? What's in it?
}

Fix: Inline test data or use explicit builders.

public function test_imports_products(): void
{
    $data = [
        ['name' => 'Book', 'price' => 1000],
        ['name' => 'Pen', 'price' => 100],
    ];

    $this->importer->import($data);

    self::assertCount(2, $this->repository->findAll());
}

6. Eager Test

Problem: Single test verifies multiple unrelated behaviors.

Detection:

# Multiple assert groups with different subjects
Grep: "self::assert" --glob "tests/​**/​*Test.php" -C 5
# Count assertions per test method

Example (Bad):

public function test_user_operations(): void
{
    // Testing creation
    $user = $this->service->create('[email protected]');
    self::assertNotNull($user);

    // Testing update (different behavior!)
    $user->setName('John');
    $this->service->update($user);
    self::assertEquals('John', $user->getName());

    // Testing deletion (another behavior!)
    $this->service->delete($user);
    self::assertNull($this->repository->find($user->id));
}

Fix: One test per behavior.

public function test_creates_user(): void { ... }
public function test_updates_user_name(): void { ... }
public function test_deletes_user(): void { ... }

7. Assertion Roulette

Problem: Multiple assertions without messages, unclear which failed.

Detection:

# Count assertions per test method
Grep: "self::assert" --glob "tests/​**/​*Test.php"
# More than 5 assertions without context

Example (Bad):

public function test_order_properties(): void
{
    self::assertEquals('pending', $order->status);
    self::assertEquals(100, $order->total);
    self::assertEquals(3, count($order->items));
    self::assertEquals('[email protected]', $order->customer->email);
    self::assertEquals('2024-01-01', $order->createdAt->format('Y-m-d'));
    // Which one failed?
}

Fix: Group related assertions or add messages.

public function test_order_has_correct_status(): void
{
    self::assertEquals('pending', $order->status);
}

public function test_order_calculates_total(): void
{
    self::assertEquals(100, $order->total);
}

8. Obscure Test

Problem: Test purpose unclear from name or structure.

Detection:

# Generic test names
Grep: "test_it_works\|test_test\|test_foo" --glob "tests/​**/​*Test.php"
# Missing assertions
Grep: "function test_" --glob "tests/​**/​*Test.php" -A 10

Example (Bad):

public function test_it_works(): void  // ❌ What works?
{
    $x = $this->service->doSomething($this->data);
    self::assertTrue($x);
}

Fix: Descriptive name following convention.

public function test_calculate_total_with_discount_returns_reduced_price(): void
{
    // Clear intent
}

9. Test Code Duplication

Problem: Same setup/​assertion code repeated across tests.

Detection:

# Repeated patterns across test methods
# Manual review or static analysis tools

Example (Bad):

public function test_confirm_order(): void
{
    $order = new Order(OrderId::generate(), CustomerId::generate());
    $order->addItem(new Product('Book', Money::EUR(100)));
    // ... same setup in 10 tests
}

Fix: Extract to setUp, Builder, or Mother.

protected function setUp(): void
{
    $this->order = OrderBuilder::anOrder()->withItem($this->book)->build();
}

10. Conditional Test Logic

Problem: Different assertions based on conditions.

Detection:

Grep: "if.*assert\|assert.*if" --glob "tests/​**/​*Test.php"

Example (Bad):

public function test_process(): void
{
    $result = $this->service->process($input);
    if ($result !== null) {  // ❌
        self::assertInstanceOf(Order::class, $result);
    } else {
        self::fail('Should not be null');
    }
}

Fix: Explicit assertions.

public function test_process_returns_order(): void
{
    $result = $this->service->process($input);

    self::assertNotNull($result);
    self::assertInstanceOf(Order::class, $result);
}

11. Hard-Coded Test Data

Problem: Magic values without meaning.

Detection:

Grep: "'[a-z0-9]{8,}'" --glob "tests/​**/​*Test.php"
Grep: "12345\|999\|100" --glob "tests/​**/​*Test.php"

Example (Bad):

$order = new Order('550e8400-e29b-41d4-a716-446655440000');  // ❌ Magic UUID
$money = Money::EUR(12345);  // ❌ Magic number

Fix: Named constants or builders.

private const KNOWN_ORDER_ID = 'order-123';

$order = OrderBuilder::anOrder()
    ->withId(OrderId::fromString(self::KNOWN_ORDER_ID))
    ->withTotal(Money::EUR(100))  // Meaningful amount
    ->build();

12. Testing Private Methods

Problem: Tests access private/​protected methods directly.

Detection:

Grep: "setAccessible\(true\)" --glob "tests/​**/​*Test.php"
Grep: "ReflectionMethod\|ReflectionProperty" --glob "tests/​**/​*Test.php"

Example (Bad):

$method = new ReflectionMethod(Order::class, 'calculateDiscount');
$method->setAccessible(true);
$result = $method->invoke($order, $amount);  // ❌ Testing internals

Fix: Test through public API.

$order->applyDiscount($coupon);

self::assertEquals($expectedTotal, $order->total());  // ✅ Public API

13. Slow Test

Problem: Unit test takes >100ms.

Detection:

# Run PHPUnit with timing
phpunit --log-junit timing.xml
# Check for sleep, HTTP calls, file I/​O
Grep: "sleep\|usleep\|file_\|curl_" --glob "tests/​Unit/​**/​*Test.php"

Fix: Mock external dependencies, use in-memory implementations.


14. Mocking Final Classes

Problem: Attempting to mock final classes.

Detection:

# Find final classes
Grep: "final class" --glob "src/​**/​*.php"
# Cross-reference with mocks in tests
Grep: "createMock\(.*::" --glob "tests/​**/​*Test.php"

Fix: Mock interfaces, not implementations.

// Bad: $mock = $this->createMock(FinalService::class);
// Good:
$mock = $this->createMock(ServiceInterface::class);

15. Mocking Value Objects

Problem: Mocking immutable value objects.

Detection:

# Find readonly classes (likely VOs)
Grep: "readonly class" --glob "src/​**/​*.php"
# Check if they're mocked

Fix: Use real value objects — they're simple and deterministic.

// Bad: $email = $this->createMock(Email::class);
// Good:
$email = new Email('[email protected]');

Output Format

# Test Smell Report

## Summary

| Smell | Count | Severity |
|-------|-------|----------|
| Logic in Test | 5 | High |
| Mock Overuse | 3 | High |
| Mystery Guest | 2 | Medium |
| Eager Test | 8 | Medium |

## Findings

### Logic in Test (5 occurrences)

| File | Line | Code |
|------|------|------|
| OrderTest.php | 45 | `foreach ($items as $item)` |
| UserTest.php | 23 | `if ($result !== null)` |

**Recommendation:** Extract to data providers or inline values.

### Mock Overuse (3 occurrences)

| File | Test Method | Mock Count |
|------|-------------|------------|
| PaymentTest.php | test_process | 6 |
| OrderServiceTest.php | test_place | 5 |

**Recommendation:** Use Fakes (InMemory implementations) or split tests.

## Action Items

1. **High Priority**
   - Refactor `PaymentTest::test_process` — 6 mocks indicates design smell
   - Remove loops from `OrderTest` — use DataProvider

2. **Medium Priority**
   - Inline fixture data in `ImporterTest`
   - Split `UserTest::test_user_operations` into 3 tests

## Recommended Skills

| Smell | Fix With |
|-------|----------|
| Mock Overuse | `acc-create-mock-repository` |
| Mystery Guest | `acc-create-test-builder` |
| Test Duplication | `acc-create-test-builder` |

Severity Matrix

SmellSeverityImpact
Logic in TestHighUnreliable results
Mock OveruseHighDesign problem indicator
Test InterdependenceHighFlaky tests
Fragile TestHighMaintenance burden
Mocking Final/​VOHighRuntime errors
Mystery GuestMediumHard to understand
Eager TestMediumHard to diagnose failures
Slow TestMediumDevelopment slowdown
Obscure TestLowDocumentation issue
Hard-Coded DataLowReadability

Refactoring for Testability

See references/​refactoring-patterns.md for detailed refactoring patterns:

  • Extract Interface (Seam)
  • Constructor Injection
  • Replace Singleton with DI
  • Break Temporal Coupling
  • Extract Pure Function
  • Replace new with Factory
  • Testability Score Checklist
  • Smell → Refactoring Matrix

More skills from diegosouzapw

accessibility logo
diegosouzapw/awesome-omni-skill

accessibility

Accessibility guidelines for VS Code features — covers accessibility help dialogs, accessible views, verbosity settings, accessibility signals, ARIA alerts/status announcements, keyboard navigation, and ARIA labels/roles. Applies to both new interactive UI surfaces and updates to existing feature...

62 1
View
mermaid-diagrams logo
diegosouzapw/awesome-omni-skill

mermaid-diagrams

Mermaid diagram creation for flowcharts, sequences, ERDs, and more. Generate diagrams from text in markdown files. Use for documentation, architecture diagrams, and visual representations. Triggers on mermaid, flowchart, sequence diagram, ERD, entity relationship, gantt chart, pie chart, class di...

62 1
View
tauri2-react-rust logo
diegosouzapw/awesome-omni-skill

tauri2-react-rust

Guides development of cross-platform desktop apps with Tauri 2, TypeScript, React, and Rust. Use when building Tauri apps, implementing IPC, designing Rust backend or TypeScript/React frontend, when researching or cloning a website (open site, snapshot elements), when verifying local dev or built...

62 1
View
fp-ts-errors logo
diegosouzapw/awesome-omni-skill

fp-ts-errors

Handle errors as values using fp-ts Either and TaskEither for cleaner, more predictable TypeScript code. Use when implementing error handling patterns with fp-ts.

62 0
View
ring:pre-dev-research logo
diegosouzapw/awesome-omni-skill

ring:pre-dev-research

Gate 0 research phase for pre-dev workflow. Dispatches 3 parallel research agents to gather codebase patterns, external best practices, and framework documentation BEFORE creating PRD/TRD. Outputs research.md with file:line references.

62 0
View
create-assistant logo
diegosouzapw/awesome-omni-skill

create-assistant

Create and configure Vapi voice AI assistants with models, voices, transcribers, tools, hooks, and advanced settings. Use when building voice agents, phone bots, customer support assistants, or any conversational AI that handles phone or web calls.

62 0
View
codex-headless logo
diegosouzapw/awesome-omni-skill

codex-headless

Delegiere Aufgaben an OpenAI Codex CLI im Headless-Mode. Nutzt ChatGPT Subscription (KEIN API Key). Nur Workspace-Zugriff, KEIN System-Zugriff. Auto-Accept mit --full-auto. Web-Recherche mit --search.

62 0
View
social-media-scheduler logo
diegosouzapw/awesome-omni-skill

social-media-scheduler

Generate a full week of social media content for any topic. Outputs platform-optimized posts for Twitter/X, LinkedIn, and Instagram with hashtags and posting times.

62 0
View
Data Privacy Compliance logo
diegosouzapw/awesome-omni-skill

Data Privacy Compliance

Data privacy and regulatory compliance specialist for GDPR, CCPA, HIPAA, and international data protection laws. Use when implementing privacy controls, conducting data protection impact assessments, ensuring regulatory compliance, or managing data subject rights. Expert in consent management, da...

62 0
View
triple-review logo
diegosouzapw/awesome-omni-skill

triple-review

Reviews a task using the TRIPLE protocol, as the review agent.

62 0
View
Tech Stack & Architecture Decision logo
diegosouzapw/awesome-omni-skill

Tech Stack & Architecture Decision

Define and document the technology stack and architecture decisions for a project. Use when the user needs to choose a tech stack, make architecture decisions, define infrastructure choices, or document technology selections. Triggers on requests like "define the tech stack", "choose technologies...

62 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View