How practice Feature Testing with PHP examples.

Feature tests are where the magic of real-world validation happens. While unit tests ensure each LEGO brick of your system works perfectly, feature tests make sure the entire castle stands strong when those bricks are assembled. They simulate real user actions or API flows to ensure everything plays nicely together — from the database to the controllers, to the returned response.

This article dives into what feature testing is, why it’s crucial, and how to apply it effectively in PHP (especially with frameworks like Laravel). We’ll explore common mistakes, best practices, and end with a live sandbox example that shows how to test system-level behavior even without a framework.

You can find the previous article in this category below:

A feature test checks how multiple parts of your system interact.
Instead of testing a single method in isolation, it verifies the behavior of a complete feature — such as creating an order, logging in, or applying a discount via an API call.

For example in Laravel, feature tests often hit routes, use HTTP requests, and verify full responses. Outside frameworks, you can simulate this behavior manually — the goal is the same: confirm that everything works together correctly.

  • System-level focus: They test workflows, not just methods.
  • Integration awareness: Feature tests touch the database, files, or APIs.
  • Higher confidence: They reflect how real users interact with your application.
  • Slower but powerful: They run slower than unit tests, but catch real-world bugs early.

Imagine a login feature: your unit tests confirm the password validator and the user model both work. But what if the controller forgets to hash passwords before saving? Unit tests won’t catch that — feature tests will.

Feature tests ensure that independent parts actually collaborate correctly. They catch “lost connection” bugs between layers that unit tests can’t see.

  • Prevent broken integrations between components.
  • Verify user journeys end-to-end (e.g., registration → confirmation → login).
  • Improve reliability before production.
  • Serve as living documentation for how your API or system behaves.

A great feature test mirrors a real use case, such as:

“When a user submits an order with a valid coupon, they should receive a 10% discount.”

Good feature tests are:

  • Realistic – mimic user behavior.
  • Focused – each test checks one end-to-end scenario.
  • Independent – setup and teardown data per test.
  • Readable – describe intent in plain language.

AspectUnit TestFeature Test
ScopeSingle function/classMultiple components
DependenciesNone (mocked)Real (DB, files, APIs)
SpeedFastSlower
PurposeVerify logic correctnessVerify system behavior
ExampleDiscountService::calculate()“User applies discount on checkout”

In practice, you’ll use both:

  • Unit tests → check building blocks.
  • Feature tests → verify the finished house.
  1. Test key user flows – login, CRUD operations, checkout, etc.
  2. Keep setup simple – use seeders or test factories.
  3. Name tests by behavior – e.g., test_user_can_create_post().
  4. Run them in CI – feature tests ensure real-world stability.
  5. Balance runtime – aim for <10 min total (unit + feature).

  • ❌ Mixing too much logic in one test — keep one scenario per test.
  • ❌ Depending on fragile external services — use fake/stub APIs.
  • ❌ Ignoring database cleanup — tests must not affect each other.
  • ❌ Forgetting edge cases — simulate both success and failure paths.

Sandbox link: https://onlinephp.io/c/ec36c

This example simulates a simple order checkout feature.
We test that applying a coupon correctly updates the order total — end to end.

1. Application logic

OrderService handles business logic (e.g., discounts).

OrderController represents the feature endpoint — the system entry point.

class OrderController {
    public function checkout($data) {
        $orderService = new OrderService();
        return $orderService->process($data);
    }
}

class OrderService {
    public function process($data) {
        $price = $data['price'];
        $coupon = $data['coupon'] ?? null;

        if ($coupon === 'SUMMER10') {
            $price *= 0.9;
        }

        return [
            'status' => 'success',
            'final_price' => round($price, 2),
        ];
    }
}
PHP

2. Mini Testing Framework

function assertFeature($expected, $actual, $name, $output = null) {
    // non-strict comparison to avoid int/float mismatch
    if ($expected == $actual) {
        echo "✅ $name passed\n";
    } else {
        echo "❌ $name failed\n";
        echo "   → Expected: $expected, got: $actual\n";
        if ($output) echo "   → Full output: " . json_encode($output) . "\n";
    }
}
PHP

3. Feature Tests

$controller = new OrderController();

// 1️⃣ Valid coupon should apply 10% discount
$response = $controller->checkout(['price' => 100, 'coupon' => 'SUMMER10']);
assertFeature(90, $response['final_price'], "Valid coupon applies 10% discount", $response);

// 2️⃣ No coupon keeps full price
$response = $controller->checkout(['price' => 100]);
assertFeature(100, $response['final_price'], "No coupon keeps full price", $response);

// 3️⃣ Invalid coupon is ignored
$response = $controller->checkout(['price' => 100, 'coupon' => 'INVALID']);
assertFeature(100, $response['final_price'], "Invalid coupon ignored", $response);

// 4️⃣ Edge case: zero price (should stay zero)
$response = $controller->checkout(['price' => 0, 'coupon' => 'SUMMER10']);
assertFeature(0, $response['final_price'], "Zero price stays zero", $response);

// 5️⃣ Edge case: floating-point price
$response = $controller->checkout(['price' => 199.99, 'coupon' => 'SUMMER10']);
assertFeature(179.99, $response['final_price'], "Floating point discount works", $response);
PHP

✅ Each test calls the controller directly — simulating a real “feature” run.
✅ We check the full flow: controller → service → final output.
✅ This is exactly what a feature test does — verifying behavior across multiple components.

Feature testing ensures your system behaves as expected when real users interact with it. It catches bugs that unit tests can’t see — especially when components depend on each other.

In my own workflow, unit tests protect the core logic, while feature tests confirm that everything works together — the API endpoints, data flow, and user outcomes.

The goal is not just to prove your code works — it’s to prove your software delivers value correctly.

Start simple, automate early, and let your feature tests give you confidence that every release is solid.

Leave a Reply

Your email address will not be published. Required fields are marked *