Master dependency injection and mocking to isolate your controllers. Learn to simulate failures and test your business logic without hitting the database.
Previously in this course, we discussed Feature Testing Fundamentals, where we wrote end-to-end tests that hit the database. While those are vital for verifying the entire stack, they can become slow and fragile. Today, we're adding mocking to our toolkit, allowing us to isolate controllers from their dependencies, simulate edge cases, and speed up our test suite.
When you test a controller, you often trigger a chain reaction: the controller calls a service, the service calls a repository, and the repository hits the database. If your test fails, it might be because of a database constraint, a missing record, or a bug in the service layer—not the controller itself.
By leveraging dependency injection, we can swap these real implementations for "mocks" or "stubs" during testing. This allows us to verify that a controller correctly handles a 200 OK or a 500 Internal Server Error without actually executing the underlying business logic.
Imagine a TaskController that uses a TaskRepositoryInterface to fetch data. We want to test that the controller returns a JSON response when a task is found, without needing to seed the database.
In Laravel, we use the instance() or mock() methods on the service container to override bindings during a test.
PHPpublic function test_can_show_task_details() { #6A9955">// Create a mock of the repository $mock = Mockery::mock(TaskRepositoryInterface::class); #6A9955">// Define the expected behavior $mock->shouldReceive('find') ->once() ->with(123) ->andReturn(new Task(['title' => 'Test Task'])); #6A9955">// Swap the binding in the container $this->instance(TaskRepositoryInterface::class, $mock); #6A9955">// Call the endpoint $response = $this->getJson('/api/tasks/123'); #6A9955">// Assert the response $response->assertStatus(200) ->assertJsonPath('data.title', 'Test Task'); }
One of the biggest benefits of mocking is the ability to trigger "impossible" scenarios, like a database connection timeout or a service-level exception, to see how your app handles them.
If your service throws a custom exception, you don't want to manually break your database to test the error handling. Instead, you instruct your mock to throw the exception.
PHPpublic function test_handles_service_failure_gracefully() { $mock = Mockery::mock(TaskService::class); #6A9955">// Simulate a service failure $mock->shouldReceive('createTask') ->once() ->andThrow(new \Exception('Service unavailable')); $this->instance(TaskService::class, $mock); $response = $this->postJson('/api/tasks', ['title' => 'New Task']); #6A9955">// Assert that we get a 500 or a specific error response $response->assertStatus(500); }
TaskController from Service-Oriented Task Management.tests/Feature/TaskControllerTest.php.TaskService to return an empty array when listTasks() is called.200 OK with an empty data collection.Mockery::close() manually, ensure it's in the tearDown method to prevent memory leaks.Mocking is a powerful way to isolate components and test edge cases that are difficult to trigger with real data. By injecting interfaces and swapping them with mocks during tests, you ensure your controllers handle success and failure scenarios predictably. This approach complements your broader Repository Pattern implementation, creating a robust, testable architecture.
Up next: We'll look at how to verify side effects like background emails and notifications using Event::fake() and Queue::fake().
Stop hitting live APIs in your tests. Learn how to use test doubles, stubs, and mocks to isolate your Laravel application logic for faster, reliable tests.
Read moreMaster Testing DDD components in Laravel. Learn to mock external services, isolate domain logic, and write reliable PHPUnit tests for your Action classes.
Mocking Services and Repositories in Tests