0

Unit test — testing one class in isolation, all dependencies replaced

Beginner5 min read·eng-17-001
interview

Concept

Unit test — a test that verifies a single unit of code (a class or function) in isolation, with all dependencies replaced by test doubles.

"Unit" definition: The smallest testable piece of the system. Usually one class, one method, or one function. Some define it as one class; others as one function.

"In isolation": All dependencies (database, HTTP clients, email senders, other services) are replaced with test doubles (mocks, stubs, fakes). The unit test only tests the logic WITHIN the unit, not its interactions with the real world.

Properties of a good unit test:

  • Fast: Milliseconds. No I/O, no network, no real DB.
  • Isolated: Does not depend on other tests. Can run in any order.
  • Repeatable: Same result every time. No random data, no time dependency.
  • Self-checking: Has assertions that verify expected behavior.
  • Small: Tests one behavior at a time.

What unit tests are NOT good for:

  • Testing that your SQL query is correct (use integration tests with a real DB).
  • Testing that your HTTP client handles a 429 response (integration test).
  • Testing that the full user registration flow works (feature test).

Unit tests in PHP/PHPUnit:

  • Class extending \PHPUnit\Framework\TestCase.
  • Method names start with test_ or have @test annotation.
  • No database, no Laravel app boot. Pure PHP.

Ratio in test pyramid: Unit tests form the base — many fast unit tests at the bottom. Few slow integration/feature tests at the top.

Code Example

php
<?php
// UNIT UNDER TEST — pure business logic
class PricingEngine
{
    public function applyDiscount(float $price, float $discountPercent): float
    {
        if ($discountPercent < 0 || $discountPercent > 100) {
            throw new \InvalidArgumentException("Discount must be 0–100%, got {$discountPercent}");
        }
        return round($price * (1 - $discountPercent / 100), 2);
    }

    public function calculateTax(float $subtotal, float $taxRate): float
    {
        return round($subtotal * $taxRate, 2);
    }
}

// UNIT TEST — PHPUnit, no DB, no framework
class PricingEngineTest extends \PHPUnit\Framework\TestCase
{
    private PricingEngine $engine;

    protected function setUp(): void
    {
        $this->engine = new PricingEngine(); // no mocking needed — no dependencies
    }

    public function test_applies_discount_correctly(): void
    {
        $discounted = $this->engine->applyDiscount(100.00, 10);
        $this->assertEquals(90.00, $discounted);
    }

    public function test_zero_discount_returns_original_price(): void
    {
        $this->assertEquals(50.00, $this->engine->applyDiscount(50.00, 0));
    }

    public function test_full_discount_returns_zero(): void
    {
        $this->assertEquals(0.00, $this->engine->applyDiscount(100.00, 100));
    }

    public function test_throws_for_negative_discount(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->engine->applyDiscount(100.00, -5);
    }

    public function test_throws_for_discount_over_100(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->engine->applyDiscount(100.00, 101);
    }

    public function test_calculates_tax(): void
    {
        $this->assertEquals(9.00, $this->engine->calculateTax(100.00, 0.09));
    }
}

// WITH MOCKED DEPENDENCY — testing class that has dependencies
class OrderService
{
    public function __construct(
        private readonly OrderRepositoryInterface $repo,
        private readonly LoggerInterface          $logger,
    ) {}

    public function place(array $data): Order
    {
        $order = $this->repo->save($data);
        $this->logger->log("Order {$order->id} placed");
        return $order;
    }
}

class OrderServiceTest extends \PHPUnit\Framework\TestCase
{
    public function test_logs_after_placing_order(): void
    {
        $mockOrder  = new Order(['id' => 42]);
        $mockRepo   = $this->createMock(OrderRepositoryInterface::class);
        $mockLogger = $this->createMock(LoggerInterface::class);

        $mockRepo->method('save')->willReturn($mockOrder);
        $mockLogger->expects($this->once())->method('log')->with('Order 42 placed');

        $service = new OrderService($mockRepo, $mockLogger);
        $service->place(['total' => 100]);
        // No DB, no file system — pure logic test
    }
}