Back to Blog
Testing Architecture: Designing for Testability
Testability as Architecture
Systems designed without testing in mind become untestable. Dependency injection, interface segregation, and proper layering aren't just good design—they're prerequisites for comprehensive testing.
Dependency Injection
// Hard to test: creates its own dependencies
class OrderService
{
public function process(Order $order): void
{
$payment = new StripePayment(); // Untestable!
$payment->charge($order->total);
}
}
// Testable: dependencies injected
class OrderService
{
public function __construct(
private PaymentGateway $payment
) {}
public function process(Order $order): void
{
$this->payment->charge($order->total);
}
}
Interface Segregation
// Define interfaces for external services
interface PaymentGateway
{
public function charge(Money $amount): PaymentResult;
public function refund(string $transactionId): RefundResult;
}
// Easy to mock
class FakePaymentGateway implements PaymentGateway
{
public array $charges = [];
public function charge(Money $amount): PaymentResult
{
$this->charges[] = $amount;
return new PaymentResult(success: true);
}
}
Test Doubles
class OrderServiceTest extends TestCase
{
public function test_processes_payment(): void
{
$fakePayment = new FakePaymentGateway();
$service = new OrderService($fakePayment);
$order = Order::factory()->make(['total' => 100]);
$service->process($order);
$this->assertCount(1, $fakePayment->charges);
$this->assertEquals(100, $fakePayment->charges[0]->amount);
}
}
Repository Pattern for Data Access
interface OrderRepository
{
public function find(int $id): ?Order;
public function save(Order $order): void;
}
// Production: uses database
class EloquentOrderRepository implements OrderRepository { }
// Testing: in-memory
class InMemoryOrderRepository implements OrderRepository
{
private array $orders = [];
public function find(int $id): ?Order
{
return $this->orders[$id] ?? null;
}
public function save(Order $order): void
{
$this->orders[$order->id] = $order;
}
}
Feature Flags for Testing
class FeatureTest extends TestCase
{
public function test_new_checkout_flow(): void
{
Feature::activate('new-checkout');
$response = $this->get('/checkout');
$response->assertSee('New Checkout');
}
}
Conclusion
Design for testability from the start. Use dependency injection, define interfaces for external services, and implement repository patterns. The investment pays off in maintainability and confidence.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch