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
Leave a Reply