suggest-testability-improvements
Testability Improvement Suggestions
Provide actionable suggestions to improve code testability.
Improvement Categories
1. Extract Interface for Dependencies
// BEFORE: Concrete dependency
class PaymentProcessor
{
public function __construct(
private StripeGateway $gateway, // Concrete class
) {}
}
// Test problem: Must mock StripeGateway internals
// AFTER: Interface dependency
interface PaymentGatewayInterface
{
public function charge(Money $amount, PaymentMethod $method): PaymentResult;
}
class PaymentProcessor
{
public function __construct(
private PaymentGatewayInterface $gateway,
) {}
}
// Test benefit: Simple mock implementation
$mockGateway = $this->createMock(PaymentGatewayInterface::class);
$mockGateway->method('charge')->willReturn(PaymentResult::success());
2. Inject Time/Random Dependencies
// BEFORE: Hard to test time-based logic
class TokenGenerator
{
public function generate(): Token
{
return new Token(
bin2hex(random_bytes(32)),
new DateTime('+1 hour'),
);
}
}
// AFTER: Injectable dependencies
interface ClockInterface
{
public function now(): DateTimeImmutable;
}
interface RandomGeneratorInterface
{
public function bytes(int $length): string;
}
class TokenGenerator
{
public function __construct(
private ClockInterface $clock,
private RandomGeneratorInterface $random,
) {}
public function generate(): Token
{
return new Token(
bin2hex($this->random->bytes(32)),
$this->clock->now()->modify('+1 hour'),
);
}
}
// Test with frozen time
$clock = new FrozenClock(new DateTimeImmutable('2024-01-01 12:00:00'));
$random = new FixedRandom('0123456789abcdef...');
$generator = new TokenGenerator($clock, $random);
$token = $generator->generate();
// Now assertions are deterministic
3. Create Test Builders
// BEFORE: Tedious test setup
public function testOrderProcessing(): void
{
$customer = new Customer();
$customer->setId(1);
$customer->setName('John');
$customer->setEmail('john@example.com');
$customer->setStatus('active');
$product = new Product();
$product->setId(1);
$product->setName('Widget');
$product->setPrice(new Money(1000, 'USD'));
$order = new Order();
$order->setCustomer($customer);
$order->addItem(new OrderItem($product, 2));
// ... 20 more lines
}
// AFTER: Fluent builder
public function testOrderProcessing(): void
{
$order = OrderBuilder::create()
->withCustomer(CustomerBuilder::active()->build())
->withItem('Widget', 1000, quantity: 2)
->build();
$result = $this->processor->process($order);
$this->assertTrue($result->isSuccessful());
}
4. Separate Pure Logic from I/O
// BEFORE: Logic mixed with I/O
class PricingService
{
public function calculateOrderPrice(int $orderId): Money
{
$order = $this->repository->find($orderId); // I/O
$customer = $this->customerRepo->find($order->getCustomerId()); // I/O
$rates = $this->taxApi->getRates($customer->getCountry()); // I/O
// Business logic
$subtotal = $this->calculateSubtotal($order);
$discount = $this->applyDiscount($customer, $subtotal);
$tax = $this->calculateTax($subtotal, $rates);
return $subtotal->subtract($discount)->add($tax);
}
}
// AFTER: Pure calculator
final readonly class OrderPriceCalculator
{
public function calculate(
Order $order,
Customer $customer,
TaxRates $taxRates,
): Money {
$subtotal = $this->calculateSubtotal($order);
$discount = $this->applyDiscount($customer, $subtotal);
$tax = $this->calculateTax($subtotal, $taxRates);
return $subtotal->subtract($discount)->add($tax);
}
}
// I/O in thin service
final class PricingService
{
public function __construct(
private OrderRepository $orderRepo,
private CustomerRepository $customerRepo,
private TaxApiInterface $taxApi,
private OrderPriceCalculator $calculator,
) {}
public function calculateOrderPrice(int $orderId): Money
{
$order = $this->orderRepo->find($orderId);
$customer = $this->customerRepo->find($order->getCustomerId());
$rates = $this->taxApi->getRates($customer->getCountry());
return $this->calculator->calculate($order, $customer, $rates);
}
}
// Now calculator is easily testable without mocks
5. Use Repository Pattern for Data Access
// BEFORE: Direct database access
class UserService
{
public function __construct(private PDO $pdo) {}
public function findActive(): array
{
$stmt = $this->pdo->query("SELECT * FROM users WHERE active = 1");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// AFTER: Repository interface
interface UserRepositoryInterface
{
/** @return User[] */
public function findActive(): array;
public function findById(int $id): ?User;
public function save(User $user): void;
}
class UserService
{
public function __construct(
private UserRepositoryInterface $repository,
) {}
public function findActive(): array
{
return $this->repository->findActive();
}
}
// In-memory implementation for tests
class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function findActive(): array
{
return array_filter($this->users, fn($u) => $u->isActive());
}
public function givenUser(User $user): void
{
$this->users[$user->getId()] = $user;
}
}
6. Create Test Doubles
// Fake implementation for testing
final class FakeEmailSender implements EmailSenderInterface
{
private array $sentEmails = [];
public function send(Email $email): void
{
$this->sentEmails[] = $email;
}
public function getSentEmails(): array
{
return $this->sentEmails;
}
public function assertEmailSentTo(string $address): void
{
foreach ($this->sentEmails as $email) {
if ($email->getTo() === $address) {
return;
}
}
throw new AssertionError("No email sent to $address");
}
}
7. Parameter Objects for Complex Methods
// BEFORE: Many parameters, hard to mock
public function createOrder(
int $customerId,
array $items,
string $shippingMethod,
string $paymentMethod,
?string $couponCode,
?string $notes,
): Order {}
// AFTER: DTO with builder
final readonly class CreateOrderRequest
{
public function __construct(
public int $customerId,
public array $items,
public string $shippingMethod,
public string $paymentMethod,
public ?string $couponCode = null,
public ?string $notes = null,
) {}
}
// Test with builder
$request = CreateOrderRequestBuilder::create()
->forCustomer(1)
->withItem('SKU-001', 2)
->withShipping('express')
->build();
Implementation Priority
| Improvement | Impact | Effort |
|---|---|---|
| Extract interface | High | Low |
| Inject time/random | High | Medium |
| Create test builders | Medium | Medium |
| Separate pure logic | High | High |
| Repository pattern | High | Medium |
Output Format
### Testability Improvement: [Description]
**Location:** `file.php:line`
**Type:** [Extract Interface|Inject Dependency|Create Builder|...]
**Impact:** High/Medium/Low
**Current Problem:**
[Why current code is hard to test]
**Suggested Improvement:**
```php
// Improved code
Implementation Steps:
- Create interface XxxInterface
- Update class to depend on interface
- Create test double/mock
- Update test
Testing Benefit: [How this makes testing easier]
More from dykyi-roman/awesome-claude-code
psr-overview-knowledge
PHP Standards Recommendations (PSR) overview knowledge base. Provides comprehensive reference for all accepted PSRs including PSR-1,3,4,6,7,11,12,13,14,15,16,17,18,20. Use for PSR selection decisions and compliance audits.
22detect-code-smells
Detects code smells in PHP codebases. Identifies God Class, Feature Envy, Data Clumps, Long Parameter List, Long Method, Primitive Obsession, Message Chains, Inappropriate Intimacy. Generates actionable reports with refactoring recommendations.
15clean-arch-knowledge
Clean Architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Clean Architecture and Hexagonal Architecture audits.
15ddd-knowledge
DDD architecture knowledge base. Provides patterns, antipatterns, and PHP-specific guidelines for Domain-Driven Design audits.
14testing-knowledge
Testing knowledge base for PHP 8.4 projects. Provides testing pyramid, AAA pattern, naming conventions, isolation principles, DDD testing guidelines, and PHPUnit patterns.
12bug-root-cause-finder
Root cause analysis methods for PHP bugs. Provides 5 Whys technique, fault tree analysis, git bisect guidance, and stack trace parsing.
12