7.5 KiB
title | permalink |
---|---|
ATDC: Lesson 10 - Mocking services | /atdc/10-mocking-services |
{% block head_meta %}
{% 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
:
<?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:
$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.
$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:
$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:
$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:
$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.
$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.
$nodeStorage->method('loadByProperties')->willReturn([
$node1,
$node2,
$node3,
]);
Finally, add some assertions that the nodes returned are the correct ones and in the correct order:
$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:
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:
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 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.
I also offer private workshops and talks for development teams, 1-on-1 consulting calls and pair programming sessions, development team coaching and Drupal development subscriptions.
Happy testing!
Oliver
{% endblock %}