Mocking Services and Repositories in Laravel Tests
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.
The Problem with Real Dependencies
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.
Mocking Repositories in Controllers
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'); }
Simulating Service Failures
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); }
Hands-on Exercise
- Open your existing
TaskControllerfrom Service-Oriented Task Management. - Create a new test file:
tests/Feature/TaskControllerTest.php. - Write a test that mocks your
TaskServiceto return an empty array whenlistTasks()is called. - Assert that the controller returns a
200 OKwith an empty data collection.
Common Pitfalls
- Over-mocking: Don't mock everything. If you mock the repository, the service, and the controller, you're testing your own mocks, not your code. Use mocks for boundaries (external APIs, heavy logic, or complex DB operations).
- Interface Mismatch: If you rename a method in your Repository Interface but forget to update the mock expectations, your tests will pass (or fail) silently. Always mock against the interface, not the concrete class.
- Forgetting to Reset: Laravel automatically resets the container between tests, but if you're using
Mockery::close()manually, ensure it's in thetearDownmethod to prevent memory leaks.
Recap
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().
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.