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?
|