Mocking in Drupal Tests
Mocking is an essential technique in unit and functional testing that allows developers to isolate the code being tested by replacing dependencies with controlled substitutes. In Drupal development, where components are highly interconnected, effective mocking is crucial for writing reliable, fast, and maintainable tests.
Why Mock in Drupal Tests?
- Isolation: Mocking allows you to test specific functionality without relying on external systems or complex dependencies.
- Speed: Tests with mocks run faster than those that need to set up and tear down actual database connections or services.
- Control: Mocks give you precise control over the behaviour of dependencies, allowing you to test edge cases and error conditions.
- Independence: Your tests become independent of implementation details of other components, making them more resilient to changes elsewhere in the codebase.
Mocking Tools and Frameworks for Drupal
PHPUnit
Drupal 8+ uses PHPUnit as its primary testing framework, which provides built-in mocking capabilities:
createMock(): Creates a basic mock objectgetMockBuilder(): Offers more advanced configuration optionsgetMockForAbstractClass(): Creates mocks for abstract classesgetMockForTrait(): Creates mocks for traits
Prophecy
Prophecy is a more modern mocking framework that's integrated with PHPUnit and offers a more expressive syntax:
$prophet = new \Prophecy\Prophet();
$service = $prophet->prophesize(MyService::class);
$service->doSomething('input')->willReturn('mocked output');Drupal Test Classes
Drupal provides several test base classes that help with mocking common Drupal services:
- UnitTestCase: Base class for unit tests with mocking support
- KernelTestBase: For tests requiring some Drupal services but not a full bootstrap
- BrowserTestBase: For functional tests with a full Drupal instance
Common Mocking Techniques in Drupal
1. Mocking Services
Services are a primary target for mocking in Drupal tests. Here's how to mock a service:
// Create a mock service
$service = $this->createMock(MyServiceInterface::class);
$service->method('getData')
->willReturn(['mocked' => 'data']);
// Replace the service in the container
$this->container->set('my_service', $service);2. Mocking the Entity API
Entity mocking is common in Drupal tests:
// Mock a node entity
$node = $this->createMock(NodeInterface::class);
$node->expects($this->once())
->method('getTitle')
->willReturn('Mocked Title');3. Mocking the Database Layer
For unit tests, you often need to mock database interactions:
// Mock a database connection
$database = $this->createMock(Connection::class);
$statement = $this->createMock(\PDOStatement::class);
$statement->method('fetchAll')
->willReturn([['nid' => 1, 'title' => 'Test']]);
$database->method('query')
->willReturn($statement);4. Mocking HTTP Requests
For external API interactions:
// Mock the HTTP client
$response = $this->createMock(ResponseInterface::class);
$response->method('getBody')
->willReturn(json_encode(['status' => 'success']));
$client = $this->createMock(ClientInterface::class);
$client->method('request')
->willReturn($response);Practical Examples
Example 1: Mocking a Custom Service
/**
* @coversDefaultClass \Drupal\my_module\Service\DataProcessor
* @group my_module
*/
class DataProcessorTest extends UnitTestCase {
/**
* Tests the process method.
*/
public function testProcess() {
// Create mocks for dependencies
$logger = $this->createMock(LoggerInterface::class);
$config = $this->createMock(ConfigFactoryInterface::class);
// Configure the config mock
$configObject = $this->createMock(ImmutableConfig::class);
$configObject->method('get')
->willReturn(['setting' => 'value']);
$config->method('get')
->willReturn($configObject);
// Create the service with mocked dependencies
$processor = new DataProcessor($logger, $config);
// Test the service
$result = $processor->process(['input' => 'data']);
$this->assertEquals('expected output', $result);
}
}
Example 2: Mocking the Entity Type Manager
/**
* @coversDefaultClass \Drupal\my_module\NodeStatistics
* @group my_module
*/
class NodeStatisticsTest extends UnitTestCase {
/**
* Tests the getPublishedCount method.
*/
public function testGetPublishedCount() {
// Create a mock entity type manager
$entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
// Create a mock node storage
$nodeStorage = $this->createMock(EntityStorageInterface::class);
// Configure the entity type manager to return our mock storage
$entityTypeManager->method('getStorage')
->with('node')
->willReturn($nodeStorage);
// Configure the node storage to return a count
$nodeStorage->method('getQuery')
->willReturnCallback(function() {
$query = $this->createMock(QueryInterface::class);
$query->method('condition')->willReturnSelf();
$query->method('count')->willReturnSelf();
$query->method('execute')->willReturn(42);
return $query;
});
// Create the service with mocked dependencies
$statistics = new NodeStatistics($entityTypeManager);
// Test the method
$count = $statistics->getPublishedCount();
$this->assertEquals(42, $count);
}
}Best Practices for Mocking in Drupal
- Mock interfaces, not implementations: Whenever possible, mock interfaces rather than concrete classes to ensure your tests aren't tightly coupled to specific implementations.
- Only mock what you need: Don't mock everything. Mock only the dependencies that are directly relevant to the test.
- Use test doubles appropriately:
- Stubs for providing canned answers
- Mocks for verifying interactions
- Keep mocks simple: Overly complex mocks can make tests hard to understand and maintain.
- Test the contract, not the implementation: Focus on testing the expected behaviour, not the internal implementation details.
- Use Drupal's testing infrastructure: Take advantage of Drupal's testing classes and traits that provide pre-configured mocks for common Drupal services.
- Consider dependency injection: Design your code with dependency injection in mind to make it more testable and easier to mock dependencies.
Effective mocking is a crucial skill for Drupal developers who want to write robust, maintainable tests. By isolating components and controlling their dependencies, you can create tests that are faster, more reliable, and better at catching regressions.
Drupal's modern architecture, with its emphasis on services and dependency injection, makes it particularly well-suited for mocking techniques. Whether you're writing unit tests, kernel tests, or functional tests, understanding how to properly mock Drupal's components will help you build a more reliable test suite and, ultimately, a more stable application.
Remember that while mocking is powerful, it's just one tool in your testing toolkit. Combine it with other testing approaches like integration tests and end-to-end tests to ensure comprehensive coverage of your Drupal application.
Additional Resources:
- Drupal.org Testing Guide
- Drupal 10 Testing API Overview
- PHPUnit Documentation
- Drupal Test Examples module - Contains examples of different types of tests
- Drupal Core Tests - Real-world examples of tests in Drupal core