Introduction to Unit Tests and Feature Tests in Laravel

Testing is a critical part of modern software development. Laravel provides one of the most powerful and developer-friendly testing environments in the PHP ecosystem. At the heart of Laravel’s testing framework are two major test types: Unit Tests and Feature Tests. Although both are essential for building reliable and maintainable applications, they serve very different purposes and operate at different levels of the system.

Understanding the difference between Unit Tests and Feature Tests helps developers design better applications, choose the right testing strategy, improve code quality, and ensure reliability at scale. This comprehensive guide explains the conceptual differences, technical differences, performance considerations, scopes of testing, tooling, examples, best practices, real-world use cases, and how to decide which type of test to write in different scenarios.

What Are Unit Tests

Unit Tests focus on the smallest pieces of your application: individual functions, methods, or classes. They isolate a piece of logic and check whether it works correctly without considering external systems like databases, file storage, or HTTP requests.

The primary goal of a unit test is to validate that a piece of code behaves exactly as expected in controlled conditions. Because they work in isolation, unit tests run extremely fast and allow developers to catch bugs early in the development cycle.

Unit tests are valuable because they ensure that the internal logic of your application is correct and remains correct even after future changes.


Characteristics of Unit Tests

Unit Tests have several characteristics that set them apart:

  • They test isolated pieces of logic
  • They do not depend on external services
  • They avoid using real databases or HTTP interactions
  • They are extremely fast
  • They are easy to debug
  • They often rely on mocks, stubs, and fakes
  • They validate internal logic rather than full features

Unit tests answer questions like: “Does this function return the correct value?” or “Does this class behave correctly?”


Location of Unit Tests in Laravel

Laravel stores unit tests inside:

tests/Unit

You can generate a unit test using Artisan:

php artisan make:test CalculateTotalTest --unit

This creates a test file in the Unit directory.


What Are Feature Tests

Feature Tests test full workflows, meaning they simulate how the application behaves in real usage scenarios. A feature test might simulate an HTTP request, check the response, validate that data was written to the database, verify session output, and more.

Feature tests closely replicate real user flows. If your feature test passes, you can be confident that the entire stack for that feature works end-to-end.

Feature tests are slower than unit tests because they interact with controllers, middleware, databases, views, and external resources.


Characteristics of Feature Tests

Feature Tests often include:

  • Full HTTP request simulation
  • Routing
  • Controller logic
  • Middleware
  • Database interactions
  • Validation
  • Authentication
  • Business workflows
  • APIs

Feature tests answer questions like: “Does the user dashboard load successfully?” or “Does a form submission save data and return the correct response?”


Location of Feature Tests in Laravel

Laravel stores feature tests inside:

tests/Feature

Generate a feature test:

php artisan make:test UserRegistrationTest

Feature tests simulate real application behavior.


Key Differences Between Unit Tests and Feature Tests


Level of Testing

Unit Tests:

  • Test the smallest units of code
  • Do not test full systems

Feature Tests:

  • Test full systems and workflows
  • Often span multiple components

Isolation

Unit Tests:

  • Completely isolated
  • No database, no HTTP, no external dependencies

Feature Tests:

  • Touch multiple layers of the framework
  • Work with databases, routes, middleware, and controllers

Speed

Unit Tests:

  • Extremely fast
  • Can run thousands in seconds

Feature Tests:

  • Slower due to real interactions
  • Useful for ensuring stability across features

Complexity

Unit Tests:

  • Short and simple
  • Easy to debug

Feature Tests:

  • More complex
  • Harder to debug failures because many layers are involved

Dependencies

Unit Tests:

  • Use mocks, stubs, and fakes to replace dependencies

Feature Tests:

  • Use real components unless explicitly mocked

Testing Goals

Unit Tests:

  • Validate functions, classes, methods

Feature Tests:

  • Validate application behavior and full user interactions

Example of a Unit Test

Unit Test for a helper class:

class PriceCalculatorTest extends TestCase
{
public function test_total_price_is_calculated_correctly()
{
    $calculator = new PriceCalculator;
    $result = $calculator->calculate(100, 0.1);
    $this->assertEquals(110, $result);
}
}

This test does not touch the database, HTTP routes, or controllers.


Example of a Feature Test

public function test_user_can_register()
{
$response = $this->post('/register', [
    'name' => 'John Doe',
    'email' => '[email protected]',
    'password' => 'secret123',
    'password_confirmation' => 'secret123',
]);
$response->assertStatus(302);
$this->assertDatabaseHas('users', [
    'email' => '[email protected]',
]);
}

This test checks:

  • HTTP request
  • Form submission
  • Database interaction
  • Authentication system

When to Write Unit Tests

Write a unit test when:

  • You want to validate a method or function
  • You are implementing domain logic
  • You are building utility classes
  • You want fast feedback
  • You want to catch regressions early
  • You want to isolate logic from external dependencies

Unit tests are ideal for logic-heavy classes.


When to Write Feature Tests

Write a feature test when:

  • Testing controllers
  • Testing routes
  • Testing middleware
  • Testing form submissions
  • Testing APIs
  • Testing user authentication
  • Testing database-driven workflows

Feature tests ensure the entire application works correctly.


Mocking in Unit Tests

Unit tests often require mocks to isolate logic.

Example:

$service = Mockery::mock(PaymentGateway::class);
$service->shouldReceive('charge')->with(100)->andReturn(true);

Mocks allow you to simulate dependencies without calling the real implementation.


Using Database in Feature Tests

Feature tests often use the database:

use RefreshDatabase;

public function test_post_can_be_created()
{
$this->post('/posts', [
    'title' => 'New Post',
    'body' => 'Content here'
]);
$this->assertDatabaseHas('posts', ['title' => 'New Post']);
}

Laravel automatically uses an in-memory SQLite database or a test database.


Testing HTTP Requests

Feature tests simulate real HTTP requests:

$response = $this->get('/dashboard');

$response->assertStatus(200);

Unit tests do not do this.


Testing Responses in Feature Tests

Example:

$response->assertSee('Welcome');
$response->assertJson(['status' => 'success']);

Feature tests validate the exact response structure.


Difference in Assertions

Unit Tests:

  • Assert values
  • Assert class behavior

Feature Tests:

  • Assert HTTP status
  • Assert views
  • Assert JSON
  • Assert session data
  • Assert redirected URLs
  • Assert database records

Database Transactions in Tests

Feature tests often require transaction management:

use RefreshDatabase;

Each test runs in its own transaction.


Memory Usage Differences

Unit Tests:

  • Very low memory usage

Feature Tests:

  • Higher memory usage because they bootstrap the full application

Execution Times

Example:

  • 100 unit tests: maybe 50ms
  • 100 feature tests: several seconds

Feature tests cover more ground but at a cost.


Writing Reliable Unit Tests

To write strong unit tests:

  • Keep logic small and focused
  • Avoid testing too much in one test
  • Avoid real database
  • Use mocks responsibly
  • Use dependency injection

Unit tests should never break because of external factors.


Writing Reliable Feature Tests

To write stable feature tests:

  • Use database transactions
  • Seed data when required
  • Test one flow per test
  • Avoid unnecessary heavy operations
  • Use factories to generate test data

Feature tests should reflect real user behavior.


Trade-Offs Between Unit and Feature Tests

Unit Tests:

  • High speed
  • High precision
  • Low scope

Feature Tests:

  • High coverage
  • High confidence
  • Slower performance

Balancing both gives a strong test suite.


Testing Strategy in Laravel

Laravel applications typically follow this structure:

  • 70% unit tests
  • 30% feature tests

Unit tests catch logic bugs, while feature tests catch system-wide issues.


Using Factories in Feature Tests

Laravel’s model factories help generate test data.

$user = User::factory()->create();

You can then act as the user:

$this->actingAs($user)->get('/dashboard')
 ->assertStatus(200);

Example: Unit Test for a Calculator Service

public function test_it_calculates_percentage()
{
$service = new PercentageService;
$this->assertEquals(20, $service->calculate(100, 0.2));
}

This is fast and isolated.


Example: Feature Test for a Controller

public function test_product_creation()
{
$response = $this->post('/products', [
    'name' => 'Laptop',
    'price' => 1500
]);
$response->assertStatus(302);
$this->assertDatabaseHas('products', ['name' => 'Laptop']);
}

This checks the entire workflow.


Performance Considerations

To improve performance:

  • Run unit tests often
  • Run feature tests before deployments
  • Keep feature tests minimal
  • Use caching for factories
  • Use parallel testing

Parallel Testing in Laravel

Run tests in parallel:

php artisan test --parallel

Great for large test suites.


Debugging Unit Test Failures

Unit test failures are easy to debug because the logic is isolated.

Example failure:

Expected 10, got 12

Debugging Feature Test Failures

Feature tests can fail due to multiple layers.

Common failure sources:

  • Middleware
  • Routes
  • Database
  • Validation
  • Authentication
  • Permissions

Debugging may require checking logs or printing response output:

$response->dump();

Coverage Achieved by Unit Tests vs Feature Tests

Unit Tests:

  • Excellent code coverage
  • Poor system coverage

Feature Tests:

  • Excellent system coverage
  • Lower internal logic coverage

Both are necessary.


Testing Models With Unit Tests

Models often include logic worth testing:

public function test_full_name_accessor()
{
$user = new User(['first_name' => 'John', 'last_name' => 'Doe']);
$this->assertEquals('John Doe', $user->full_name);
}

Testing Controllers With Feature Tests

Example:

public function test_dashboard_requires_authentication()
{
$this->get('/dashboard')->assertRedirect('/login');
}

Combining Both Test Types in Real Projects

Example flow:

Unit Test:

  • Product price calculation logic

Feature Test:

  • User creates a product
  • Product is stored correctly
  • Price logic applied

Both tests ensure stability.


Best Practices for Using Unit Tests and Feature Tests

  • Write unit tests for all critical logic
  • Write feature tests for all user flows
  • Keep feature tests readable
  • Use factories to generate data
  • Mock external APIs in unit tests
  • Avoid over-mocking
  • Test behavior, not implementation
  • Use descriptive test names
  • Separate tests logically
  • Keep test suites fast

Common Mistakes Developers Make

  • Using feature tests for everything
  • Using unit tests for full workflows
  • Testing implementation details instead of behavior
  • Forgetting database migrations in tests
  • Not using factories
  • Not using the RefreshDatabase trait
  • Using real API calls in tests

Example: Combined Testing Strategy for a Registration System

Unit Tests:

  • Validate password rules
  • Validate email formatting
  • Test User model accessors

Feature Tests:

  • Test user registration
  • Test email verification flow
  • Test login functionality

Comments

Leave a Reply

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