autoscale: true build-lists: true footer: @opdavies | oliverdavies.uk theme: next, 8 [.hide-footer] # Test Driven Drupal Development with SimpleTest and PHPUnit --- ## opdavies - Web Developer and Linux System Administrator - Drupal core contributor, mentor, contrib module maintainer - Senior Drupal Developer, Appnovation --- ## Why Test? - Write better code - Write less code - Piece of mind - Ensure consistency - Drupal core requirement - --- ## Why Not Test? No time/budget to write tests. --- ## 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. -- https://www.drupal.org/core/gates#testing --- ## Testing in Drupal ### SimpleTest - Based on - In D7 core - `*.test` files - All test classes in one file --- ## 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 - - D8 core tests to change to PHPUnit - Deprecate SimpleTest, remove in D9 - "A big chunk of old tests" converted on Feb 21st --- ## 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. -- https://groups.drupal.org/node/516229 --- ## Types of Tests ### Unit Tests - Tests PHP logic - No database interaction - Fast to run --- ## Types of Tests ### Web Tests - Tests functionality - Interacts with database - Slower to run --- ## Writing Testable Code - Single responsibility principle - DRY - Dependency Injection - Interfaces --- ## Test Driven Development (TDD) - Write a test, see it fail - Write code until test passes - Repeat - Refactor when tests are green --- ## Writing Tests --- [.hide-footer] ```ini # example.info name = Example core = 7.x files[] = example.test ``` --- [.hide-footer] ```php // 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] ```php class ExampleTestCase extends DrupalWebTestCase { ... public function testSomething { $this->assertTrue(TRUE); } } ``` --- [.hide-footer] ## Creating the World ```php public function setUp() { // Enable any other required modules. parent::setUp(['foo', 'bar']); // Anything else we need to do. } ``` --- [.hide-footer] ## Creating the World ```php $this->drupalCreateUser(); $this->drupalLogin(); $this->drupalCreateNode(); $this->drupalLogout(); ``` --- ## Assertions - assertTrue - assertFalse - assertNull - assertNotNull - assertEqual --- ## Assertions - assertRaw - assertResponse - assertField - assertFieldById - assertTitle --- [.hide-footer] ## Running Tests --- ![fit](images/simpletest-1.png) --- ![fit](images/simpletest-2.png) --- ![fit](images/simpletest-3.png) --- ![fit](images/simpletest-4.png) --- ## Running SimpleTest From The Command Line [.hide-footer] ```bash # Drupal 7 $ php scripts/run-tests.sh # Drupal 8 $ php core/scripts/run-tests.sh ``` --- [.hide-footer] ## Running SimpleTest From The Command Line ```bash --color --verbose --all --module --class --file ``` --- [.hide-footer] ## Running PHPUnit From The Command Line ```bash $ phpunit $ phpunit [directory] $ phpunit --filter [method] ``` --- ## Example: Collection Class --- ## Collection Class - - Adds a `Collection` class, based on Laravel’s - Provides helper methods for array methods --- [.hide-footer] ```php $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] ```php 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] ```php 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 ```php 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 ```php public function testCollectFunction() { $this->assertEqual( get_class($this->firstCollection), 'Drupal\collection_class\Collection' ); } ``` --- [.hide-footer] ## Testing ```php public function testAll() { $this->assertEqual( array('foo', 'bar', 'baz'), $this->firstCollection->all() ); } ``` --- [.hide-footer] ## Testing ```php public function testCount() { $this->assertEqual( 3, $this->firstCollection->count() ); } ``` --- [.hide-footer] ## Testing ```php 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](images/collection-class-1.png) --- ![fit](images/collection-class-2.png) --- [.hide-footer] ## Example: Toggle Optional Fields --- ## 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%](images/toggle-optional-fields-button.png) --- [.hide-footer] ## Example ```php // 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] ```php // 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 ```php $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](images/toggle-optional-fields-1.png) --- [.hide-footer] ## Web Tests ```php 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 ```php private function assertTagsFieldNotHidden() { $this->assertFieldByName( 'field_tags[und]', NULL, t('Tags field visible.') ); } ``` --- ## Testing Hidden Fields [.hide-footer] ```php 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 ```php ... $this->drupalPost( 'node/add/article', array(), t('Show optional fields') ); $this->assertHideOptionalFieldsButtonFound(); $this->assertShowOptionalFieldsButtonNotFound(); $this->assertTagsFieldNotHidden(); } ``` --- ![fit](images/toggle-optional-fields-2.png) --- ![fit](images/toggle-optional-fields-3.png) --- ## Take Aways - Testing can produce better quality code - Writing tests is an investment - OK to start small, introduce tests gradually --- ## Questions?