2020-03-13 11:17:16 +00:00
# Workshop: Automated Testing and Test Driven Development in Drupal 8
## Table of Contents
- [Introduction ](#introduction )
2020-03-13 17:01:23 +00:00
- [Contribution ](#contribution )
- [Feedback ](#feedback )
2020-03-13 11:17:16 +00:00
- [Creating a new Drupal project with Composer ](#creating-a-new-drupal-project-with-composer )
2020-11-13 17:25:23 +00:00
- [Using DDEV for local development ](#using-ddev-for-local-development )
- [Using the PHP web server for local development ](#using-the-php-web-server-for-local-development )
2020-03-13 11:17:16 +00:00
- [The different types of available tests ](#the-different-types-of-available-tests )
- [Different approaches to testing ](#different-approaches-to-testing )
2020-03-19 11:02:38 +00:00
- [The structure of a test ](#the-structure-of-a-test )
2020-03-13 11:17:16 +00:00
- [What is Test Driven Development? ](#what-is-test-driven-development )
- [Acceptance criteria ](#acceptance-criteria )
- [Step 0: Preparation ](#step-0-preparation )
- [Step 1: Writing your first test ](#step-1-writing-your-first-test )
- [1a. Create the module directory ](#1a-create-the-module-directory )
- [1b. Create an .info.yml file for the module ](#1b-create-an-infoyml-file-for-the-module )
- [1c. Create the directory structure for tests ](#1c-create-the-directory-structure-for-tests )
- [1d. Create your first test case ](#1d-create-your-first-test-case )
- [1e. Preparing PHPUnit ](#1e-preparing-phpunit )
- [1f. Running the tests ](#1f-running-the-tests )
- [1g. (Optional) Running tests via a Composer script ](#1g-optional-running-tests-via-a-composer-script )
- [Step 2: Adding more test methods ](#step-2-adding-more-test-methods )
- [2a. Ensure that anonymous users cannot access admin pages ](#2a-ensure-that-anonymous-users-cannot-access-admin-pages )
- [2b. Ensure that administrators can access admin pages ](#2b-ensure-that-administrators-can-access-admin-pages )
- [Step 3: Building a blog ](#step-3-building-a-blog )
- [3a. Anonymous users should be able to view the blog page ](#3a-anonymous-users-should-be-able-to-view-the-blog-page )
- [3b. Add a route for the blog page ](#3b-add-a-route-for-the-blog-page )
- [3c. Fix permission error ](#3c-fix-permission-error )
- [3d. Add the BlogPageController ](#3d-add-the-blogpagecontroller )
- [3e. Refactor, add more assertions ](#3e-refactor-add-more-assertions )
- [Step 4: Getting blog posts ](#step-4-getting-blog-posts )
- [4a Creating our first kernel test ](#4a-creating-our-first-kernel-test )
- [4b. Starting with an assertion ](#4b-starting-with-an-assertion )
- [4c. Trying to use the ArticleRepository ](#4c-trying-to-use-the-articlerepository )
- [4d. Creating an article repository ](#4d-creating-an-article-repository )
- [4e. Adding the getAll() method ](#4e-adding-the-getall-method )
- [4f. Building up the ArticleRepository ](#4f-building-up-the-articlerepository )
- [4g. Adding articles ](#4g-adding-articles )
- [4h. Making this test less brittle ](#4h-making-this-test-less-brittle )
- [4i. Ensuring that only published articles are returned ](#4i-ensuring-that-only-published-articles-are-returned )
- [4j. Ensuring that articles are returned in the correct order ](#4j-ensuring-that-articles-are-returned-in-the-correct-order )
- [4k. Linking up the repository to the BlogPageController ](#4k-linking-up-the-repository-to-the-blogpagecontroller )
- [Step 5: Wrapping up with unit tests ](#step-5-wrapping-up-with-unit-tests )
- [5a. Creating an ArticleWrapper ](#5a-creating-an-articlewrapper )
- [5b. Ensure that we’ re only wrapping articles ](#5b-ensure-that-were-only-wrapping-articles )
- [5c. Determine if articles are publishable ](#5c-determine-if-articles-are-publishable )
2020-03-13 16:57:49 +00:00
## Contribution
2020-03-18 19:38:14 +00:00
Please feel free to create issues and/or submit pull requests to this repository whilst working through these instructions. Any contributions would be greatly appreciated!
2020-03-13 16:57:49 +00:00
## Feedback
2020-11-13 16:27:52 +00:00
Feedback would also be appreciated! You can contact me via < oliver @ odwd . uk > , [@opdavies on Twitter ](https://twitter.com/opdavies ) or `opdavies` on Drupal Slack.
2020-03-13 16:57:49 +00:00
2020-03-13 11:17:16 +00:00
## Introduction
## Creating a new Drupal project with Composer
2020-03-18 12:25:04 +00:00
If don’ t have Composer, visit < https: / / getcomposer . org / download > for instructions on how to install it on your computer.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
This assumes that Composer is installed globally and is available by running the `composer` command. Alternatively, you can download the phar file and run `php composer.phar` instead.
2020-03-13 11:17:16 +00:00
```bash
# Create a new Drupal project
composer create-project drupal/recommended-project my-project
cd my-project
# Add testing dependencies
composer require --dev drupal/core-dev
```
2020-03-18 12:25:04 +00:00
You should now have files present including `web/index.php` and `vendor/bin/phpunit` . Now you can start serving this site locally.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
**Note:** Development dependencies, including PHPUnit, should only be installed locally and should not be present on public servers. Doing so would present a security risk to your application.
2020-03-13 11:17:16 +00:00
2020-11-13 17:25:23 +00:00
## Using DDEV for local development
2020-03-13 11:17:16 +00:00
2020-11-13 17:25:23 +00:00
- Docker based development environment for PHP applications (Drupal, WordPress, Magento etc).
- More information at < https: // www . ddev . com > .
- Documentation at < https: // ddev . readthedocs . io > .
- Installation via Homebrew on Linux and macOS, and Chocolatey on Windows. More information at < https: // www . ddev . com / get-started > .
- Example at < https: // github . com / opdavies / workshop-drupal-automated-testing-code > .
2020-03-13 11:17:16 +00:00
2020-11-13 17:25:23 +00:00
To run PHPUnit within DDEV, we can prefix the command with `ddev exec` :
2020-03-13 11:17:16 +00:00
2020-11-13 17:25:23 +00:00
```
$ ddev exec vendor/bin/phpunit
```
## Using the PHP web server for local development
If you have all of [Drupal's required PHP extensions ](https://www.drupal.org/docs/system-requirements/php-requirements#extensions ) installed and would like better performance (particularly on macOS), you could use the PHP's local web server.
As we're going to use SQLite to run the tests, there's no need for a connection to a MySQL database or another service.
If you need to override any environment variables, you can do so before running the command:
```
$ SIMPLETEST_BASE_URL=http://localhost:8000 vendor/bin/phpunit
```
2020-03-19 11:06:39 +00:00
2020-03-13 11:17:16 +00:00
## The different types of available tests
2020-03-18 12:25:04 +00:00
- **Functional** (web, feature) - tests behaviour and functionality, makes HTTP requests to the webserver and has access to the database and other services via the service container. Slower to run.
2020-03-13 11:17:16 +00:00
- **FunctionalJavascript** - functional tests, but access to JavaScript.
2020-03-18 12:25:04 +00:00
- **Kernel** (integration) - no browser capabilities, has access to the database and other services but requires more configuration.
- **Unit** - no access to the database or service container, all dependencies need to be mocked. Fast to run.
2020-03-13 11:17:16 +00:00
## Different approaches to testing
2020-03-18 12:25:04 +00:00
- Inside-out (testing pyramid) - mostly unit tests, some integration tests, few functional tests.
- Outside-in (testing trophy) - mostly functional tests, some integration tests, few unit tests. More flexible, easier to refactor.
2020-03-13 11:17:16 +00:00
2020-03-19 11:02:38 +00:00
## The structure of a test
2020-03-13 11:17:16 +00:00
- **Arrange** - set up the environment. Create users, nodes, set up dependencies
- **Act** - perform an action
- **Assert** - verify that something happened
## What is Test Driven Development?
- Writing tests first
- Red, green, refactor loop
- Writing failing test first
- Write code to make the test pass
- Refactor when tests pass
- Three (simplified) rules of TDD:
- Write production code only to make a failing unit test pass.
- Write only enough of a unit test to fail.
- Write only enough production code to make the failing unit test pass.
- < http: // www . javiersaldana . com / articles / tech / refactoring-the-three-laws-of-tdd >
## Acceptance criteria
2020-03-18 12:25:04 +00:00
This module will be used to demonstrate how to take a test-driven approach to develop a module to the following acceptance criteria:
2020-03-13 11:17:16 +00:00
- As a site visitor
- I want to see a list of all published articles at `/blog`
- Ordered by post date, with the newest posts first
## Step 0: Preparation
2020-11-13 21:10:45 +00:00
To begin, we need the site to be running.
2020-03-13 11:17:16 +00:00
```bash
2020-11-13 21:10:45 +00:00
# Using DDEV
ddev start
2020-03-13 11:17:16 +00:00
2020-11-13 21:10:45 +00:00
# Using PHP's web server
php -S localhost:8000 -t web
2020-03-13 11:17:16 +00:00
```
2020-11-13 21:10:45 +00:00
You don’ t need to install Drupal. It just needs to be able to connect to the database.
2020-03-13 11:17:16 +00:00
## Step 1: Writing your first test
### 1a. Create the module directory
Create a directory for the new module:
```
2020-03-19 20:29:42 +00:00
mkdir -p web/modules/custom/my_module
2020-03-13 11:17:16 +00:00
```
### 1b. Create an `.info.yml` file for the module
2020-03-18 12:25:04 +00:00
In order to install the module, it needs an info.yml file that matches the name of the module (e.g. `my_module.info.yml` ).
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
Within the `my_module` directory, create the file and paste in the following content:
2020-03-13 11:17:16 +00:00
```yaml
name: My Module
type: module
core_version_requirement: ^8 || ^9
```
### 1c. Create the directory structure for tests
```bash
mkdir -p tests/src/Functional
```
### 1d. Create your first test case
```php
2020-11-13 22:16:36 +00:00
< ?php
2020-03-13 11:17:16 +00:00
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
2020-11-13 22:16:36 +00:00
class FrontPageTest extends BrowserTestBase {
2020-03-13 11:17:16 +00:00
2020-11-13 22:16:36 +00:00
protected static $modules = ['node', 'views'];
2020-03-13 11:17:16 +00:00
2020-11-13 22:16:36 +00:00
/** @test */
2020-03-13 11:17:16 +00:00
public function the_front_page_loads_for_anonymous_users() {
2020-11-13 22:16:36 +00:00
$this->config('system.site')
->set('page.front', '/node')
->save(TRUE);
2020-03-13 11:17:16 +00:00
$this->drupalGet('< front > ');
2020-11-13 22:16:36 +00:00
$assert = $this->assertSession();
$assert->statusCodeEquals(Response::HTTP_OK);
$assert->pageTextContains('Welcome to Drupal');
$assert->pageTextContains('No front page content has been created yet.');
2020-03-13 11:17:16 +00:00
}
}
```
Some things to note:
- The name of the class must match the filename.
- The filename must end with `Test.php` .
2020-03-18 12:25:04 +00:00
- The namespace is `Drupal\Tests\{module_name}` , followed by any additional directories (e.g. `Functional` ).
2020-03-13 11:17:16 +00:00
- Test methods must start with `test` , or use the `@test` annotation.
### 1e. Preparing PHPUnit
2020-03-18 12:25:04 +00:00
Firstly, we need to create a `phpunit.xml` file to configure PHPUnit. Core has a `phpunit.xml.dist` file that we can duplicate and edit.
2020-03-13 11:17:16 +00:00
```bash
cp web/core/phpunit.xml.dist web/core/phpunit.xml
```
2020-03-18 12:25:04 +00:00
Update the `SIMPLETEST_BASE_URL` value to be the address that the website is currently running on. This needs to be correct so that functional tests return the correct response codes, so ensure that any port numbers are correct and also that the site is correctly marked as HTTP or HTTPS.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
We also need to configure the database for Drupal to connect to and use when running functional and kernel tests. This could be your project’ s MySQL or PostgreSQL database with a table prefix, but in this case, we’ ll use a separate SQLite database.
2020-03-13 11:17:16 +00:00
```diff
- < env name = "SIMPLETEST_BASE_URL" value = "" />
2020-11-13 20:47:14 +00:00
+ < env name = "SIMPLETEST_BASE_URL" value = "http://localhost" / >
2020-03-13 11:17:16 +00:00
- < env name = "SIMPLETEST_DB" value = "" />
2020-11-13 20:47:14 +00:00
+ < env name = "SIMPLETEST_DB" value = "sqlite://localhost//dev/shm/test.sqlite" / >
2020-03-13 11:17:16 +00:00
```
### 1f. Running the tests
```bash
2020-11-13 22:16:36 +00:00
$ vendor/bin/phpunit modules/custom/my_module
2020-03-13 11:17:16 +00:00
```
2020-03-18 12:25:04 +00:00
You should see a summary of the number of tests and assertions that were run. This is the expected output if all of the tests pass:
2020-03-13 11:17:16 +00:00
2020-11-13 22:16:36 +00:00
> OK (1 test, 3 assertions)
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
If a test failed, the output would show the class and method name for the failing test, and give a summary of the failure.
2020-03-13 11:17:16 +00:00
2020-11-13 22:16:36 +00:00
> Drupal\Tests\my_module\Functional\FrontPageTest::the_front_page_loads_for_anonymous_users
2020-03-18 12:25:04 +00:00
> Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected.
2020-03-13 11:17:16 +00:00
Other useful options include `--stop-on-failure` , `--filter` and `--testdox` .
### 1g. (Optional) Running tests via a Composer script
2020-03-18 12:25:04 +00:00
To simplify running tests, the command could be simplified by [adding a script][composer scripts] to `composer.json` :
2020-03-13 11:17:16 +00:00
```json
"scripts": {
2020-11-13 22:19:57 +00:00
"test:phpunit": "phpunit --verbose --testdox --colors=always",
2020-03-13 11:17:16 +00:00
"test": [
"@test:phpunit"
]
}
```
2020-11-13 22:19:57 +00:00
This means that you can run just `ddev composer test:phpunit` or `ddev composer test` and it will execute the `phpunit` command.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
This approach can be useful if you want to run other commands in addition to PHPUnit such as PHPStan, PHP Code Sniffer or Drupal Check. Each command can be added to the script and they will each be executed.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
If needed, you can still pass additional arguments and options to the command by appending `--` followed by the arguments.
2020-03-13 11:17:16 +00:00
```bash
2020-11-13 21:10:45 +00:00
ddev composer test:phpunit -- modules/custom/my_module \
2020-03-13 11:17:16 +00:00
--stop-on-failure \
--filter=the_front_page_loads_for_anonymous_users
```
2020-11-13 21:10:45 +00:00
Locally, ensure that the command is prefixed with `ddev` so that it is run within the container. This ensures that the correct PHP version etc is used.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
[composer scripts]: https://getcomposer.org/doc/articles/scripts.md#writing-custom-commands
2020-03-13 11:17:16 +00:00
## Step 2: Adding more test methods
### 2a. Ensure that anonymous users cannot access admin pages
2020-03-18 12:25:04 +00:00
Now that we’ re sure that the front page loads correctly, lets also check anonymous users cannot access the administration area. This test is very similar to the previous one, though instead we’ re making a GET request to `/admin` and ensuring that the response code is 403 (forbidden).
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
As this functionality is provided by Drupal core by default, this should pass automatically.
2020-03-13 11:17:16 +00:00
```php
/** @test */
public function the_admin_page_is_not_accessible_to_anonymous_users() {
$this->drupalGet('admin');
2020-11-13 23:52:51 +00:00
$this->assertSession()->statusCodeEquals(Response::HTTP_FORBIDDEN);
2020-03-13 11:17:16 +00:00
}
```
### 2b. Ensure that administrators can access admin pages
Now let’ s check that an administrator user _can_ access the admin pages.
2020-03-18 12:25:04 +00:00
This introduces some new concepts. We need to create a user to begin with, and assign it some permissions. Because tests may be included within Drupal core a contributed module, permissions need to be added to users directly as modules won’ t know about roles that are specific to your site.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
The `BrowserTestBase` class gives access to a number of helper methods, including ones for creating and logging-in users (`createUser` and `drupalLogin` respectively). When creating a user, the first argument is an array of permission names to add. In this case, we can make the user an admin user by adding the `access administration pages` permission.
2020-03-13 11:17:16 +00:00
```php
/** @test */
public function the_admin_page_is_accessible_by_admin_users() {
$adminUser = $this->createUser([
'access administration pages',
]);
$this->drupalLogin($adminUser);
$this->drupalGet('admin');
2020-11-13 23:52:51 +00:00
$this->assertSession()->statusCodeEquals(Response::HTTP_OK);
2020-03-13 11:17:16 +00:00
}
```
2020-03-18 12:25:04 +00:00
Again, as this functionality is provided by Drupal core by default, this should pass. However, we can be confident that the test is doing what’ s needed by making it fail by removing or changing the assigned permissions, or not logging in the user before accessing the page.
2020-03-13 11:17:16 +00:00
## Step 3: Building a blog
### 3a. Anonymous users should be able to view the blog page
2020-03-18 12:25:04 +00:00
Let’ s start by building a blog page. This will look very similar to the admin page tests, but instead we’ ll be testing the `/blog` page.
2020-03-13 11:17:16 +00:00
```php
2020-11-13 23:53:13 +00:00
< ?php
2020-03-13 11:17:16 +00:00
// tests/src/Functional/BlogPageTest.php
namespace Drupal\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;
class BlogPageTest extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $modules = [
'my_module',
];
/** @test */
public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text() {
$this->drupalGet('blog');
$session = $this->assertSession();
$session->statusCodeEquals(Response::HTTP_OK);
}
}
```
2020-03-18 12:25:04 +00:00
This test will fail as there’ s no route for `/blog` and no View that generates that page. Because of this, the response code will be a 404 instead of the 200 that we want.
2020-03-13 11:17:16 +00:00
> Current response status code is 404, but 200 expected.
### 3b: Add a route for the blog page
2020-03-18 12:25:04 +00:00
We’ ll create a blog page using a custom route in the module. You could also do this with the Views module by creating a View with a page on that path, and exporting the configuration into the module’ s `config/install` directory.
2020-03-13 11:17:16 +00:00
To add a route, we need to create a `my_module.routing.yml` file.
```yaml
# my_module.routing.yml
blog.page:
path: /blog
defaults:
_controller: Drupal\my_module\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content
```
2020-03-18 12:25:04 +00:00
We set our path to `/blog` , specify the Controller to use and which permission the needs to have to access the page.
2020-03-13 11:17:16 +00:00
If we run the tests now, we get an access denied error (403 response).
> Current response status code is 403, but 200 expected.
### 3c: Fix permission error
2020-03-18 12:25:04 +00:00
Because we need to node module to be able to access the `access content` permission, we need to enable it within our tests.
2020-03-13 11:17:16 +00:00
We can do this by adding it to the `$modules` array within the test.
```diff
protected static $modules = [
+ 'node',
'my_module',
];
```
2020-03-18 12:25:04 +00:00
Now the error has changed, and is now returning a 500 response because we’ ve specified a Controller that doesn’ t exist.
2020-03-13 11:17:16 +00:00
> Current response status code is 500, but 200 expected.
### 3d: Add the BlogPageController
Let’ s create the `BlogPageController` .
```
mkdir src/Controller
```
2020-03-18 12:25:04 +00:00
Let’ s start by creating a minimal controller, that returns an empty render array. Because we didn’ t specify a method to use within the route file, we use PHP’ s `__invoke()` method.
2020-03-13 11:17:16 +00:00
```php
2020-11-13 23:53:13 +00:00
< ?php
2020-03-13 11:17:16 +00:00
// src/Controller/BlogPageController
namespace Drupal\my_module\Controller;
class BlogPageController {
public function __invoke(): array {
return [];
}
}
```
2020-03-18 12:25:04 +00:00
This is enough for the test to pass. Though it just returns an empty page, it now returns the correct 200 response code.
2020-03-13 11:17:16 +00:00
> OK (1 test, 3 assertions)
This is how the page looks in a browser:
![](docs/images/1.png)
### 3e: Refactor, add more assertions
2020-03-18 12:25:04 +00:00
Now that the test is passing, we can do some refactoring and make the test more robust by ensuring that the correct text is displayed.
2020-03-13 11:17:16 +00:00
```diff
public function the_blog_page_loads_for_anonymous_users_and_contains_the_right_text() {
$this->drupalGet('/blog');
$session = $this->assertSession();
$session->statusCodeEquals(Response::HTTP_OK);
+ $session->responseContains('< h1 > Blog< / h1 > ');
+ $session->pageTextContains('Welcome to my blog!');
}
```
2020-03-18 12:25:04 +00:00
The page title is created by the `_title` value within the routing file, but no page text has been added yet so this will cause the test to fail.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
> Behat\Mink\Exception\ResponseTextException: The text "Welcome to my blog!" was not found anywhere in the text of the current page.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
To fix this, we can return some text from the BlogPageController rather than an empty render array.
2020-03-13 11:17:16 +00:00
```diff
namespace Drupal\my_module\Controller;
+ use Drupal\Core\StringTranslation\StringTranslationTrait;
class BlogPageController {
+ use StringTranslationTrait;
public function __invoke(): array {
- return [];
+ return [
+ '#markup' => $this->t('Welcome to my blog!'),
+ ];
}
}
```
Now the tests will pass because we’ re returning the correct text.
> OK (1 test, 3 assertions)
![](docs/images/2.png)
## Step 4. Getting blog posts
### 4a Creating our first kernel test
2020-03-18 12:25:04 +00:00
We’ ll be using an ArticleRepository class to get the blog posts from the database, and this is also a good time to switch to writing kernel tests as we don’ t need to check any responses from the browser.
2020-03-13 11:17:16 +00:00
Within the tests directory, create a new `Kernel` directory.
```
mkdir tests/src/Kernel
```
And an `ArticleRepositoryTest` class.
```php
2020-11-13 23:53:13 +00:00
< ?php
2020-03-13 11:17:16 +00:00
// tests/src/Kernel/ArticleRepositoryTest.php
namespace Drupal\Tests\my_module\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
class ArticleRepositoryTest extends EntityKernelTestBase {
/** @test */
public function nodes_that_are_not_articles_are_not_returned() {
}
}
```
2020-03-18 12:25:04 +00:00
This test looks very similar to the functional ones that we’ ve already written, except it extends a different base class.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
This test is extending `EntityKernelTestBase` as we’ re working with entities and this performs some useful setup steps for us. There are different base classes that can be used though based on what you need - including `KernelTestBase` and `ConfigFormTestBase` .
2020-03-13 11:17:16 +00:00
### 4b. Starting with an assertion
Let’ s write this test 'backwards' and start with the 'assert' phase.
2020-03-18 12:25:04 +00:00
We know that the end objective for this test is to have 3 article nodes returned, so let’ s add that assertion first.
2020-03-13 11:17:16 +00:00
```diff
/** @test */
public function nodes_that_are_not_articles_are_not_returned() {
+ $this->assertCount(3, $articles);
}
```
2020-03-18 12:25:04 +00:00
As we aren’ t yet returning any articles, or even creating that variable, the test is going to fail, but we can move on to getting articles.
2020-03-13 11:17:16 +00:00
> Undefined variable: articles
### 4c. Trying to use the ArticleRepository
2020-03-18 12:25:04 +00:00
As the test name suggests, we’ re going to be retrieving the articles from an `ArticleRepository` service - though this doesn’ t exist yet, but let’ s let the tests tell us that.
2020-03-13 11:17:16 +00:00
```diff
/** @test */
public function nodes_that_are_not_articles_are_not_returned() {
+ $repository = $this->container->get(ArticleRepository::class);
+
$this->assertCount(3, $articles);
}
```
2020-03-18 12:25:04 +00:00
Because this is a kernel test, we have access to the container, and we can use it to retrieve our repository service.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
Running the test now gives us a different error, and tells us what the next step is:
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
> Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : You have requested a non-existent service "Drupal\Tests\my_module\Kernel\ArticleRepository".
2020-03-13 11:17:16 +00:00
### 4d. Creating an article repository
```bash
mkdir src/Repository
```
To begin with, let’ s create a basic `ArticleRepository` class.
```php
2020-11-13 23:53:13 +00:00
< ?php
2020-03-13 11:17:16 +00:00
// my_module/src/Repository/ArticleRepository
namespace Drupal\my_module\Repository;
class ArticleRepository {
}
```
2020-03-18 12:25:04 +00:00
We also need to create a `my_module.services.yml` file that we can use to register the `ArticleRepository` as a service.
2020-03-13 11:17:16 +00:00
```yaml
# my_module.services.yml
services:
Drupal\my_module\Repository\ArticleRepository: ~
```
2020-03-18 12:25:04 +00:00
Within `ArticleRepositoryTest` , we need to add the import statement for the `ArticleRepository` as well enabling the module.
2020-03-13 11:17:16 +00:00
```diff
+ use Drupal\my_module\Repository\ArticleRepository;
+ public static $modules = [
+ 'my_module',
+ ];
```
2020-03-18 12:25:04 +00:00
We’ ve fixed the missing repository, though we still haven’ t created the `$articles` variable or given it a value yet.
2020-03-13 11:17:16 +00:00
> Undefined variable: articles
### 4e. Adding the `getAll()` method
2020-03-18 12:25:04 +00:00
We’ ll use a `getAll()` method on the repository to retrieve the articles from the database, and use the value of this for the `$articles` variable:
2020-03-13 11:17:16 +00:00
```diff
$repository = $this->container->get(ArticleRepository::class);
+ $articles = $repository->getAll();
$this->assertCount(3, $articles);
```
This method doesn’ t exist on the repository yet, so the test will fail.
> Error : Call to undefined method
> Drupal\my_module\Repository\ArticleRepository::getAll()
2020-03-18 12:25:04 +00:00
Let’ s fix that by adding the `getAll()` method. For now, have it return an empty array.
2020-03-13 11:17:16 +00:00
```php
// my_module/Repository/ArticleRepository.php
public function getAll(): array {
return [];
}
```
2020-03-18 12:25:04 +00:00
Now we’ ve got everything in place, and the test failure is because we aren’ t returning the correct number of articles.
2020-03-13 11:17:16 +00:00
> Failed asserting that actual size 0 matches expected size 3.
We can fix this by building up the `ArticleRepository` class.
### 4f. Building up the ArticleRepository
2020-03-18 12:25:04 +00:00
The `ArticleRepository` needs to return some articles. We can do this by injecting the `EntityTypeManager` and using it to return nodes from the `getAll()` method rather than the empty array.
2020-03-13 11:17:16 +00:00
```diff
2020-03-14 00:25:20 +00:00
+ use Drupal\Core\Entity\EntityTypeManagerInterface;
+
2020-03-13 11:17:16 +00:00
+ /**
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ private $nodeStorage;
+ public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+ $this->nodeStorage = $entityTypeManager->getStorage('node');
+ }
public function getAll(): array {
- return [];
+ return $this->nodeStorage->loadMultiple();
}
```
2020-03-18 12:25:04 +00:00
Within our services file, we now need to add the `EntityTypeManager` as an argument so that it’ s used to create the `ArticleRepository` . Currently we don’ t have enough arguments.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
> ArgumentCountError : Too few arguments to function Drupal\my_module\Repository\ArticleRepository::\_\_construct(), 0 passed and exactly 1 expected
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
The quickest way to do that is to enable autowiring for the ArticleRepository within `my_module.services.yml` . This will automatically inject services rather than needing to specify each argument individually.
2020-03-13 11:17:16 +00:00
```diff
- Drupal\my_module\Repository\ArticleRepository: ~
2020-03-19 23:21:02 +00:00
+ Drupal\Core\Entity\EntityTypeManagerInterface:
+ alias: entity_type.manager
+
2020-03-13 11:17:16 +00:00
+ Drupal\my_module\Repository\ArticleRepository:
+ autowire: true
```
2020-03-18 12:25:04 +00:00
> Drupal\Component\Plugin\Exception\PluginNotFoundException : The "node" entity type does not exist.
2020-03-13 11:17:16 +00:00
As we did previously, we need to enable the `node` module.
```diff
public static $modules = [
+ 'node',
'my_module',
];
```
2020-03-18 12:25:04 +00:00
The `ArticleRepository` is now working, but is still returning no articles - though this is because we haven’ t created any inside the test.
2020-03-13 11:17:16 +00:00
> Failed asserting that actual size 0 matches expected size 3.
### 4g. Adding articles
2020-03-18 12:25:04 +00:00
To test the ArticleRepository, we need articles to be created so that they can be returned.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
Within the `ArticleRepositoryTest` we can make use of one of a number of traits that are provided.
2020-03-13 11:17:16 +00:00
Within the class, enable the trait:
```diff
+ use NodeCreationTrait
```
2020-03-18 12:25:04 +00:00
Include the import statement at the top of the file if it hasn’ t been added automatically.
2020-03-13 11:17:16 +00:00
```diff
+ use Drupal\Tests\node\Traits\NodeCreationTrait;
```
2020-03-18 12:25:04 +00:00
This gives us a `createNode` method that we can use to create nodes by passing an array of values.
2020-03-13 11:17:16 +00:00
As we need 3 articles, let’ s create them.
```diff
+ $this->createNode(['type' => 'article'])->save();
+ $this->createNode(['type' => 'article'])->save();
+ $this->createNode(['type' => 'article'])->save();
2020-03-18 19:32:25 +00:00
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
2020-03-13 11:17:16 +00:00
```
The next error is a little cryptic:
> Error : Call to a member function id() on bool
2020-03-18 12:25:04 +00:00
Looking at the stack trace, this error is within `filter.module` , where it’ s trying to call an `id()` method on a filter format.
2020-03-13 11:17:16 +00:00
```php
// filter.module
$formats = filter_formats($account);
$format = reset($formats);
return $format->id();
```
2020-03-18 12:25:04 +00:00
As kernel tests don’ t install configuration by default, we need to install the missing filter configuration.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
As we’ ll need this for all test methods in this case, we can use the `setUp()` method that will run before each test.
2020-03-13 11:17:16 +00:00
```diff
+ protected function setUp() {
+ parent::setUp();
+
+ $this->installConfig([
+ 'filter',
+ ]);
+ }
```
We also need to create the `node_access` table as indicated by the next error:
2020-03-18 12:25:04 +00:00
> Drupal\Core\Entity\EntityStorageException : SQLSTATE[HY000]: General error: 1 no such table: test90927710.node_access: DELETE FROM {node_access}
2020-03-13 11:17:16 +00:00
```diff
protected function setUp() {
parent::setUp();
+
+ $this->installSchema('node', ['node_access']);
+
$this->installConfig([
'filter',
]);
}
```
We’ ve successfully returned our three articles and this test now passes.
2020-11-13 23:53:24 +00:00
> OK (1 test, 11 assertions)
2020-03-13 11:17:16 +00:00
### 4h. Making this test less brittle
The test is passing, but it currently returns _all_ nodes and not just articles.
2020-03-18 12:25:04 +00:00
If we create some page nodes too, these will also be returned and the assertion will now fail.
2020-03-13 11:17:16 +00:00
```diff
$this->createNode(['type' => 'article'])->save();
+ $this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
+ $this->createNode(['type' => 'page'])->save();
$this->createNode(['type' => 'article'])->save();
```
> Failed asserting that actual size 5 matches expected size 3.
2020-03-18 12:25:04 +00:00
We can make a change to the `ArticleRepository` to fix this, and ensure that we’ re only loading and returning article nodes.
2020-03-13 11:17:16 +00:00
```diff
- return $this->nodeStorage->loadMultiple();
+ return $this->nodeStorage->loadByProperties([
+ 'type' => 'article',
+ ]);
```
### 4i. Ensuring that only published articles are returned
2020-03-18 12:25:04 +00:00
We now know that only article nodes are returned, but _all_ articles are being returned. On our blog, we only want to published articles to be displayed.
2020-03-13 11:17:16 +00:00
Let’ s create another test for this.
```diff
+ /** @test */
+ public function only_published_articles_are_returned() {
+ $repository = $this->container->get(ArticleRepository::class);
+ $articles = $repository->getAll();
+ $this->assertCount(3, $articles);
+ }
```
2020-03-18 12:25:04 +00:00
We already know that only articles are returned, so in this test we can focus on the published status. We can create a number of articles, some which are published and some which are unpublished.
2020-03-13 11:17:16 +00:00
```diff
/** @test */
public function only_published_articles_are_returned() {
+ $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+ $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+ $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
+ $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED])->save();
+ $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED])->save();
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
}
```
2020-03-18 12:25:04 +00:00
Because we have no filter on the published state, all the articles are returned including the unpublished ones.
2020-03-13 11:17:16 +00:00
> Failed asserting that actual size 5 matches expected size 3.
2020-03-18 12:25:04 +00:00
We can again update the `ArticleRepository` to add this extra condition to filter out any unpublished articles.
2020-03-13 11:17:16 +00:00
```diff
+ use Drupal\node\Entity\Node;
public function getAll(): array {
return $this->nodeStorage->loadByProperties([
+ 'status' => Node::PUBLISHED,
'type' => 'article',
]);
}
```
With this added, the test passes again.
> OK (1 test, 6 assertions)
### 4j. Ensuring that articles are returned in the correct order
2020-03-18 12:25:04 +00:00
As per our acceptance criteria, we need the articles to be returned based on their created date, so let’ s create another test.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
In this test, let’ s ensure assert that some article node IDs are returned within a specific order.
2020-03-13 11:17:16 +00:00
```diff
+ /** @test */
+ public function nodes_are_ordered_by_date_and_newest_first() {
+ $this->assertSame([3, 1, 2, 5, 4], $nodeIds);
+ }
```
We can use the same `getAll()` method, and get the node IDs from the array keys.
```diff
/** @test */
public function nodes_are_ordered_by_date_and_returned_newest_first() {
+ $repository = $this->container->get(ArticleRepository::class);
+ $nodes = $repository->getAll();
+ $nodeIds = array_keys($nodes);
$this->assertSame([3, 1, 2, 5, 4], $nodeIds);
}
```
2020-03-18 12:25:04 +00:00
Use `createNode()` again to create some article nodes, each with a different `created` date to match our assertion. This is to ensure that the test doesn’ t pass by default.
2020-03-13 11:17:16 +00:00
```diff
/** @test */
public function nodes_are_ordered_by_date_and_returned_newest_first() {
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]);
+ $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]);
$repository = $this->container->get(ArticleRepository::class);
$nodes = $repository->getAll();
$nodeIds = array_keys($nodes);
$this->assertSame([3, 1, 2, 5, 4], $nodeIds);
}
```
2020-03-18 12:25:04 +00:00
This test fails as expected, as the nodes are returned in their default order, by node ID.
2020-03-13 11:17:16 +00:00
> Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@
> Array &0 (
>
> - 0 => 3
> - 1 => 1
> - 2 => 2
> - 3 => 5
> - 4 => 4
>
> * 0 => 1
> * 1 => 2
> * 2 => 3
> * 3 => 4
> * 4 => 5 )
2020-03-18 12:25:04 +00:00
To order the articles by their created date, we can update the `getAll()` method within the `ArticleRepository` .
2020-03-13 11:17:16 +00:00
```diff
public function getAll(): array {
- return $this->nodeStorage->loadByProperties([
+ $articles = $this->nodeStorage->loadByProperties([
'status' => Node::PUBLISHED,
'type' => 'article',
]);
+
2020-03-19 23:23:41 +00:00
+ $this->sortByCreatedDate($articles);
+
+ return $articles;
+ }
+
+ private function sortByCreatedDate(array & $articles): void {
2020-03-13 11:17:16 +00:00
+ uasort($articles, function (NodeInterface $a, NodeInterface $b): bool {
+ return $a->getCreatedTime() < $b->getCreatedTime();
+ });
}
```
2020-03-18 12:25:04 +00:00
The nodes are now ordered by their created date, and in the correct order to match the assertion.
2020-03-13 11:17:16 +00:00
2020-11-13 23:53:24 +00:00
> OK (1 test, 11 assertions)
2020-03-13 11:17:16 +00:00
### 4k. Linking up the repository to the BlogPageController
2020-03-18 12:25:04 +00:00
Now that our `ArticleRepository` tests are passing, we can use it within `BlogPageController` so that articles are displayed on the page.
2020-03-13 11:17:16 +00:00
Let’ s add `BlogPageController` as a service and enable autowiring.
```diff
# my_module.services.yml
services:
+ Drupal\my_module\Controller\BlogPageController:
+ autowire: true
Drupal\my_module\Repository\ArticleRepository:
autowire: true
```
2020-03-18 12:25:04 +00:00
Now we can inject the `ArticleRepository` and use it to load and render the articles on the page.
2020-03-13 11:17:16 +00:00
```diff
namespace Drupal\my_module\Controller;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_module\Repository\ArticleRepository;
class BlogPageController {
use StringTranslationTrait;
+ /**
+ * @var \Drupal\my_module\Repository\ArticleRepository
+ */
+ private $articleRepository;
+
+ /**
+ * @var \Drupal\Core\Entity\EntityViewBuilderInterface
+ */
+ private $nodeViewBuilder;
+
+ public function __construct(
+ EntityTypeManagerInterface $entityTypeManager,
+ ArticleRepository $articleRepository
+ ) {
+ $this->nodeViewBuilder = $entityTypeManager->getViewBuilder('node');
+ $this->articleRepository = $articleRepository;
+ }
public function __invoke(): array {
- return [];
+ $build = [];
+
+ $articles = $this->articleRepository->getAll();
+ foreach ($articles as $article) {
+ $build[] = $this->nodeViewBuilder->view($article, 'teaser');
+ }
+
+ return [
+ '#markup' => render($build),
+ ];
}
}
```
In the browser, we should see our list of articles.
![](docs/images/3.png)
## Step 5: Wrapping up with unit tests
### 5a: Creating an ArticleWrapper
2020-03-18 12:25:04 +00:00
The final thing that we’ re going to do is create a wrapper class for articles so that we can add additional methods.
2020-03-13 11:17:16 +00:00
```bash
mkdir -p tests/src/Unit/Wrapper
```
```php
// tests/src/Unit/Wrapper/ArticleWrapperTest
namespace Drupal\Tests\my_module\Unit\Wrapper;
use Drupal\Tests\UnitTestCase;
class ArticleWrapperTest extends UnitTestCase {
}
```
2020-03-18 12:25:04 +00:00
The first thing that we’ re going to check is that we can wrap an article node and then retrieve it again.
2020-03-13 11:17:16 +00:00
```php
/** @test */
public function it_can_return_the_article() {
$articleWrapper = new ArticleWrapper($article);
$this->assertInstanceOf(NodeInterface::class, $articleWrapper->getOriginal());
$this->assertSame(5, $articleWrapper->getOriginal()->id());
$this->assertSame('article', $articleWrapper->getOriginal()->bundle());
}
```
Lets create the `ArticleWrapper` :
```php
// src/Wrapper/ArticleWrapper
namespace Drupal\my_module\Wrapper;
use Drupal\node\NodeInterface;
class ArticleWrapper {
private $article;
public function __construct(NodeInterface $node) {
$this->article = $node;
}
public function getOriginal(): NodeInterface {
return $this->article;
}
}
```
In the `ArticleWrapperTest` , add the import statement for the wrapper.
```diff
+ use Drupal\my_module\Wrapper\ArticleWrapper;
use Drupal\Tests\UnitTestCase;
```
Currently, we don’ t yet have an article to wrap.
> Undefined variable: article
2020-03-18 12:25:04 +00:00
Because this is a unit test which has no access to the database, we can’ t use the `createNode()` method or `Node::create()` so we need to create a mock node.
2020-03-13 11:17:16 +00:00
```diff
+ use Drupal\node\NodeInterface;
$article = $this->createMock(NodeInterface::class);
```
This satisfies the instance assertion, but the other assertions are failing.
> Failed asserting that null is identical to 5.
2020-03-18 12:25:04 +00:00
In order for this to pass, we need to specify what value each method will method will return (e.g. which node ID and which bundle type) so that these values are populated when used within the production code.
2020-03-13 11:17:16 +00:00
```diff
$article = $this->createMock(NodeInterface::class);
+ $article->method('id')->willReturn(5);
+ $article->method('bundle')->willReturn('article');
```
> OK (1 test, 3 assertions)
### 5b. Ensure that we’ re only wrapping articles
2020-03-18 12:25:04 +00:00
The ArticleWrapper is only supposed to be used for wrapping articles, so we want to throw an Exception if a different type of node is used.
2020-03-13 11:17:16 +00:00
Let’ s create a page node and assert that an Exception should be thrown.
```php
/** @test */
public function it_throws_an_exception_if_the_node_is_not_an_article() {
$this->expectException(\InvalidArgumentException::class);
$page = $this->createMock(NodeInterface::class);
$page->method('bundle')->willReturn('page');
new ArticleWrapper($page);
}
```
Because we’ re currently not throwing an Exception, the assertion fails.
> Failed asserting that exception of type "InvalidArgumentException" is thrown.
2020-03-18 12:25:04 +00:00
Within `ArticleWrapper` , add a check using the `bundle()` method and throw an Exception if it is not an article.
2020-03-13 11:17:16 +00:00
```diff
public function __construct(NodeInterface $node) {
+ $this->verifyNodeType($node);
$this->article = $node;
}
+ private function verifyNodeType(NodeInterface $node): void {
+ if ($node->bundle() != 'article') {
+ throw new \InvalidArgumentException(sprintf(
+ '%s is not an article',
+ $node->bundle()
+ ));
+ }
+ }
```
If the Exception is thrown, the test will pass.
### 5c. Determine if articles are publishable
2020-03-18 12:25:04 +00:00
Let’ s imagine that there is a rule that articles less than 3 days old are not eligible to be published and we want to add an `isPublishable()` method that will contain this logic and determine whether or not an article is publishable.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
We need access to the `Time` class to get the system time, so this is added as an argument to the constructor and used within `isPublishable()` to compare the created time of the article to the current time.
2020-03-13 11:17:16 +00:00
```diff
+ use Drupal\Component\Datetime\TimeInterface;
use Drupal\node\NodeInterface;
class ArticleWrapper {
private $article;
- public function __construct(NodeInterface $node) {
+ public function __construct(TimeInterface $time, NodeInterface $node) {
$this->verifyNodeType($node);
+ $this->time = $time;
$this->article = $node;
}
+ public function isPublishable(): bool {
+ $created = $this->article->getCreatedTime();
+
+ $difference = $this->time->getRequestTime() - $created;
+
+ return $difference >= 60 * 60 * 24 * 3;
+ }
}
```
2020-03-18 12:25:04 +00:00
We want to compare against a number of dates to ensure that this is working as expected. Rather than writing separate tests, we can use a data provider. This is a method that returns data to be passed to the tests as parameters.
2020-03-13 11:17:16 +00:00
```php
public function articleCreatedDateProvider() {
return [
['-1 day', FALSE],
['-2 days 59 minutes', FALSE],
['-3 days', TRUE],
['-1 week', TRUE],
];
}
```
2020-03-18 12:25:04 +00:00
Use the `@dataProvider` annotation for the test to specify the method to use, and add the parameters to the test.
2020-03-13 11:17:16 +00:00
2020-03-19 23:39:01 +00:00
We also need to create a mock for `TimeInterface` so that it can be passed into the article wrapper, and so that methods on it can return certain values.
2020-03-13 11:17:16 +00:00
```php
/**
* @test
* @dataProvider articleCreatedDateProvider
*/
public function articles_created_less_than_3_days_ago_are_not_publishable(
string $offset,
bool $expected
) {
2020-03-19 23:39:01 +00:00
$time = $this->createMock(TimeInterface::class);
$time->method('getRequestTime')->willReturn(
2020-03-13 11:17:16 +00:00
(new \DateTime())->getTimestamp()
);
$article = $this->createMock(NodeInterface::class);
$article->method('bundle')->willReturn('article');
$article->method('getCreatedTime')->willReturn(
(new \DateTime())->modify($offset)->getTimestamp()
);
2020-03-19 23:39:01 +00:00
$articleWrapper = new ArticleWrapper($time, $article);
2020-03-13 11:17:16 +00:00
$this->assertSame($expected, $articleWrapper->isPublishable());
}
```
2020-03-18 12:25:04 +00:00
We can use `$offset` value to modify the returned date from `getCreatedTime()` , and use `$expected` to make the assertion against `isPublishable()` .
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
The test is run, is then run against each set of data and passes or fails accordingly.
2020-03-13 11:17:16 +00:00
2020-03-18 12:25:04 +00:00
> - Articles created less than 3 days ago are not publishable with data set #0 [23.76 ms]
> - Articles created less than 3 days ago are not publishable with data set #1 [0.32 ms]
> - Articles created less than 3 days ago are not publishable with data set #2 [0.25 ms]
> - Articles created less than 3 days ago are not publishable with data set #3 [0.37 ms]
2020-03-19 23:39:01 +00:00
The other tests will also need to be updated to mock `TimeInterface` and pass it to the article wrapper.
## 5d: Refactoring the unit tests
With the unit tests passing, we can refactor them and simplify things by extracting a method for creating an article wrapper.
As each test will need the mocked time, this can be moved into a `setUp` method, and we can use `$this->time` instead within the test.
```php
private $time;
protected function setUp() {
parent::setUp();
$this->time = $this->createMock(Time::class);
}
```
```diff
- $time->method('getRequestTime')->willReturn(
+ $this->time->method('getRequestTime')->willReturn(
(new \DateTime())->getTimestamp()
);
```
To make it simpler to create an article wrapper, we can create a helper method for that. This will also be responsible for creating and injecting any dependencies.
```php
private function createArticleWrapper(NodeInterface $article): ArticleWrapper {
return new ArticleWrapper($this->time, $article);
}
```
Now we can update the tests to use the new `createArticleWrapper()` method:
```diff
- $time = $this->createMock(TimeInterface::class);
- $articleWrapper = new ArticleWrapper($time, $article);
+ $articleWrapper = $this->createArticleWrapper($article);
- $time = $this->createMock(TimeInterface::class);
- new ArticleWrapper($time, $page);
+ $articleWrapper = $this->createArticleWrapper($page);
- $articleWrapper = new ArticleWrapper($time, $article);
+ $articleWrapper = $this->createArticleWrapper($article);
```
If the refactor was successful, the tests will still pass.