22 KiB
autoscale: true build-lists: true header-emphasis: #3D85C6 header: alignment(left) text: alignment(left) text-emphasis: #3D85C6 theme: poster, 8 code: Monaco, #6699FF, #999999, #6666FF, #66FF66, #66FF66, line-height(1.5)
[.header: alignment(center)]
[fit] Drupal Testing Workshop
September 2018
[.header: alignment(center)]
Why write tests?
Why write tests?
- Catch bugs earlier
- Peace of mind
- Prevent regressions
- Write less code
- Documentation
- Drupal core requirement - https://www.drupal.org/core/gates#testing
- More important with regular D8 releases
^ Dave Liddament talk - better and cheaper to catch bugs earlier (e.g. whilst developing rather than after it's been released) Refer to tests when writing implementation code ONO merge conflict
[.header: alignment(center)]
[fit] Having tests does not mean
[fit] there will be no bugs
[.header: alignment(center)]
[fit] Testing may add time now
[fit] but save more time in the future
Testing in Drupal
- Drupal 7 - Simpletest (testing) module provided as part of core
- Drupal 8 - PHPUnit added as a core dependency
- PHPUnit Initiative - Simpletest to be deprecated and removed
^ Focussing on PHPUnit today
[.header: #3D85C6]
Writing Tests (Drupal 8)
- PHP class with .php extension
- tests/src directory within each module
- Within the Drupal\Tests\module_name namespace
- Class name must match the filename
- Namespace must match the directory structure
- One test class per feature
- Each method must start with test
^ Different to D7
Exercise 1
Local site setup
[.header: #3D85C6]
Docksal
- Docker based local development environment
- Microserve standard
- Open source
- Per site configuration and customisation
- fin CLI, Apache, MySQL, Solr, Varnish, Mailhog, PHPMyAdmin etc
- Virtualbox or native Docker
- Can slow down tests
- Provides consistency
- https://github.com/opdavies/drupal-testing-workshop
- https://docksal.io/installation
- git clone
- fin init
- http://drupaltest.docksal
^ Contains Drupal 8 with Composer, examples module
Exercise 2
Running Tests
Option 1
Simpletest module (UI)
Option 2
Command line
Prerequisite (creating a phpunit.xml file)
- Configures PHPUnit
- Needed to run some types of tests
- Ignored by Git by default
- Copy core/phpunit.xml.dist to core/phpunit.xml
- Add and change as needed
SIMPLETEST_BASE_URL
,SIMPLETEST_DB
,BROWSERTEST_OUTPUT_DIRECTORY
stopOnFailure="true"
cd web
../vendor/bin/phpunit -c core \
modules/contrib/examples/phpunit_example
cd web/core
../../vendor/bin/phpunit \
../modules/contrib/examples/phpunit_example
Pro-tip: Add paths to $PATH
# ~/.zshrc
export PATH=$HOME/bin:/usr/local/bin:$PATH
export PATH=vendor/bin:$PATH
export PATH=../vendor/bin:$PATH
export PATH=node_modules/.bin:$PATH
Option 2
CLI with Docksal
fin bash
cd web
../vendor/bin/phpunit -c core \
modules/contrib/examples/phpunit_example
fin bash
cd web/core
../../vendor/bin/phpunit \
../modules/contrib/examples/phpunit_example
Option 3
Docksal PHPUnit addon
- Custom Docksal command
- Submitted to the Docksal addons repo
- fin addon install phpunit
- Wrapper around phpunit command
- Copies a stub phpunit.xml file if exists, or duplicates phpunit.xml.dist
- Shorter command, combines two actions
^ Checks for core/phpunit.xml on each test run Will create one if is not present
fin phpunit web/modules/contrib/examples/phpunit_example
Copying stubs from /var/www/.docksal/addons/phpunit/stubs
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.
Testing web/modules/contrib/examples/phpunit_example
.................................. 34 / 34 (100%)
Time: 46.8 seconds, Memory: 6.00MB
OK (34 tests, 41 assertions)
fin phpunit web/modules/contrib/examples/phpunit_example
Copying /var/www/web/core/phpunit.xml.dist to /var/www/web/core/phpunit.xml.
Please edit it's values as needed and re-run 'fin phpunit'.
fin phpunit web/modules/contrib/examples/phpunit_example
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.
Testing web/modules/contrib/examples/phpunit_example
.................................. 34 / 34 (100%)
Time: 48.62 seconds, Memory: 6.00MB
OK (34 tests, 41 assertions)
Option 4
IDE/text editor integration
[.header: alignment(center)]
Types of tests
[.header: #3D85C6]
Functional tests
- Tests functionality
- Interacts with database
- Full Drupal installation
- With/without JavaScript
^ testing profile
[.header: #3D85C6]
Functional tests
- Slower to run
- Easiest to start with
- Provide most value
^ Less setup steps No mocking etc.
Exercise
Let's write a
functional test
- Create a web/modules/custom/workshop directory
- Create a
workshop.info.yml
file
# workshop.info.yml
name: Drupal Testing Workshop
core: 8.x
type: module
- Create a tests/src/Functional directory
- Create an ExampleFunctionalTest.php file
// ExampleFunctionalTest.php
namespace Drupal\Tests\workshop\Functional;
use Drupal\Tests\BrowserTestBase;
class ExampleFunctionalTest extends BrowserTestBase {
}
// ExampleFunctionalTest.php
public function testExamplePageExists() {
}
// ExampleFunctionalTest.php
public function test_example_page_exists() {
}
^ Snake case test method names Still works because it has the 'test' prefix More readable than camel case? Works with Simpletest/D7
// ExampleFunctionalTest.php
/** @test */
public function example_page_exists() {
}
^ Remove the prefix, use annotation PHPUnit only
// ExampleFunctionalTest.php
/** @test */
public function example_page_exists() {
// Arrange
// Act
// Assert
}
// ExampleFunctionalTest.php
/** @test */
public function example_page_exists() {
// Given that I am an anonymous user.
// When I go to /blog.
// I should see the blog page.
}
// ExampleFunctionalTest.php
/** @test */
public function example_page_exists() {
$this->drupalGet('/example-one');
$this->assertSession()->statusCodeEquals(200);
}
// ExampleFunctionalTest.php
protected static $modules = ['workshop'];
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.
Testing Drupal\Tests\workshop\Functional\ExampleFunctionalTest
Behat\Mink\Exception\ExpectationException : Current response status code is 404, but 200 expected.
/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/workshop/tests/src/Functional/ExampleFunctionalTest.php:14
Time: 18.2 seconds, Memory: 6.00MB
ERRORS!
Tests: 1, Assertions: 2, Errors: 1.
- Create a workshop.routing.yml file
- Create a Controller
# workshop.routing.yml
workshop.example:
path: '/example-one'
defaults:
_controller: 'Drupal\workshop\Controller\ExampleController::index'
requirements:
_access: 'TRUE'
// src/Controller/ExampleController.php
namespace Drupal\workshop\Controller;
class ExampleController {
public function index() {
return [];
}
}
Time: 25.22 seconds, Memory: 6.00MB
OK (1 test, 3 assertions)
[.header: #3D85C6]
Kernel tests
- Integration tests
- Can install modules, interact with services, container, database
- Minimal Drupal bootstrap
- Faster than functional tests
- More setup required
Exercise
Let's write a
kernel test
- Create a tests/src/Kernel directory
- Create an ExampleKernelTest.php file
- Create a Service
- Use the service within the test to perform an action
// tests/src/Kernel/ExampleKernelTest.php
namespace Drupal\Tests\workshop\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\user\Entity\User;
class ExampleKernelTest extends EntityKernelTestBase {
public static $modules = ['workshop'];
}
// tests/src/Kernel/ExampleKernelTest.php
public function testUserDeleter {
$user = $this->createUser();
$user_deleter = \Drupal::service(UserDeleter::class);
$user_deleter->delete($user);
$user = $this->reloadEntity($user);
$this->assertNull($user);
}
# workshop.services.yml
services:
workshop.user_deleter:
class: Drupal\workshop\Service\UserDeleter
# workshop.services.yml
services:
Drupal\workshop\Service\UserDeleter: ~
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException :
You have requested a non-existent service
"Drupal\Tests\workshop\Kernel\UserDeleter".
// src/Service/UserDeleter.php
namespace Drupal\workshop\Service;
use Drupal\Core\Session\AccountInterface;
class UserDeleter {
public function delete(AccountInterface $user) {
user_delete($user->id());
}
}
// tests/src/Kernel/ExampleKernelTest.php
namespace Drupal\Tests\workshop\Kernel;
use Drupal\workshop\Service\UserDeleter;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\user\Entity\User;
...
^ Import new UserDeleter
Drupal\Core\Entity\EntityStorageException : SQLSTATE[HY000]:
General error: 1 no such table:
test89378988.users_data: DELETE FROM {users_data}
WHERE uid IN (:db_condition_placeholder_0); Array
(
[:db_condition_placeholder_0] => 1
)
// tests/src/Kernel/ExampleKernelTest.php
protected function setUp() {
parent::setUp();
$this->installSchema('user', ['users_data']);
}
OK (1 test, 5 assertions)
[.header: #3D85C6]
Unit tests
- Tests PHP logic
- No database interaction
- Fast to run
- Tightly coupled
- Mocking dependencies
- Hard to refactor
Exercise
Let's write a
unit test
// tests/src/Unit/Service/ExampleUnitTest.php
namespace Drupal\Tests\workshop\Unit;
use Drupal\Tests\UnitTestCase;
class ExampleUnitTest extends UnitTestCase {
public function testAdd() {
$this->assertEquals(5, (new Calculator(3))->add(2)->calculate());
}
}
Error : Class 'Drupal\Tests\workshop\Unit\Calculator' not found
/var/www/web/modules/custom/workshop/tests/src/Unit/Service/ExampleUnitTest.php:10
// src/Service/Calculator.php
namespace Drupal\workshop\Service;
class Calculator {
private $total;
public function __construct($value) {
$this->total = $value;
}
public function add($value) {
$this->total += $value;
return $this;
}
public function calculate() {
return $this->total;
}
}
// tests/src/Unit/Service/ExampleUnitTest.php
namespace Drupal\Tests\workshop\Unit;
use Drupal\workshop\Service\Calculator;
use Drupal\Tests\UnitTestCase;
...
Time: 4.55 seconds, Memory: 4.00MB
OK (1 test, 1 assertion)
[.header: alignment(center)]
Test driven
development (TDD)
Test Driven Development
- Write a failing test
- Write code until passes
- Refactor
- Repeat
[.background-color: #FFFFFF] [.footer: https://github.com/foundersandcoders/testing-tdd-intro] [.footer-style: #2F2F2F]
How I Write Tests - "Outside In"
- Start with functional tests
- Drop down to kernel or unit tests where needed
- Programming by wishful thinking
- Write comments first, then fill in the code
- Sometimes write assertions first
Exercise
Let's build a blog using test driven development
Acceptance criteria
- As a site visitor
- I want to see a list of published articles at /blog
- Ordered by post date
Tasks
- Ensure the blog page exists
- Ensure only published articles are shown
- Ensure the articles are shown in the correct order
Implementation
- Use views module
- Do the mininum amount at each step, make no assumptions, let the tests guide us
- Start with functional test
Step 1
Create the module
# tdd_blog.info.yml
name: 'TDD Blog'
core: '8.x'
type: 'module'
Step 2
Ensure the blog page exists
// tests/src/Functional/BlogPageTest.php
namespace Drupal\Tests\tdd_blog\Functional;
use Drupal\Tests\BrowserTestBase;
class BlogPageTest extends BrowserTestBase {
protected static $modules = ['tdd_blog'];
}
public function testBlogPageExists() {
$this->drupalGet('/blog');
$this->assertSession()->statusCodeEquals(200);
}
There was 1 error:
1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists
Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected.
- The view has not been created
- Create a new view, page display
- Set the path
- Export the config
- Copy it into the module's
config/install
directory
drush cex -y
cp ../config/default/views.view.blog.yml \
modules/custom/tdd_blog/config/install
# views.view.blog.yml
- uuid: 84305edf-7aef-4109-bc93-e87f685fb678
langcode: en
status: true
dependencies:
config:
- node.type.article
module:
- node
- user
- _core:
- default_config_hash: iGZkqLWpwWNORq6_fy6v_Kn_KE4BjYHqj9vpgQsWJCs
id: blog
...
1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists
Drupal\Core\Config\UnmetDependenciesException: Configuration objects provided
by <em class="placeholder">tdd_blog</em>
have unmet dependencies: <em class="placeholder">views.view.blog
(node.type.article, node, views)</em>
# tdd_blog.info.yml
name: 'TDD Blog'
description: 'A demo module to show test driven module development.'
core: 8.x
type: module
dependencies:
- 'drupal:node'
- 'drupal:views'
1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists
Drupal\Core\Config\UnmetDependenciesException: Configuration objects provided
by <em class="placeholder">tdd_blog</em> have unmet dependencies:
<em class="placeholder">views.view.blog (node.type.article)</em>
- Add the article content type
OK (1 test, 3 assertions)
[.build-lists: false]
Tasks
Ensure the blog page exists- Ensure only published articles are shown
- Ensure the articles are shown in the correct order
Step 3
Ensure only published articles are shown
public function testOnlyPublishedArticlesAreShown() {
// Given I have a mixture of published and unpublished articles,
// as well as other types of content.
// When I view the blog page.
// I should only see the published articles.
}
Option 1
Functional tests
// modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php
public function testOnlyPublishedArticlesAreShown() {
// Given I have a mixture of published and unpublished articles,
// as well as other types of content.
$node1 = $this->drupalCreateNode(['type' => 'page', 'status' => 1]);
$node2 = $this->drupalCreateNode(['type' => 'article', 'status' => 1]);
$node3 = $this->drupalCreateNode(['type' => 'article', 'status' => 0]);
// When I view the blog page.
$this->drupalGet('/blog');
// I should only see the published articles.
$assert = $this->assertSession();
$assert->pageTextContains($node2->label());
$assert->pageTextNotContains($node1->label());
$assert->pageTextNotContains($node3->label());
}
^ Different ways to achieve this. This is taking the functional test approach.
Option 2
Kernel tests
namespace Drupal\Tests\tdd_blog\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
class BlogPageTest extends EntityKernelTestBase {
use NodeCreationTrait;
public static $modules = ['node'];
}
public function testOnlyPublishedArticlesAreShown() {
$this->createNode(['type' => 'page', 'status' => 1]);
$this->createNode(['type' => 'article', 'status' => 1]);
$this->createNode(['type' => 'article', 'status' => 0]);
}
^ Kernel test approach Dropping down a level No need for the brower, not asserting against HTML Faster to run
1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown
Error: Call to a member function id() on boolean
/var/www/web/core/modules/filter/filter.module:212
/var/www/web/core/modules/node/tests/src/Traits/NodeCreationTrait.php:73
/var/www/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:13
$this->installConfig(['filter']);
public function testOnlyPublishedArticlesAreShown() {
...
$results = views_get_view_result('blog');
}
public function testOnlyPublishedArticlesAreShown() {
...
$results = views_get_view_result('blog');
$this->assertCount(1, $results);
$this->assertEquals(2, $results[0]->_entity->id());
}
public static $modules = [
'node',
'tdd_blog',
'views',
];
public function setUp() {
parent::setUp();
$this->installConfig(['filter', 'tdd_blog']);
}
There was 1 failure:
1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown
Failed asserting that actual size 2 matches expected size 1.
/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:23
- There is no content type filter on the view
- Add the filter
- Re-export and save the view
OK (1 test, 6 assertions)
[.build-lists: false]
Tasks
Ensure the blog page existsEnsure only published articles are shown- Ensure the articles are shown in the correct order
Step 4
Ensure the articles are ordered by date
// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php
public function testArticlesAreOrderedByDate() {
// Given that I have numerous articles with different post dates.
// When I go to the blog page.
// The articles are ordered by post date.
}
// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php
public function testArticlesAreOrderedByDate() {
// Given that I have numerous articles with different post dates.
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 day'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 month'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+3 days'))->getTimestamp()]);
$this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 hour'))->getTimestamp()]);
// When I go to the blog page.
// The articles are ordered by post date.
}
$this->createNode([
'type' => 'article',
'created' => (new DrupalDateTime('+1 day'))->getTimestamp(),
]);
^ Array of default values
// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php
public function testArticlesAreOrderedByDate() {
...
// When I go to the blog page.
$results = views_get_view_result('blog');
// The articles are ordered by post date.
}
// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php
public function testArticlesAreOrderedByDate() {
...
// When I go to the blog page.
$results = views_get_view_result('blog');
$nids = array_map(function(ResultRow $result) {
return $result->_entity->id();
}, $results);
// The articles are ordered by post date.
}
// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php
public function testArticlesAreOrderedByDate() {
...
// The articles are ordered by post date.
$this->assertEquals([4, 1, 3, 2], $nids);
}
There was 1 failure:
1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testArticlesAreOrderedByDate
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
- 0 => 4
- 1 => 1
- 2 => 3
- 3 => 2
+ 0 => '1'
+ 1 => '2'
+ 2 => '3'
+ 3 => '4'
- There is no sort order defined on the view
- Add the sort order
- Re-export the view
OK (1 test, 5 assertions)
[.build-lists: false]
Tasks
Ensure the blog page existsEnsure only published articles are shownEnsure the articles are shown in the correct order
[.header: alignment(center)]
Take Aways
- Testing has made me a better developer
- Testing can produce better quality code
- Use the right type of test for the right situation
- Use the right base class, use available traits
- Writing tests is an investment
- OK to start small, introduce tests gradually
- Easier to refactor
- Tests can pass, but things can still be broken. Tests only report on what they cover.
^ Made me think about how I'm going to do something more starting to do it Less cruft, only write code that serves a purpose Spending time writing tests pays dividends later on Start by introducing tests for new features or regression tests when fixing bugs If you know things pass, then you can refactor code knowing if something is broken Manual testing is still important
[.header: alignment(center)]
Questions?
[.header: alignment(center)]