Awesome-claude-code suggest-testability-improvements
Suggests testability improvements for PHP code. Provides DI refactoring suggestions, mock opportunities, interface extraction, testing strategy recommendations.
install
source · Clone the upstream repo
git clone https://github.com/dykyi-roman/awesome-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/dykyi-roman/awesome-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/suggest-testability-improvements" ~/.claude/skills/dykyi-roman-awesome-claude-code-suggest-testability-improvements && rm -rf "$T"
manifest:
skills/suggest-testability-improvements/SKILL.mdsource content
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]