Automated Testing:
PHPUnit all the way
Klaus Purer (klausi)
Daniel Wehner (dawehner)
Coding and Development / Core Conversation
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
What is Automated Testing?
Manual Testing
Automated Testing
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.��..........��Time: 25.14 seconds, Memory: 6.00Mb��OK (10 tests, 50 assertions)
Automated Testing
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
A whole Testing universe
Testing in Drupal
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.) ...
Unit Tests
Unit Tests
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());
}
}
Run tests
Kernel Tests
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.');
}
}
Browser Tests
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.');
}
}
JavaScript Browser Tests
autocompletion etc.
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());
}
}
Test ingredients
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]);
}
Assertions
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
Mocking
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();
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.
Test Fixtures
@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\"", '�"', 'Escapes invalid sequence "\xc2\""');
$tests[] = array("Fooÿñ", "Fooÿñ", 'Does not escape valid sequence "Fooÿñ"');
return $tests;
}
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)
Writing good tests
Assertions
$this->assertEquals(TRUE, $giraffe->isBiggerThan(100));
$this->assertInstanceOf(ExampleClass::class, $object);
$this->assertJsonStringEqualsJsonString();
Random data
Risky Tests
<?php
function functionToTest() {
print "here";
// Sleep should take milliseconds, just like timeout in JS.
sleep(20);
$GLOBALS['user'] = 'test';
return 123;
}
function test() {
functionToTest();
}
t() calls for translation
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);
Test abstractions
PHPUnit battleplan in Drupal core
Converting Simpletests to Browser tests
Future of Simpletest UI?
Future of run-tests.sh?
Use selenium/webdriver?
Deprecate / Remove Simpletest?
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
Evaluate This Session
THANK YOU!
events.drupal.org/dublin2016/schedule
WHAT DID YOU THINK?