talks/tdd-test-driven-drupal/slides.md
2017-10-30 01:08:00 +00:00

18 KiB
Raw Blame History

autoscale: true build-lists: true theme: next, 9

[fit] TDD - Test
Driven Drupal


  • PHP code
  • Mixture of D7 and D8
  • SimpleTest (D7)
  • PHPUnit (D8)

[.build-lists: false]

  • Senior Developer at Microserve
  • Contrib module maintainer
  • Occasional core contributor
  • Sticker collector and elePHPant herder
  • @opdavies
  • oliverdavies.uk

right


inline fit

^ First experience of testing with a real module. Used on 11,046 sites (84 D5, 7,094 D6, 3,868 D7). Currently used on 28,398 (10 D5, 2,207 D6, 23,206 D7, 2,975 D8). Tests crucial to preventing regressions when adding new features or fixing bugs.


Why Test?

^ 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


Why Not Test?

  • Don't know how
  • No time/budget to write tests

^ "I'd love to write tests, but I don't have the time to learn."


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]


Testing in Drupal - SimpleTest


Testing in Drupal - PHPUnit

  • Used in other PHP projects (e.g. Symfony, Laravel)
  • In D8 core, but not default
  • *.php files
  • One test class per file

The PHPUnit Initiative


The PHPUnit Initiative

As part of the PHPUnit initiative a considerable part of Simpletests will be converted to PHPUnit based browser tests on February 21st 2017. A backwards compatibility layer has been implemented so that many Simpletests can be converted by just using the new BrowserTestBase base class and moving the test file. There is also a script to automatically convert test files in the conversion issue.

Developers are encouraged to use BrowserTestBase instead of Simpletest as of Drupal 8.3.0, but both test systems are fully supported during the Drupal 8 release cycle.

The timeline for the deprecation of Simpletest's WebTestBase is under discussion.

[.footer: https://groups.drupal.org/node/516229]


Types of Tests

Unit Tests

  • UnitTestCase
  • Tests PHP logic
  • No database interaction
  • Fast to run

Types of Tests

Unit Tests

Pros:

  • Verify individual parts
  • Quickly find problems in code
  • Fast execution
  • No system setup for the test run

Types of Tests

Unit Tests

Cons:

  • Rewrite on every refactoring
  • Complicated mocking
  • No guarantee that the whole system actually works

Types of Tests

Kernel Tests

  • Kernel tests are integration tests that test on components. You can install modules.
  • KernelTestBase

[.footer: https://www.drupal.org/docs/8/testing/types-of-tests-in-drupal-8]


Types of Tests

Kernel Tests

Pros:

  • Verify that components actually work together
  • Somewhat easy to locate bugs

Types of Tests

Kernel Tests

Cons:

  • Slower execution
  • System setup required
  • No guarantee that end user features actually work

Types of Tests

Web/Functional/FunctionalJavascript Tests

  • DrupalWebTestCase (D7)
  • WebTestBase, BrowserTestBase, JavascriptTestBase (D8)
  • Tests functionality
  • Interacts with database
  • Slower to run
  • With/without JavaScript (D8)

^ - Use JavascriptTestBase when you need to test how the system works for a user with Javascript enabled.


Test Driven Development (TDD)

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

right 100%

[.footer: http://www.agilenutshell.com/assets/test-driven-development/tdd-circle-of-life.png]

^ "Grab for green." Not the only way Write code beforehand and test afterwards Write code first, comment out/reset branch, then TDD


Porting Modules to Drupal 8

  • Make a new branch git checkout --orphan 8.x-1.x
  • Add/update the tests
  • Write code to make the tests pass
  • Refactor
  • Repeat

Writing Tests (SimpleTest)


[.hide-footer]

# example.info

name = Example
core = 7.x
files[] = example.test

[.hide-footer]

// example.test

class ExampleTestCase extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Example tests',
      'description' => 'Web tests for the example module.',
      'group' => 'Example',
    );
  }

}

[.hide-footer]

class ExampleTestCase extends DrupalWebTestCase {

  ...

  public function testSomething {
    $this->assertTrue(TRUE);
  }

}

Writing Tests (PHPUnit)

  • No need to load test classes expicitly.
  • Add classes into tests/src directory.
  • Extend BrowserTestBase.
  • No getInfo method.

^ Classes get autoloaded PSR-4


[.hide-footer]

Creating the World

public function setUp() {
  // Enable any other required modules.
  parent::setUp(['foo', 'bar']);

  // Anything else we need to do.
}

[.hide-footer]

Creating the World

$this->drupalCreateUser();

$this->drupalLogin();

$this->drupalCreateNode();

$this->drupalLogout();

Assertions

  • assertTrue
  • assertFalse
  • assertNull
  • assertNotNull
  • assertEqual assertEquals

Assertions

  • assertRaw
  • assertResponse assertSession()->statusCodeEquals()
  • assertField
  • assertFieldById
  • assertTitle

[fit] Running Tests


SimpleTest UI


fit


fit


fit


fit


Running SimpleTest From The Command Line

[.hide-footer]

# Drupal 7
$ php scripts/run-tests.sh

# Drupal 8
$ php core/scripts/run-tests.sh

[.hide-footer]

Running SimpleTest From The Command Line

--color

--verbose

--all

--module

--class

--file

[.hide-footer]

Running PHPUnit From The Command Line

$ vendor/bin/phpunit

$ vendor/bin/phpunit [directory]

$ vendor/bin/phpunit --filter [method]

Example: Collection Class


Collection Class

^ xautoload gives PSR-4 namespaces and autoloading similar to Drupal 8.


[.hide-footer]

$collection = collect([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]

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

  ...

[.hide-footer]

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]

Testing

public function setUp() {
  $this->firstCollection = collect(['foo', 'bar', 'baz']);

  $this->secondCollection = collect([
      array('title' => 'Foo', 'status' => 1),
      array('title' => 'Bar', 'status' => 0),
      array('title' => 'Baz', 'status' => 1)
  ]);

  parent::setUp();
}

[.hide-footer]

Testing

public function testCollectFunction() {
  $this->assertEqual(
    get_class($this->firstCollection),
    'Drupal\collection_class\Collection'
  );
}

[.hide-footer]

Testing

public function testAll() {
  $this->assertEqual(
    array('foo', 'bar', 'baz'),
    $this->firstCollection->all()
  );
}

[.hide-footer]

Testing

public function testCount() {
  $this->assertEqual(
    3,
    $this->firstCollection->count()
  );
}

[.hide-footer]

Testing

public function testMerge() {
  $first = collect(array('a', 'b', 'c'));
  $second = collect(array('d', 'e', 'f'));

  $this->assertEqual(
    array('a', 'b', 'c', 'd', 'e', 'f'),
    $first->merge($second)->all()
  );
}

fit


fit


[.hide-footer]

Example: Toggle Optional Fields


Toggle Optional Fields

  • http://dgo.to/toggle_optional_fields
  • Adds a button to toggle optional fields on node forms using form alters
  • Possible to override using an custom alter hook
  • Uses unit and web tests

right 85%


[.hide-footer]

Example

// Looping through available form elements...

// Only affect fields.
if (!toggle_optional_fields_element_is_field($element_name)) {
  return;
}

$element = &$form[$element_name];

if (isset($overridden_fields[$element_name])) {
  return $element['#access'] = $overridden_fields[$element_name];
}

// If the field is not required, disallow access to hide it.
if (isset($element[LANGUAGE_NONE][0]['#required'])) {
  return $element['#access'] = !empty($element[LANGUAGE_NONE][0]['#required']);
}

What to Test?

  • Functional: Are the correct fields shown and hidden?
  • Unit: Is the field name check returning correct results?

Unit Tests

[.hide-footer]

// Returns TRUE or FALSE to indicate if this is a field.

function toggle_optional_fields_element_is_field($name) {
  if (in_array($name, array('body', 'language'))) {
    return TRUE;
  }

  return substr($name, 0, 6) == 'field_';
}

[.hide-footer]

Unit Tests

$this->assertTrue(
  toggle_optional_fields_element_is_field('field_tags')
);

$this->assertTrue(
  toggle_optional_fields_element_is_field('body')
);

$this->assertFalse(
  toggle_optional_fields_element_is_field('title')
);

fit


[.hide-footer]

Web Tests

public function setUp() {
  parent::setUp();

  $this->drupalLogin(
    $this->drupalCreateUser(array(
      'create article content',
      'create page content'
    ));
  );

  // Enable toggling on article node forms.
  variable_set('toggle_optional_fields_node_types', array('article'));

  $this->refreshVariables();
}

[.hide-footer]

Custom Assertions

private function assertTagsFieldNotHidden() {
  $this->assertFieldByName(
    'field_tags[und]',
    NULL,
    t('Tags field visible.')
  );
}

Testing Hidden Fields

[.hide-footer]

public function testFieldsHiddenByDefault() {
  variable_set('toggle_optional_fields_hide_by_default', TRUE);

  $this->refreshVariables();

  $this->drupalGet('node/add/article');

  $this->assertShowOptionalFieldsButtonFound();
  $this->assertHideOptionalFieldsButtonNotFound();
  $this->assertTagsFieldHidden();

  ...

[.hide-footer]

Testing Hidden Fields

  ...

  $this->drupalPost(
    'node/add/article',
    array(),
    t('Show optional fields')
  );

  $this->assertHideOptionalFieldsButtonFound();
  $this->assertShowOptionalFieldsButtonNotFound();
  $this->assertTagsFieldNotHidden();
}

fit


fit


[fit] Building a new
D8 module
with TDD


As a site visitor

I want to see a list of all published pages at /pages

Ordered alphabetically by title.


# tdd_dublin.info.yml

name: DrupalCamp Dublin test
core: 8.x
type: module

// tests/src/Functional/ListingPageTest.php

class ListingPageTest extends BrowserTestBase {

  protected static $modules = ['tdd_dublin'];

  public function testListingPageExists() {
    $this->drupalGet('pages');

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

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests/
E

Time: 25.94 seconds, Memory: 6.00MB

There was 1 error:

1) PageListTest::testListingPage
Behat\Mink\Exception\ExpectationException: Current response status code is 404,
but 200 expected.

/var/www/vendor/behat/mink/src/WebAssert.php:770
/var/www/vendor/behat/mink/src/WebAssert.php:130
/var/www/modules/tdd_dublin/tests/src/PageListTest.php:11

  • Add the view.
  • Copy the config into config/install.

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests/
E

Time: 19.07 seconds, Memory: 6.00MB

There was 1 error:

1) PageListTest::testListingPage
Drupal\Core\Config\UnmetDependenciesException:
Configuration objects provided by <em class="placeholder">tdd_dublin</em>
have unmet dependencies:
<em class="placeholder">node.type.page (node),
views.view.pages (node, views)</em>

name: DrupalCamp Dublin tests
core: 8.x
type: module

dependencies:
  - drupal:node
  - drupal:views

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests/
.

Time: 29.58 seconds, Memory: 6.00MB

OK (1 test, 1 assertion)

public function testOnlyPublishedPagesAreShown() {
  // Given I have a mixture of published and unpublished pages,
  // as well as other types of content.

  // When I view the pages list.

  // I should only see the published pages.
}

public function testOnlyPublishedPagesAreShown() {
  $this->drupalCreateContentType(['type' => 'article']);

  $this->drupalCreateNode(['type' => 'page', 'status' => TRUE]);

  $this->drupalCreateNode(['type' => 'article']);

  $this->drupalCreateNode(['type' => 'page', 'status' => FALSE]);

  // When I view the pages list.

  // I should only see the published pages.
}

public function testOnlyPublishedPagesAreShown() {
  ...

  $results = views_get_view_result('pages');

  $nids = collect($results)->pluck('nid')->all();
  // [1, 3]

  // I should only see the published pages.
}

public function testOnlyPublishedPagesAreShown() {
  ...

  $results = views_get_view_result('pages');

  $nids = collect($results)->pluck('nid')->all();
  // [1, 3]

  $this->assertEquals([1], $nids);
}

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
--filter=testOnlyPublishedPagesAreShown
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests
F

Time: 26.4 seconds, Memory: 6.00MB

There was 1 failure:

1) PageListTest::testOnlyPublishedPagesAreShown
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
+    1 => '3'
 )

/var/www/core/tests/Drupal/Tests/BrowserTestBase.php:1240
/var/www/modules/tdd_dublin/tests/src/PageListTest.php:25

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

[.build-lists: false]

  • Edit the view
  • Add the status filter
  • Update the module config

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
--filter=testOnlyPublishedPagesAreShown
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests
.

Time: 26.53 seconds, Memory: 6.00MB

OK (1 test, 3 assertions)

public function testPagesAreOrderedAlphabetically() {
  // Given I have multiple pages with different titles.

  // When I view the pages list.

  // I see the pages in the correct order.
}

public function testPagesAreOrderedAlphabetically() {
  $this->drupalCreateNode(['title' => 'Page A']);
  $this->drupalCreateNode(['title' => 'Page D']);
  $this->drupalCreateNode(['title' => 'Page B']);
  $this->drupalCreateNode(['title' => 'Page C']);

  $results = views_get_view_result('pages');

  $nids = collect($results)->pluck('nid')->all();

  $this->assertEquals([1, 3, 4, 2], $nids);
}

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests 
-filter=testPagesAreOrderedAlphabetically
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests
F

Time: 28.03 seconds, Memory: 6.00MB

There was 1 failure:

1) PageListTest::testPagesAreOrderedAlphabetically
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
-    1 => 3
-    2 => 4
-    3 => 2
+    0 => '1'
+    1 => '2'
+    2 => '3'
+    3 => '4'
 )

/var/www/core/tests/Drupal/Tests/BrowserTestBase.php:1240
/var/www/modules/tdd_dublin/tests/src/PageListTest.php:36

  • Edit the view
  • Remove the default sort criteria (created on)
  • Add new sort criteria
  • Update the module config

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
--filter=testPagesAreOrderedAlphabetically
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests
.

Time: 27.67 seconds, Memory: 6.00MB

OK (1 test, 2 assertions)

docker@cli:/var/www/core$ ../vendor/bin/phpunit ../modules/tdd_dublin/tests
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing ../modules/tdd_dublin/tests
...

Time: 1.17 minutes, Memory: 6.00MB

OK (3 tests, 6 assertions)

Takeaways

  • Testing has made me a better developer
  • Testing can produce better quality code
  • 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


Thanks!

Questions?

@opdavies

oliverdavies.uk