175 lines
5 KiB
Markdown
175 lines
5 KiB
Markdown
---
|
|
title: 'ATDC: Lesson 9 - Introducing Unit Tests'
|
|
permalink: /atdc/9-introducing-unit-tests
|
|
---
|
|
|
|
{% block head_meta %}
|
|
<meta name="robots" content="noindex">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
Unit tests are the last type of test we'll cover in this course.
|
|
|
|
Similar to Kernel tests, in a Unit test, there is no browser to make HTTP requests with, but also no database or service container, so everything needs to be created from scratch.
|
|
|
|
I do outside-in testing and start with Functional and Kernel tests, so I don't tend to write many Unit tests.
|
|
|
|
I prefer to use real objects as opposed to mocks and have seen tests that create mocks and only test the mock and not the rest of the code.
|
|
|
|
I've also seen Unit tests that are very tightly coupled to the implementation, such as asserting a method is only called a certain number of times. This makes the code harder to refactor and could result in a test failing when its functionality is working.
|
|
|
|
## Your first Unit test
|
|
|
|
Based on what you've learned so far, let's write a Unit test that we'd expect to pass:
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace Drupal\Tests\atdc\Unit;
|
|
|
|
use Drupal\node\Entity\Node;
|
|
use Drupal\node\NodeInterface;
|
|
use Drupal\Tests\UnitTestCase;
|
|
|
|
final class PostWrapperTest extends UnitTestCase {
|
|
|
|
/** @test */
|
|
public function it_wraps_a_post(): void {
|
|
$node = Node::create(
|
|
entity_type: 'post',
|
|
values: [],
|
|
);
|
|
|
|
self::assertInstanceOf(NodeInterface::class, $node);
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
This test is within the `tests/src/Unit` directory and the equivalent namespace and extends the `UnitTestCase` class.
|
|
|
|
However, when you run the test, you'll get an error:
|
|
|
|
> Drupal\Core\DependencyInjection\ContainerNotInitializedException: \Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container.
|
|
|
|
In a Unit test, there is no database or service container, so you need to use mocks instead.
|
|
|
|
Update the test to create a mock version of `NodeInterface` instead.
|
|
|
|
As the mock an instance of `NodeInterface`, it satisfies the assertion and the test passes.
|
|
|
|
```php
|
|
/** @test */
|
|
public function it_wraps_a_post(): void {
|
|
$node = $this->createMock(NodeInterface::class);
|
|
|
|
self::assertInstanceOf(NodeInterface::class, $node);
|
|
}
|
|
```
|
|
|
|
Next, add an assertion to ensure the bundle is correct:
|
|
|
|
```php
|
|
self::assertSame('post', $node->bundle());
|
|
```
|
|
|
|
This will fail with this error:
|
|
|
|
> Failed asserting that null is identical to 'post'.
|
|
|
|
Because you're using a mock, all methods will return `NULL`.
|
|
|
|
To get this to pass, you need to define what `$this->bundle()` will return:
|
|
|
|
```php
|
|
$node->method('bundle')->willReturn('post');
|
|
```
|
|
|
|
However, this leads us to the situation I described, where you're only testing what's defined in the mock and not any valuable logic.
|
|
|
|
Let's improve this by introducing a `PostWrapper`.
|
|
|
|
## Wrapping posts
|
|
|
|
Let's create a `PostWrapper` class that wraps a post node and has some methods that return specific values from it.
|
|
|
|
Within the test, instantiate a new `Postwrapper` class that takes the node as an argument.
|
|
|
|
Then, add an assertion that a `getType()` method should return `post`.
|
|
|
|
```php
|
|
$wrapper = new PostWrapper($node);
|
|
|
|
self::assertSame('post', $wrapper->getType());
|
|
```
|
|
|
|
Next, create a `PostWrapper` class with the `getType()` method:
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace Drupal\atdc;
|
|
|
|
use Drupal\node\NodeInterface;
|
|
|
|
final class PostWrapper {
|
|
|
|
public function __construct(private NodeInterface $post) {
|
|
}
|
|
|
|
public function getType(): string {
|
|
return $this->post->bundle();
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
Now the test isn't testing the mock data directly, but the mock data is used within the `PostWrapper` to assert it is returning the expected value.
|
|
|
|
## Not wrapping a page
|
|
|
|
We've tested that the `PostWrapper` works with post nodes, but let's also ensure it won't work with other node types.
|
|
|
|
Create a new test that creates a mock node and returns `page` as the bundle:
|
|
|
|
```php
|
|
/**
|
|
* @test
|
|
* @testdox It can't wrap a page
|
|
*/
|
|
public function it_cant_wrap_a_page(): void {
|
|
self::expectException(\InvalidArgumentException::class);
|
|
|
|
$node = $this->createMock(NodeInterface::class);
|
|
$node->method('bundle')->willReturn('page');
|
|
|
|
new PostWrapper($node);
|
|
}
|
|
```
|
|
|
|
Before creating a new `PostWrapper`, assert that an `InvalidArgumentException` should be thrown. As no assertion is thrown, this test should fail:
|
|
|
|
> Failed asserting that exception of type "InvalidArgumentException" is thrown.
|
|
|
|
To fix it, within the constructor for `PostWrapper`, check the bundle and throw the expected Exception if the bundle is not `post`:
|
|
|
|
```php
|
|
/**
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public function __construct(private NodeInterface $post) {
|
|
if ($post->bundle() !== 'post') {
|
|
throw new \InvalidArgumentException();
|
|
}
|
|
}
|
|
```
|
|
|
|
Again, instead of making assertions against the mock data directly, it's used to provide known data to the classes that need it.
|
|
|
|
## Conclusion
|
|
|
|
In this lesson, I introduced unit testing and mocking.
|
|
|
|
In tomorrow's lesson, the final one in this course, I'll show you an example of how to use mocks with Service classes.
|
|
{% endblock %}
|