Add ATDC course pages
This commit is contained in:
parent
11ae0f9fc7
commit
72c3d50cf7
11 changed files with 2178 additions and 0 deletions
174
source/_pages/atdc/9.md
Normal file
174
source/_pages/atdc/9.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
title: 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue