648 lines
9.5 KiB
Markdown
648 lines
9.5 KiB
Markdown
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 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
|
||
|
||
- <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?
|