Back to Blog
Lesson 17 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20263 min read

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.

LaravelTestingPHPUnitMockeryDependency InjectionUnit Testingphpbackend

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.

PHP
public 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.

PHP
public 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

  1. Open your existing TaskController from Service-Oriented Task Management.
  2. Create a new test file: tests/Feature/TaskControllerTest.php.
  3. Write a test that mocks your TaskService to return an empty array when listTasks() is called.
  4. Assert that the controller returns a 200 OK with 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 the tearDown method 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().

Similar Posts