1 of 43

2 of 43

Automated Testing:

PHPUnit all the way

Klaus Purer (klausi)

Daniel Wehner (dawehner)

Coding and Development / Core Conversation

3 of 43

Who we are

Klaus Purer // klausi

Software Engineer @ epiqo.com

Drupal Security Team

Drupal.org project applications admin

Maintainer of REST module, Coder, Rules

PHPUnit initiative

Daniel Wehner // dawehner

Drupal Developer @ Chapter Three

Views

Menu system

API-first, PHPUnit initiative

4 of 43

What is Automated Testing?

5 of 43

Manual Testing

  • Create a checklist what your site should deliver
  • Example: Check that there is a log in link on the front page
  • Make changes to your site, aka development
  • A human goes through the checklist to see if everything still works

6 of 43

Automated Testing

  • You write your checklist down as executable test code
  • Example: the test code checks that there is a log in link on the front page
  • Make changes, aka development
  • Run the test code
  • Automate test runs
  • Continuous integration

PHPUnit 4.8.11 by Sebastian Bergmann and contributors.��..........��Time: 25.14 seconds, Memory: 6.00Mb��OK (10 tests, 50 assertions)

7 of 43

Automated Testing

8 of 43

Testing levels

Unit

Tests on functions, methods, classes

Pros:

Verify individual parts

Quickly find problems in code

Fast execution

No system setup for the test run

Cons:

Rewrite on every refactoring

Complicated mocking

No guarantee that the whole system actually works

Integration

Tests on components

Pros:

Verify that components actually work together

Somewhat easy to locate bugs

Cons:

Slower execution

System setup required

No guarantee that end user features actually work

System / Functional

Tests on the complete system

Pros:

Verify that the system works as experienced by the user

Verify that the system works when code is refactored

Cons:

Very slow execution

Heavy system setup

Hard to locate origins of bugs

Prone to random test fails

Hard to change

9 of 43

A whole Testing universe

  • Testing methods: white box vs. black box testing
  • Testing types: Acceptance, Performance / Load, Security, Smoke & Sanity tests, Accessibility, Regression, Usability, Visual Regression
  • Test Driven Development (TDD)
  • Continuous Testing + Continuous Delivery
  • Property based testing

10 of 43

Testing in Drupal

11 of 43

Test frameworks

Simpletest

Our Past

In Drupal since 2008

405 tests

Only Functional tests

Served Drupal well in the past

PHPUnit

The Future

In Drupal since 2013

757 tests

Mostly Unit + Integration tests

Comes with a universe of tools (IDE etc.) ...

12 of 43

Unit Tests

  • Test in isolation
  • Good for testing logic

  • Pure functions <3
  • Speed: light

13 of 43

Unit Tests

  • Test in isolation
  • Good for testing logic

  • Pure functions <3
  • Speed: light

14 of 43

Unit Test Example

namespace Drupal\Tests\Component\Render;

/**

* Tests the HtmlEscapedText class.

*

* @coversDefaultClass \Drupal\Component\Render\HtmlEscapedText

* @group utility

*/

class HtmlEscapedTextTest extends UnitTestCase {

/**

* @covers ::count

*/

public function testCount() {

$string = 'Can I please have a <em>kitten</em>';

$escapeable_string = new HtmlEscapedText($string);

$this->assertEquals(strlen($string), $escapeable_string->count());

}

}

15 of 43

Run tests

  • Copy core/phpunit.xml.dist to core/phpunit.xml

  • Adapt environment variables

  • Run

16 of 43

Kernel Tests

  • Integration tests
  • Minimal Drupal, full API without HTTP
  • Provides a database
  • Some setup required
  • Speed: sound

17 of 43

Kernel Test Example

class LockTest extends KernelTestBase {

protected $lock;

protected function setUp() {

parent::setUp();

$this->lock = new DatabaseLockBackend($this->container->get('database'));

}

public function testBackendLockRelease() {

$success = $this->lock->acquire('lock_a');

$this->assertTrue($success, 'Could acquire first lock.');

$is_free = $this->lock->lockMayBeAvailable('lock_a');

$this->assertFalse($is_free, 'First lock is unavailable.');

}

}

18 of 43

Browser Tests

  • Functional tests
  • Installs Drupal
  • Uses a browser
  • Test actual user interaction
  • Speed: Walking

19 of 43

Browser Test Example

class UiPageTest extends RulesBrowserTestBase {

public function testCreateReactionRule() {

$account = $this->drupalCreateUser(['administer rules']);

$this->drupalLogin($account);

$this->drupalGet('admin/config/workflow/rules');

$this->clickLink('Add reaction rule');

$this->fillField('Label', 'Test rule');

$this->fillField('Machine-readable name', 'test_rule');

$this->fillField('React on event', 'rules_entity_insert:node');

$this->pressButton('Save');

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

$this->assertSession()->pageTextContains('Reaction rule Test rule has been created.');

}

}

20 of 43

JavaScript Browser Tests

  • Functional test
  • Uses an actual browser
  • Test #ajax behaviour,

autocompletion etc.

  • PhantomJS vs. Selenium
  • Pitfalls

21 of 43

JavaScript Browser Test Example

class ClickSortingAJAXTest extends JavascriptTestBase {

protected function setUp() { ... }

public function testClickSorting() {

$this->drupalGet('test-content-ajax');

$page = $this->getSession()->getPage();

// Now sort by title and check if the order changed.

$page->clickLink('Title');

$this->assertSession()->assertWaitOnAjaxRequest();

$rows = $page->findAll('css', 'tbody tr');

$this->assertCount(2, $rows);

$this->assertContains('Page A', $rows[0]->getHtml());

$this->assertContains('Page B', $rows[1]->getHtml());

}

}

22 of 43

Test ingredients

23 of 43

Setup and Teardown

protected function setUp() {

parent::setUp();

// Create a Content type and two test nodes.

$this->createContentType(['type' => 'page']);

$this->createNode(['title' => 'Page A', 'changed' => REQUEST_TIME]);

$this->createNode(['title' => 'Page B', 'changed' => REQUEST_TIME + 1000]);

}

  • Base test classes do most setup work for you
  • Create test database / configuration / test data (integration tests)
  • Create mock objects used by all test methods (unit tests)
  • Teardown almost never needed

24 of 43

Assertions

  • Compare EXPECTED with ACTUAL results
  • If an expectation is not fulfilled then the test is failed
  • PHPUnit: a failed assertion triggers an exception which stops test case execution
  • Simpletest: a failed assertion is recorded, but test execution continues
  • PHPUnit automatically asserts that no PHP notices, warnings, fatal errors are triggered and fails the test
  • dataProviders vs. multiple assertions

25 of 43

Assertion examples

PHPUnit core

$this->assertSame($expected, $actual);

$this->assertContains('foo bar baz', 'bar');

$this->assertContains('klausi', ['daniel', 'klausi', 'zsofi']);

$this->assertCount(3, ['daniel', 'klausi', 'zsofi']);

$this->assertNotNull($loaded_node);

$this->assertStringStartsWith('hurra', 'hurra, phpunit!');

$this->assertRegExp('/^[0-5]+$/', '012345');

Browser tests

$session_assert = $this->assertSession();

$session_assert->statusCodeEquals(200);

$session_assert->pageTextContains('Start');

$session_assert->addressEquals('/node/1');

$session_assert->buttonExists('Save');

$session_assert->checkboxChecked('Promote to frontpage');

https://phpunit.de/manual/current/en/appendixes.assertions.html

26 of 43

Mocking

  • Provide fake dependencies in unit tests
  • Mocking framework “Prophecy”
  • Objects implement/extend classes/interfaces
  • Dummy: Doubles that aren’t used
  • Stub: Doubles with predefined return values
  • Mock: Doubles with expected method calls
  • Spy: Mock, but check what has been called afterwards

27 of 43

Prophecy examples

// Set up a dummy because we don't want to send a real mail!

$mail_manager = $this->prophesize(MailManagerInterface::class);

$dummy = $mail_manager->reveal();

// Make it a stub by returning something.

$mail_manager->mail('admin@example.com', 'test')

->willReturn(['result' => TRUE])

// Make it a mock by adding expectations.

->shouldBeCalledTimes(1);

// SystemSendEmail is the class we want to test.

$rules_action = new SystemSendEmail($mail_manager->reveal());

$rules_action->setContextValue('to', 'admin@example.com')

->setContextValue('subject', 'test');

$rules_action->execute();

// Spy on the object afterwards.

$mail_manager->mail('other@example.com', 'wrong!')

->shouldNotHaveBeenCalled();

28 of 43

Implicit assertions in mocks

There was 1 error:

1) Drupal\Tests\rules\Integration\Action\SystemSendEmailTest::testSendMailToOneRecipient

Prophecy\Exception\Call\UnexpectedCallException: Method call:

- mail("rules", "other@example.com")

on Double\MailManagerInterface\P22 was not expected, expected calls were:

- mail(exact("rules"), exact("admin@example.com"))

/home/klausi/workspace/drupal-8/modules/rules/src/Plugin/RulesAction/SystemSendEmail.php:122

/home/klausi/workspace/drupal-8/modules/rules/src/Core/RulesActionBase.php:126

/home/klausi/workspace/drupal-8/modules/rules/tests/src/Integration/Action/SystemSendEmailTest.php:94

FAILURES!

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

29 of 43

Test Fixtures

  • Well known data sets
  • Repeatable and reliable test results
  • Given input => expected output
  • PHPUnit data providers with @dataProvider annotation

30 of 43

@dataProvider example

/**

* @dataProvider providerToString

*/

public function testToString($text, $expected, $message) {

$escapeable_string = new HtmlEscapedText($text);

$this->assertSame($expected, (string) $escapeable_string, $message);

$this->assertSame($expected, $escapeable_string->jsonSerialize());

}

public function providerToString() {

// Checks that invalid multi-byte sequences are escaped.

$tests[] = array("Foo\xC0barbaz", 'Foo�barbaz', 'Escapes invalid sequence "Foo\xC0barbaz"');

$tests[] = array("\xc2\"", '�&quot;', 'Escapes invalid sequence "\xc2\""');

$tests[] = array("Fooÿñ", "Fooÿñ", 'Does not escape valid sequence "Fooÿñ"');

return $tests;

}

31 of 43

Testing exceptions

Ugly:

try {

my_example('test');

$this->fail('Exception was not thrown!');

}

catch (MyException $e) {

$this->assertTrue(TRUE, 'Exception was thrown as expected!');

}

Nice:

$this->setExpectedException(MyException::class);

my_example('test');

Do not use @expectedException annotation! (deprecated)

32 of 43

Writing good tests

33 of 43

Assertions

  • Specific assertions

$this->assertEquals(TRUE, $giraffe->isBiggerThan(100));

$this->assertInstanceOf(ExampleClass::class, $object);

$this->assertJsonStringEqualsJsonString();

  • Less assertions per test

34 of 43

Random data

  • Add special characters always: +]w|GdESb!>&k6?8r*^v
  • Potentially harder to debug
  • User Input
  • Edge cases

35 of 43

Risky Tests

  • Tests without assertions
  • Unintentional code coverage
  • Output
  • Slow code
  • Global state

<?php

function functionToTest() {

print "here";

// Sleep should take milliseconds, just like timeout in JS.

sleep(20);

$GLOBALS['user'] = 'test';

return 123;

}

function test() {

functionToTest();

}

36 of 43

t() calls for translation

  • Never call t() in test code
  • Always assert plain strings
  • Locale is not enabled
  • The translation system is tested in dedicated tests
  • Drupal core violates this a lot :-(

37 of 43

Test simplicity

Bad:

$unread_topics = $this->container->get('forum_manager')->unreadTopics($this->forum['tid'],

$this->editAnyTopicsUser->id());

$unread_topics = \Drupal::translation()->formatPlural($unread_topics, '1 new post', '@count new posts');

$xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="forum__topics"]//a', $forum_arg);

$this->assertFieldByXPath($xpath, $unread_topics, 'Number of unread topics found.');

Good:

$this->assertSession()->linkExists(‘6 new posts’, 0);

38 of 43

Test abstractions

  • Readability first
  • Try to not design the same abstractions as your code
  • Other abstractions for tests are fine

39 of 43

PHPUnit battleplan in Drupal core

40 of 43

Converting Simpletests to Browser tests

  • Work on a sandbox to get conversions running
  • Provide/Expand backwards compatibility layer (and forward compatibility?)
  • Find random failures
  • Regularly automated update of big bang patch
  • Commit in 8.3 alpha (February 2017)

41 of 43

Future of Simpletest UI?

Future of run-tests.sh?

Use selenium/webdriver?

Deprecate / Remove Simpletest?

42 of 43

JOIN US FOR

CONTRIBUTION SPRINTS

First Time Sprinter Workshop - 9:00-12:00 - Room Wicklow2A

Mentored Core Sprint - 9:00-18:00 - Wicklow Hall 2B

General Sprints - 9:00 - 18:00 - Wicklow Hall 2A

43 of 43

Evaluate This Session

THANK YOU!

events.drupal.org/dublin2016/schedule

WHAT DID YOU THINK?