oliverdavies.uk/source/_pages/atdc/10.md

241 lines
7.5 KiB
Markdown
Raw Normal View History

2024-02-07 22:25:53 +00:00
---
2024-02-08 00:00:16 +00:00
title: 'ATDC: Lesson 10 - Mocking services'
2024-02-07 22:25:53 +00:00
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
2024-02-07 22:25:53 +00:00
<?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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
$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
2024-02-07 22:25:53 +00:00
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
2024-02-07 22:25:53 +00:00
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 %}