241 lines
7.5 KiB
Markdown
241 lines
7.5 KiB
Markdown
---
|
|
title: 'ATDC: Lesson 10 - Mocking services'
|
|
permalink: /atdc/10-mocking-services
|
|
---
|
|
|
|
{% block head_meta %}
|
|
<meta name="robots" content="noindex">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
In this final lesson, let's continue looking at unit testing, mocking and how we can mock Drupal's services that are dependencies for our classes.
|
|
|
|
In lesson 5, you used Kernel tests to ensure the correct posts were returned from `PostNodeRepository` and in the correct order.
|
|
|
|
Let's see how that would look as a unit test.
|
|
|
|
## Creating the test
|
|
|
|
Create a new test, `PostNodeRepositoryUnitTest` and, for now, just create a new `PostNodeRepository`:
|
|
|
|
```language-php
|
|
<?php
|
|
|
|
// web/modules/custom/atdc/tests/src/Unit/PostNodeRepositoryUnitTest.php
|
|
|
|
final class PostNodeRepositoryUnitTest extends UnitTestCase {
|
|
|
|
/** @test */
|
|
public function it_returns_posts(): void {
|
|
$repository = new PostNodeRepository();
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
Running the test will give this error:
|
|
|
|
> ArgumentCountError: Too few arguments to function Drupal\atdc\Repository\PostNodeRepository::__construct(), 0 passed
|
|
|
|
This is expected as `PostNodeRepository` has a dependency - the `EntityTypeManager`.
|
|
|
|
But, as this is a unit test, you can't get the Repository from the service container, and you need to instantiate it as well as any dependencies.
|
|
|
|
Try to fix this by creating a new `EntityTypeManager` and injecting it into the constructor:
|
|
|
|
```language-php
|
|
$repository = new PostNodeRepository(
|
|
new EntityTypeManager(),
|
|
);
|
|
```
|
|
|
|
Running the tests again will give you a similar error:
|
|
|
|
> ArgumentCountError: Too few arguments to function Drupal\Core\Entity\EntityTypeManager::__construct(), 0 passed
|
|
|
|
`EntityTypeManager` also has dependencies that need to be injected, and they may have dependencies.
|
|
|
|
Instead of doing this manually, let's start using mocks.
|
|
|
|
## Adding the first mock
|
|
|
|
Add `use Drupal\Core\Entity\EntityTypeManagerInterface;` and create a mock to use instead of the manually created version.
|
|
|
|
```language-php
|
|
$repository = new PostNodeRepository(
|
|
$this->createMock(EntityTypeManagerInterface::class),
|
|
);
|
|
```
|
|
|
|
As the mock implements `EntityTypeManagerInterface`, this will fix the failure, and the test will continue.
|
|
|
|
## Getting the posts
|
|
|
|
Next, try to get the posts from the Repository:
|
|
|
|
```language-php
|
|
$repository->findAll();
|
|
```
|
|
|
|
Instead of returning a result, this also results in an error:
|
|
|
|
> Error: Call to a member function loadByProperties() on null
|
|
|
|
Within `PostNodeRepository`, we use the `getStorage()` on `EntityTypeManager` to get the node storage, which is an instance of `EntityStorageInterface`.
|
|
|
|
For the test to work, this needs to be mocked too and returned from the `getStorage()` method.
|
|
|
|
Create a mock of `EntityStorageInterface`, which will be used as the node storage:
|
|
|
|
```language-php
|
|
$nodeStorage = $this->createMock(EntityStorageInterface::class);
|
|
```
|
|
|
|
Next, this needs to be returns from the mock `EntityTypeManager`.
|
|
|
|
To do this, specify that the `getStorage()` method when called with the value `node`, will return the mocked node storage:
|
|
|
|
```language-php
|
|
$entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
|
|
$entityTypeManager->method('getStorage')->with('node')->willReturn($nodeStorage);
|
|
|
|
$repository = new PostNodeRepository($entityTypeManager);
|
|
```
|
|
|
|
This will then be returned instead of `NULL` and fix the error.
|
|
|
|
## Creating nodes and adding assertions
|
|
|
|
Next, let's create and return the nodes we need and add the assertions.
|
|
|
|
You'll need to use a mock for each node and set what each method needs to return.
|
|
|
|
The same as the Kernel test, set a title for each post with different created times.
|
|
|
|
```language-php
|
|
$node1 = $this->createMock(NodeInterface::class);
|
|
$node1->method('bundle')->willReturn('post');
|
|
$node1->method('getCreatedTime')->willReturn(strtotime('-1 week'));
|
|
$node1->method('label')->willReturn('Post one');
|
|
|
|
$node2 = $this->createMock(NodeInterface::class);
|
|
$node2->method('bundle')->willReturn('post');
|
|
$node2->method('getCreatedTime')->willReturn(strtotime('-8 days'));
|
|
$node2->method('label')->willReturn('Post two');
|
|
|
|
$node3 = $this->createMock(NodeInterface::class);
|
|
$node3->method('bundle')->willReturn('post');
|
|
$node3->method('getCreatedTime')->willReturn(strtotime('yesterday'));
|
|
$node3->method('label')->willReturn('Post three');
|
|
```
|
|
|
|
Then, specify the `loadByProperties` method should return the posts.
|
|
|
|
```language-php
|
|
$nodeStorage->method('loadByProperties')->willReturn([
|
|
$node1,
|
|
$node2,
|
|
$node3,
|
|
]);
|
|
```
|
|
|
|
Finally, add some assertions that the nodes returned are the correct ones and in the correct order:
|
|
|
|
```language-php
|
|
$posts = $repository->findAll();
|
|
|
|
self::assertContainsOnlyInstancesOf(NodeInterface::class, $posts);
|
|
|
|
$titles = array_map(
|
|
fn (NodeInterface $node) => $node->label(),
|
|
$posts,
|
|
);
|
|
|
|
self::assertCount(3, $titles);
|
|
self::assertSame(
|
|
['Post two', 'Post one', 'Post three'],
|
|
$titles,
|
|
);
|
|
```
|
|
|
|
As the assertions should match the returned values, this test should now pass.
|
|
|
|
This is testing the same thing as the kernel test, but it's your preference which way you prefer.
|
|
|
|
## Conclusion
|
|
|
|
Hopefully, if you run your whole testsuite, you should see output like this:
|
|
|
|
```language-plain
|
|
PHPUnit 9.6.15 by Sebastian Bergmann and contributors.
|
|
|
|
......... 9 / 9 (100%)
|
|
|
|
Time: 00:07.676, Memory: 10.00 MB
|
|
```
|
|
|
|
Or, if you use `--testdox`, output like this:
|
|
|
|
```language-plain
|
|
PHPUnit 9.6.15 by Sebastian Bergmann and contributors.
|
|
|
|
Blog Page (Drupal\Tests\atdc\Functional\BlogPage)
|
|
✔ Blog page
|
|
✔ Posts are visible
|
|
✔ Only published nodes are shown
|
|
✔ Only post nodes are shown
|
|
|
|
Post Builder (Drupal\Tests\atdc\Kernel\Builder\PostBuilder)
|
|
✔ It returns a published post
|
|
✔ It returns an unpublished post
|
|
✔ It returns a post with tags
|
|
|
|
Post Node Repository (Drupal\Tests\atdc\Kernel\PostNodeRepository)
|
|
✔ Posts are returned by created date
|
|
|
|
Post Node Repository Unit (Drupal\Tests\atdc\Unit\PostNodeRepositoryUnit)
|
|
✔ It returns posts
|
|
|
|
Time: 00:07.097, Memory: 10.00 MB
|
|
|
|
OK (9 tests, 71 assertions)
|
|
```
|
|
|
|
Everything should be passing, and your testsuite should have a combination of different types of tests.
|
|
|
|
In this course, you've learned:
|
|
|
|
- How to configure Drupal and PHPUnit to run automated tests.
|
|
- How to write functional, kernel and unit tests.
|
|
- How to create data, such as node types, content and users within tests.
|
|
- How to manage configuration using test-specific modules.
|
|
- How to write unit tests and use mocks.
|
|
- Some small PHP tips and tricks, such as promoted constructor properties and the `@test` and `@testdox` parameters in PHPUnit.
|
|
|
|
I couldn't cover everything in a short email course, but I hope it was useful.
|
|
|
|
## Questions and feedback
|
|
|
|
Thank you for taking my Introduction to Automated Testing in Drupal email course.
|
|
|
|
I'd appreciate any feedback, so if you wouldn't mind, press reply and let me know what you thought of the course.
|
|
|
|
Also, I'd love to know your next steps are and what I can do to help.
|
|
|
|
You can register for my [Daily Email list][daily] to get daily software development emails and updates about future products and courses or see when the next date is for my [online Drupal testing workshop][dto].
|
|
|
|
I also offer private workshops and talks for development teams, [1-on-1 consulting calls][call] and [pair programming sessions][pair], [development team coaching][team] and [Drupal development subscriptions][subscription].
|
|
|
|
Happy testing!
|
|
|
|
Oliver
|
|
|
|
[call]: {{site.url}}/call
|
|
[daily]: {{site.url}}/daily
|
|
[dto]: {{site.url}}/dto
|
|
[pair]: {{site.url}}/pair
|
|
[podcast]: {{site.url}}/podcast
|
|
[subscription]: {{site.url}}/subscription
|
|
[team]: {{site.url}}/team-coaching
|
|
{% endblock %}
|