talks/tdd-test-driven-drupal/2017-03-04-drupalcamp-london/slides.md

648 lines
9.5 KiB
Markdown
Raw Normal View History

2017-07-01 23:51:42 +00:00
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 - <https://www.drupal.org/core/gates#testing>
---
## 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 <http://www.SimpleTest.org>
- 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
- <https://www.drupal.org/node/2807237>
- 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
- <http://dgo.to/collection_class>
- Adds a `Collection` class, based on Laravels
- 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
- <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%](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?