It's not as hard as you think!
Guards against future code causing regressions
Provides self-documentation
Provides quality assurance for future collaborators
Reduces bugs
Provides stability when expanding and creating new features
Besides...
How much code?
/**
* Implements hook_block_info().
*/
function hello_world_block_info() {
$blocks = array();
$blocks['hello_world'] = array(
'info' => t('Hello world'),
);
return $blocks;
}
/**
* Implements hook_block_view().
*/
function hello_world_block_view($delta = '') {
$block = array();
if ($delta == 'hello_world') {
$block['subject'] = t('Hello world');
$block['content'] = t('This is the block content.');
}
return $block;
}
Not so much but...
/**
* Provides abililty to wrap the content of HTML messages in multiple langauges.
*/
class MailWrap {
/**
* Wraps HTML content to a specific line width for sending html emails via smtp.
* @param string $content HTML content of email body.
* @param integer $width Line length to enforce wrapping
* @param object $lang Language of email.
*/
public function wrapHtmlContent($content, $width = 75, $lang) {
$cut = false;
if (isset($lang) && ($lang == 'ja' || $lang == 'zh-hans')) {
$cut = true;
}
$html_obj = new simple_html_dom();
$html_obj->load($content);
foreach ($html_obj->find('text') as $element ) {
$element->innertext = $this->mbWordwrap($element->innertext, $width, "\n", $cut);
}
return $html_obj->save();
}
/**
* Wraps any string to a given number of characters.
*
* This implementation is multi-byte aware and relies on {@link
* http://www.php.net/manual/en/book.mbstring.php PHP's multibyte
* string extension}.
*
* @see wordwrap()
* @link https://api.drupal.org/api/drupal/core%21vendor%21zendframework%21zend-stdlib%21Zend%21Stdlib%21StringWrapper%21AbstractStringWrapper.php/function/AbstractStringWrapper%3A%3AwordWrap/8
* @param string $string
* The input string.
* @param int $width [optional]
* The number of characters at which $string will be
* wrapped. Defaults to 75
.
* @param string $break [optional]
* The line is broken using the optional break parameter. Defaults
* to "\n"
.
* @param boolean $cut [optional]
* If the $cut is set to TRUE
, the string is
* always wrapped at or before the specified $width. So if
* you have a word that is larger than the given $width, it
* is broken apart. Defaults to FALSE
.
* @return string
* Returns the given $string wrapped at the specified
* $width.
*/
private function mbWordwrap($string, $width = 75, $break = "\n", $cut = false) {
$string = (string) $string;
if ($string === '') {
return '';
}
$break = (string) $break;
if ($break === '') {
trigger_error('Break string cannot be empty', E_USER_ERROR);
}
$width = (int) $width;
if ($width === 0 && $cut) {
trigger_error('Cannot force cut when width is zero', E_USER_ERROR);
}
if (strlen($string) === mb_strlen($string)) {
return wordwrap($string, $width, $break, $cut);
}
$stringWidth = mb_strlen($string);
$breakWidth = mb_strlen($break);
$result = '';
$lastStart = $lastSpace = 0;
for ($current = 0; $current < $stringWidth; $current++) {
$char = mb_substr($string, $current, 1);
$possibleBreak = $char;
if ($breakWidth !== 1) {
$possibleBreak = mb_substr($string, $current, $breakWidth);
}
if ($possibleBreak === $break) {
$result .= mb_substr($string, $lastStart, $current - $lastStart + $breakWidth);
$current += $breakWidth - 1;
$lastStart = $lastSpace = $current + 1;
continue;
}
if ($char === ' ') {
if ($current - $lastStart >= $width) {
$result .= mb_substr($string, $lastStart, $current - $lastStart) . $break;
$lastStart = $current + 1;
}
$lastSpace = $current;
continue;
}
if ($current - $lastStart >= $width && $cut && $lastStart >= $lastSpace) {
$result .= mb_substr($string, $lastStart, $current - $lastStart) . $break;
$lastStart = $lastSpace = $current;
continue;
}
if ($current - $lastStart >= $width && $lastStart < $lastSpace) {
$result .= mb_substr($string, $lastStart, $lastSpace - $lastStart) . $break;
$lastStart = $lastSpace = $lastSpace + 1;
continue;
}
}
if ($lastStart !== $current) {
$result .= mb_substr($string, $lastStart, $current - $lastStart);
}
return $result;
}
}
aww yiss...
Hooks and core functions?
probably not...
Data manipulation, business critical logic, lots of custom stuff?
aww yiss...
With Homebrew:
$ brew tap homebrew/homebrew-php
$ brew install composer
PHP 5 (I use 5.5 via homebrew)
$ composer init
# fill out the stuff for your project
Make sure to set phpunit as a dev-dependency
Would you like to define your dev dependencies interactively?
Search for a package []: phpunit/phpunit
Enter the version constraint to require (or leave blank to use the latest version) []:
Using version ~4.6 for phpunit/phpunit
Your resulting composer.json should be something like this:
{
"name": "dustin/module",
"description": "A cool module",
"require-dev": {
"phpunit/phpunit": "~4.6"
},
"authors": [
{
"name": "Dustin LeBlanc",
"email": "dustin@singlebrook.com"
}
]
}
path/to/sites/all/modules/custom/
./vendor
Your config might be different.
/**
* Implements hook_node_view()
*/
function example_node_view($node, $view_mode, $langcode) {
// Should I pirate?
if (!$node->nid % 2 == 0) {
return;
}
// Get page text
$wrapper = entity_metadata_wrapper('node', $node);
$text = $wrapper->body->value();
// Call to the pirate API
$r = new HttpRequest('http://isithackday.com/arrpi.php', HttpRequest::METH_GET);
$r->addQueryData(array('text' => $text));
try {
$r->send();
if ($r->getResponseCode() == 200) {
$pirate_text = $r->getResponseBody());
}
} catch (HttpException $ex) {
drupal_set_message($ex);
}
// Replace node text
$wrapper->body->set($pirate_text);
// Return the node
$node = $wrapper->value();
}
Does all the things!
/**
* Implements hook_view().
*/
function example_node_view($node, $view_mode, $langcode) {
$pirator = new Pirator();
if ($pirator->shouldYe($node)) {
$node = $pirator->translate($node);
}
}
Only concerned with evaluating condition and executing
Our rewritten module code...
class Pirator {
/**
* Determines whether a node should be pirated.
* How bout every other node?
* @return BOOL
*/
public function shouldYe(stdClass $node) {
if ($node->nid % 2 == 0) {
return TRUE;
}
return FALSE;
}
public function translate(stdClass $node) {
$wrapper = entity_metadata_wrapper('node', $node);
$text = $this->getBody($wrapper);
$translation = $this->getTranslation($text);
$this->setContent($node, $translation);
dpm($wrapper->value());
return $wrapper->value();
}
/**
* Call to the arrpi and get a translation
*
* @param string the text to translate
*/
protected function getTranslation($text) {
$data = array(
'text' => $text,
);
$url = url('http://isithackday.com/arrpi.php', array('query' => $data));
$r = drupal_http_request($url);
if ($r->code == 200) {
return $r->data;
}
else {
drupal_set_message(t($r->error), 'error');
}
}
/**
* Get page text
*
* @param EntityValueWrapper $wrapper wrapped node
*
* @return string the node body text
*/
protected function getBody(EntityDrupalWrapper $wrapper) {
return $wrapper->body->value->value();
}
/**
* Set translated
*
* @param EntityValueWrapper $wrapper wrapped node
* @param string $translation [description]
*/
protected function setContent(stdClass $node, $translation) {
return $node->content['body'][0]['#markup'] = $translation;
}
}
Each method does one job
Object-oriented setup is easier to load with php unit
Loading and calling Drupal functions is still painful
Not all of your methods will be easy to test
In composer.json we have to make a slight tweak:
{
"name": "dustin/module",
"description": "Example Module for Cornell Drupal Camp 2015 showcasing PHPUnit in Drupal module development",
"require-dev": {
"phpunit/phpunit": "~4.6",
},
"autoload": {
"classmap": ["./path/to/custom/module/includes", "./includes/bootstrap.inc"]
},
"authors": [
{
"name": "Dustin LeBlanc",
"email": "dustin@singlebrook.com"
}
]
}
Note the addition of 'autoload'
/**
* Provides testing for Pirator class.
*/
class PiratorTest extends PHPUnit_Framework_TestCase {
protected $pirator;
protected $node;
/**
* Pre-test setup
*/
protected function setUp() {
$this->pirator = new Pirator();
}
public function testShouldYe() {
$node = new stdClass();
$node->nid = 2;
$this->assertTrue($this->pirator->shouldYe($node));
}
}
at the top of your test file...
/**
* Setup environoment to have access to Drupal functions and installed modules.
*/
define('DRUPAL_ROOT', getcwd());
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
// Bootstrap Drupal.
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
$ ./vendor/bin/phpunit
PHPUnit 4.6.4 by Sebastian Bergmann and contributors.
Configuration read from ...your config file...
.
Time: 251 ms, Memory: 16.00Mb
OK (1 test, 1 assertion)