talks/tdd-test-driven-drupal/slides.md

39 KiB

autoscale: true build-lists: true footer-style: alignment(left) footer: @opdavies | oliverdavies.uk header-emphasis: #53B0EB header: alignment(left) text: alignment(left) text-emphasis: #53B0EB theme: poster, 8

[.header: alignment(center)]

TDD: Test
Driven Drupal


[.background-color: #FFFFFF] [.build-lists: false] [.header: #111111] [.text: #111111, alignment(left)]

right 1000%

  • Full stack Web Developer & System Administrator
  • Senior Developer at Microserve
  • Part-time freelancer
  • Acquia certified Drupal 8 Grand Master
  • Drupal 7 & 8 core contributor
  • opdavies (Drupal.org, GitHub, Twitter)
  • www.oliverdavies.uk

^ Work at Microserve. Maintain Drupal modules, PHP CLI tools and libraries Blog on my website


[.header: alignment(center)]

test_driven_drupal_.com_


[.header: alignment(center)]

Why?

What?

How?


[.header: alignment(center)]

I write contrib modules for the community


[.header: alignment(center)]

I write custom modules for client projects


[.background-color: #FFFFFF]

inline 150%


Override Node Options

  • Become maintainer in 2012
  • Had some existing tests
  • First experience of testing with a real module
  • Used on 11,046 sites in October 2012 (84 D5, 7,094 D6, 3,868 D7)
  • Used on 29,023 sites in June 2018 (9 D5, 1,853 D6, 23,602 D7, 3,559 D8)
  • Crucial to preventing regressions when adding new features or fixing bugs
  • Ensured consistency when porting to Drupal 8

^ Preventing regressions in my additions but also user submitted patches


[.header: alignment(center)]

Why write tests?


Why write tests?

^ 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


Core Testing Gate

New features should be accompanied by automated tests.

If the feature does not have an implementation, provide a test implementation.

Bug fixes should be accompanied by changes to a test (either modifying an existing test case or adding a new one) that demonstrate the bug.

[.footer: https://www.drupal.org/core/gates#testing]


[.header: alignment(center)]

Testing may add time now, but save more
time in the future


[.header: alignment(center)]

[fit] How do you get quicker at writing tests?

[fit] By writing more tests


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 in Drupal 9

Setting up your environment

  • Drupal includes core/phpunit.xml.dist
  • Copy to core/phpunit.xml
  • Amend values as needed
    • Add base URL, database credentials
  • Docksal - fin addon install phpunit

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


[.header: alignment(center)]

1. Arrange

2. Act

3. Assert


[.hide-footer]

<?php

// modules/example/tests/src/Functional/ExampleTest.php

namespace Drupal\Tests\example\Functional;

use Drupal\Tests\BrowserTestBase;

class ExampleTest extends BrowserTestBase {

  public function testSomething() {
    // Arrange

    // Act

    // Assert
  }

}

What to test?

  • What is the core piece of functionality?
  • What provides the most value to the client?
  • What would you not like to be fixing on a Friday afternoon or after hours?

[.background-color: #FFFFFF]

inline 120%


Types of tests

  • Functional / FunctionalJavascript (web, browser)
  • Kernel (integration)
  • Unit

Functional Tests

  • Tests functionality
  • Interacts with database
  • Full Drupal installation
  • Slower to run
  • With/without JavaScript

^ testing profile


[.hide-footer]

<?php

// modules/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Tests\BrowserTestBase;

class PHPUnitExampleMenuTest extends BrowserTestBase {

  public static $modules = ['phpunit_example'];

  public function testPhpUnitExampleMenu() {
    $this->drupalGet('/examples/phpunit-example');

    $this->assertSession()->statusCodeEquals(200);
  }
}

[.hide-footer]

<?php

// modules/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Tests\BrowserTestBase;

class PHPUnitExampleMenuTest extends BrowserTestBase {

  public static $modules = ['phpunit_example'];

  public function testPhpUnitExampleMenu() {
    $this->drupalGet('/examples/phpunit-example');

    $this->assertSession()->statusCodeEquals(200);
  }
}

[.hide-footer]

<?php

// modules/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Tests\BrowserTestBase;

class PHPUnitExampleMenuTest extends BrowserTestBase {

  public static $modules = ['phpunit_example'];

  public function testPhpUnitExampleMenu() {
    $this->drupalGet('/examples/phpunit-example');

    $this->assertSession()->statusCodeEquals(200);
  }
}

[.hide-footer]

<?php

// modules/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Tests\BrowserTestBase;

class PHPUnitExampleMenuTest extends BrowserTestBase {

  public static $modules = ['phpunit_example'];

  public function testPhpUnitExampleMenu() {
    $this->drupalGet('/examples/phpunit-example');

    $this->assertSession()->statusCodeEquals(200);
  }
}

[.hide-footer]

<?php

// modules/phpunit_example/tests/src/Functional/PHPUnitExampleMenuTest.php

namespace Drupal\Tests\phpunit_example\Functional;

use Drupal\Tests\BrowserTestBase;

class PHPUnitExampleMenuTest extends BrowserTestBase {

  public static $modules = ['phpunit_example'];

  public function testPhpUnitExampleMenu() {
    $this->drupalGet('/examples/phpunit-example');

    $this->assertSession()->statusCodeEquals(200);
  }
}

Kernel Tests

  • Integration tests
  • Can install modules, interact with services, container, database
  • Minimal Drupal bootstrap
  • Faster than functional tests
  • More setup required

Unit Tests

  • Tests PHP logic
  • No database interaction
  • Fast to run
  • Tightly coupled
  • Complicated mocking

[.hide-footer]

namespace Drupal\collection_class;

class Collection implements \Countable, \IteratorAggregate {
  private $items;

  public function __construct($items = array()) {
    $this->items = is_array($items) ? $items
      : $this->getArrayableItems($items);
  }

  public function __toString() {
    return $this->toJson();
  }

  public function all() {
    return $this->items;
  }

  public function count() {
    return count($this->items);
  }


  public function isEmpty() {
    return empty($this->items);
  }

  public function first() {
    return array_shift($this->items);
  }
}

[.hide-footer]

$collection = new Collection([1, 2, 3, 4, 5]);

// Returns all items.
$collection->all();

// Counts the number of items.
$collection->count();

// Returns the array keys.
$collection->keys();

[.hide-footer]

public function testAll() {
  $items = ['foo', 'bar', 'baz'];

  $collection = new Collection($items);

  $this->assertEqual($items, $collection->all());
}

[.header: #FFFFFF, alignment(left)]

How do I know

Which type of test to use?


Need a browser?

Use a functional test


Interact with other services?

Use a kernel test


Isolated PHP code?

Use a unit test


Setup (functional)

[.hide-footer]

drupalCreateUser()
drupalCreateRole()

drupalLogin()
drupalLogout()

drupalGet()
drupalPost()
drupalPostForm()

Setup (kernel)

# UserCreationTrait
createUser()
createAdminRole()
createRole()
checkPermissions()

# CommentTestTrait
addDefaultCommentField()

# AssertMailTrait
getMails()
assertMail()

Assertions

[.hide-footer]

assertTrue()
assertFalse()

assertEquals()

assertNull()
assertNotNull()

assertCount()
assertEmpty()

assertArraySubset()

Assertions (functional)

[.hide-footer]

pageTextContains()
pageTextNotContains()

linkByHrefExists()
linkByHrefNotExists()

statusCodeEquals()
statusCodeNotEquals()

[.header: alignment(center)]

Real life example


[.background-color: #FFFFFF] [.footer-style: #2F2F2F]

inline 150%


Specification

  • Job adverts created on third-party system, needs to create nodes in Drupal, links users to separate application system
  • Adverts need to be linked to offices
  • Advert length specified in number of days
  • Path is specified as a field in the API
  • Application URL constructed from domain, includes role ID as a GET parameter and optionally UTM parameters

[.hide-footer]

$data = [
  'command' => 'add',
  'username' => 'bobsmith',
  'password' => 'p455w0rd',
  'active_for' => '365',
  'application_email' => 'bob.12345.123@smith.aplitrak.com',
  'branch_address' => '123 Fake St, Bristol, BS1 2AB',
  'branch_name' => 'Test',
  'contract' => 'Temporary',
  'details' => 'This is the detailed description.',
  'job_id' => 'abc123_1234567',
  'job_title' => 'Healthcare Assistant (HCA)',
  'job_type' => 'Care at Home',
  'keywords' => 'flexible, Bristol, part-time',
  'locations' => 'Bath, Devizes',
  'role_id' => 'A/52/86',
  'salary' => '32,000.00 per annum',
  'salary_prefix' => 'Basic Salary',
  'status' => 'Part time',
  'summary' => 'This is the short description.',
  'url_alias' => 'healthcare-assistant-aldershot-june17',
];

Implementation

  • Added route to accept data from API as XML
  • Added user with API role to authenticate
  • active_for converted from number of days to UNIX timestamp
  • branch_name and locations converted from plain text to entity reference (job node to office node)
  • url_alias property mapped to path

Goals

  • Ensure job nodes are successfully created
  • Ensure that fields are mapped correctly
  • Ensure that calculations are correct
  • Ensure that entity references are linked correctly

Types of tests

  • Unit: ensure number of days are converted to timestamps correctly
  • Kernel: job nodes can be added and deleted, expired job nodes are deleted, application URL is generated correctly
  • Functional: job nodes are created with the correct URL and the correct response code is returned
  • FunctionalJavaScript: application URL is updated with JavaScript based on UTM parameters (hosting)

Results

  • 0 bugs!
  • Reduced debugging time
  • Easier to identify where issues occurred and responsibilities

[.header: alignment(center)]

Running Tests


fit


fit


fit


fit


$ php core/scripts/run-tests.sh

$ php core/scripts/run-tests.sh --module example

$ php core/scripts/run-tests.sh --class ExampleTest

vendor/bin/phpunit -c core path/to/module

vendor/bin/phpunit -c core path/to/module --filter testSomething

vendor/bin/phpunit -c core path/to/module --verbose

[.header: alignment(center)]

Test Driven
Development


Test Driven Development

  • Write a test
  • See it fail
  • Write code until test passes
  • Refactor when tests are green
  • Repeat

[.background-color: #FFFFFF] [.footer: https://github.com/foundersandcoders/testing-tdd-intro] [.footer-style: #2F2F2F]

100%


[.header: alignment(center)]

Red, Green, Refactor


Porting Modules to Drupal 8

  • Make a new branch
  • Add/update the tests
  • Write code to make the tests pass
  • Refactor
  • Repeat

How I Write Tests - "Outside In"

  • Start with functional tests
  • Drop down to integration or unit tests where needed
  • Programming by wishful thinking
  • Write comments first, then fill in the code
  • Write assertions first, sometimes

[.header: alignment(center)]

Building a new Drupal 8 Module with TDD


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

[.hide-footer]

# tdd_blog.info.yml

name: 'TDD Example'
core: '8.x'
type: 'module'

Task 1:

Ensure the blog page exists


[.hide-footer]

<?php

// 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);
  }

}

[.hide-footer]

<?php

// 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);
  }

}

[.hide-footer]

<?php

// 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);
  }

}

[.hide-footer]

<?php

// 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);
  }

}

[.hide-footer]

<?php

// 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);
  }

}

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 19.31 seconds, Memory: 6.00MB

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.

/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 19.31 seconds, Memory: 6.00MB

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.

/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 19.31 seconds, Memory: 6.00MB

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.

/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 19.31 seconds, Memory: 6.00MB

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.

/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 19.31 seconds, Memory: 6.00MB

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.

/var/www/vendor/behat/mink/src/WebAssert.php:768
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

  • The view has not been created
  • Create a new view
  • Set the path
  • Export the config
  • Copy it into the module's config/install directory

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 16.02 seconds, Memory: 6.00MB

There was 1 error:

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>

/var/www/web/core/lib/Drupal/Core/Config/UnmetDependenciesException.php:98
/var/www/web/core/lib/Drupal/Core/Config/ConfigInstaller.php:469
/var/www/web/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php:132
/var/www/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php:145
/var/www/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83
/var/www/web/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:437
/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:1055
/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:490

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

[.hide-footer]

# tdd_blog.info.yml

name: 'TDD Dublin'
description: 'A demo module to show test driven module development.'
core: 8.x
type: module

dependencies:
  - 'drupal:node'
  - 'drupal:views'

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
E                                                                   1 / 1 (100%)

Time: 20 seconds, Memory: 6.00MB

There was 1 error:

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>

/var/www/web/core/lib/Drupal/Core/Config/UnmetDependenciesException.php:98
/var/www/web/core/lib/Drupal/Core/Config/ConfigInstaller.php:469
/var/www/web/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php:132
/var/www/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php:145
/var/www/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83
/var/www/web/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:437
/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:1055
/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:490

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

  • Add the article content type

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog
.                                                                   1 / 1 (100%)

Time: 23.36 seconds, Memory: 6.00MB

OK (1 test, 3 assertions)

Tasks

  • Ensure the blog page exists
  • Ensure only published articles are shown
  • Ensure the articles are shown in the correct order

Task 2:

Ensure only published articles are shown


[.hide-footer]

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.
}

[.header: alignment(center)]

Option 1: Functional tests


[.hide-footer]

// 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.


[.hide-footer]

// 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());
}

[.hide-footer]

// 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());
}

[.header: alignment(center)]

Option 2: Kernel tests


[.hide-footer]

<?php

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


[.hide-footer]

<?php

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]);
  }

}

[.hide-footer]

<?php

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]);
  }

}

[.hide-footer]

<?php

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]);
  }

}

[.hide-footer]

docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog/tests/src/Kernel/
PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing modules/custom/tdd_blog/tests/src/Kernel/
E                                                                   1 / 1 (100%)

Time: 6.22 seconds, Memory: 6.00MB

There was 1 error:

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

ERRORS!
Tests: 1, Assertions: 2, Errors: 1.

[.hide-footer]

public function testOnlyPublishedArticlesAreShown() {
  $this->installConfig(['filter']);

  $this->createNode(['type' => 'page', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 0]);
}

[.hide-footer]

public function testOnlyPublishedArticlesAreShown() {
  $this->installConfig(['filter']);

  $this->createNode(['type' => 'page', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 0]);

  $results = views_get_view_result('blog');
}

[.hide-footer]

...

public static $modules = ['node', 'tdd_blog', 'views'];

public function testOnlyPublishedArticlesAreShown() {
  $this->installConfig(['filter', 'tdd_blog']);

  $this->createNode(['type' => 'page', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 0]);

  $results = views_get_view_result('blog');

  $this->assertCount(1, $results);
  $this->assertEquals(2, $results[0]->_entity->id());
}

[.hide-footer]

...

public static $modules = ['node', 'tdd_blog', 'views'];

public function testOnlyPublishedArticlesAreShown() {
  $this->installConfig(['filter', 'tdd_blog']);

  $this->createNode(['type' => 'page', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 0]);

  $results = views_get_view_result('blog');

  $this->assertCount(1, $results);
  $this->assertEquals(2, $results[0]->_entity->id());
}

[.hide-footer]

...

public static $modules = ['node', 'tdd_blog', 'views'];

public function testOnlyPublishedArticlesAreShown() {
  $this->installConfig(['filter', 'tdd_blog']);

  $this->createNode(['type' => 'page', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 1]);
  $this->createNode(['type' => 'article', 'status' => 0]);

  $results = views_get_view_result('blog');

  $this->assertCount(1, $results);
  $this->assertEquals(2, $results[0]->_entity->id());
}

^ Assert Should only be one result, should be node 2 Node IDs are reset on each test method


[.hide-footer]

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
F                                                                   1 / 1 (100%)

Time: 2.16 seconds, Memory: 6.00MB

There was 1 failure:

1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown
Failed asserting that actual size 3 matches expected size 1.

/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:23

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

[.hide-footer]

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
F                                                                   1 / 1 (100%)

Time: 2.16 seconds, Memory: 6.00MB

There was 1 failure:

1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown
Failed asserting that actual size 3 matches expected size 1.

/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:23

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

  • There are no filters on the view
  • Add the filters
  • Export and save the view

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
.                                                                   1 / 1 (100%)

Time: 2.02 seconds, Memory: 6.00MB

OK (1 test, 6 assertions)

Tasks

  • Ensure the blog page exists
  • Ensure only published articles are shown
  • Ensure the articles are shown in the correct order

Task 3:

Ensure the articles are shown in the correct order


[.hide-footer]

// 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.
}

[.hide-footer]

// 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 \DateTime())->modify('+1 day')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]);

  // When I go to the blog page.

  // The articles are ordered by post date.
}

[.hide-footer]

// 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 \DateTime())->modify('+1 day')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]);

  // When I go to the blog page.
  $results = views_get_view_result('blog');

  // The articles are ordered by post date.
}

[.hide-footer]

// 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 \DateTime())->modify('+1 day')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]);

  // 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.
}

[.hide-footer]

// 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 \DateTime())->modify('+1 day')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]);
  $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]);

  // 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.
  $this->assertEquals([4, 1, 3, 2], $nids);
}

[.hide-footer]

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
F                                                                   1 / 1 (100%)

Time: 1.42 seconds, Memory: 6.00MB

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 => '3'
+    1 => '2'
+    2 => '4'
+    3 => '1'

/Users/opdavies/Code/drupal-testing-workshop/web/core/tests/Drupal/KernelTests/KernelTestBase.php:1114
/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:43

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

[.hide-footer]

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
F                                                                   1 / 1 (100%)

Time: 1.42 seconds, Memory: 6.00MB

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 => '3'
+    1 => '2'
+    2 => '4'
+    3 => '1'

/Users/opdavies/Code/drupal-testing-workshop/web/core/tests/Drupal/KernelTests/KernelTestBase.php:1114
/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:43

FAILURES!
Tests: 1, Assertions: 4, Failures: 1.

  • There is no sort order defined on the view
  • Add the sort order
  • Re-export the view

PHPUnit 6.5.8 by Sebastian Bergmann and contributors.

Testing web/modules/custom/tdd_blog/tests/src/Kernel
.                                                                   1 / 1 (100%)

Time: 1.74 seconds, Memory: 6.00MB

OK (1 test, 5 assertions)

Tasks

  • Ensure the blog page exists
  • Ensure only published articles are shown
  • Ensure 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


[.hide-footer] [.text: alignment(center)]

inline 150%


Resources

  • github.com/opdavies/drupal-module-tdd-dublin
  • drupalize.me/series/testing-drupal-7-simpletest
  • lullabot.com/articles/an-overview-of-testing-in-drupal-8
  • mediacurrent.com/blog/writing-simple-simpletest-tests-your-d7-module
  • mediacurrent.com/blog/writing-simple-phpunit-tests-your-d8-module
  • knpuniversity.com/screencast/phpunit
  • adamwathan.me/test-driven-laravel
  • laracasts.com

  • oliverdavies.uk/talks
  • oliverdavies.uk/twitter
  • oliverdavies.uk/drupal
  • oliverdavies.uk/github
  • oliverdavies.uk/youtube

[.header: alignment(center)]

Questions?


[.header: alignment(center)]

Thanks