29513736 x 92842033 _________ ?
write a bubble sort
write Drupal 8
"[I]t's a situational thing - the team decides what makes sense to be a unit for the purposes of their understanding of the system and its testing."— Martin Fowler, martinfowler.com
A unit test verifies a function's correctness
against a specification.
A unit is...
t
)__construct
)update.php
)...one thing
There are two challenges with testing a unit:
... anything that makes it not a pure function.
Test doubles are the tools we use to test code that has shared state and/or side effects.
$dummy = $this->getMockBuilder('SomeClass')->getMock();
$cut = new ClassUnderTest($dummy);
$response = $cut->methodDoesntNeedDummy();
// Make an assertion.
... or just use null
?
$fs = new FakeFileSystem();
$cut = new ClassUnderTest($fs);
$response = $cut->write('A string going to a log.');
// Make an assertion.
Example: write a fake for a logger.
$stub = $this->getMockBuilder('User')->getMock();
$stub->method('getFirstName')->willReturn('First');
$stub->method('getLastName')->willReturn('Last');
$cut = new ClassUnderTest($stub);
$fullName = $cut->getFullName();
$this->assertEquals('First Last', $fullName);
Like a dummy, but is called and returns values.
$user = $this->prophet->prophesize('User');
$cut = new ClassUnderTest($user->reveal());
$cut->getFullName();
$user->getFirstName()->shouldHaveBeenCalled();
$user->getLastName()->shouldHaveBeenCalled();
Like a stub, but captures calls.
$stub = $this->getMockBuilder('User')->getMock();
$stub->expects($this->once())->method('getFirstName')->willReturn('First');
$stub->expects($this->once())->method('getLastName')->willReturn('Last');
$cut = new ClassUnderTest($stub);
$fullName = $cut->getFullName();
Like a spy, but runs assertions on execution of CUT.
It depends if you're doing
Sociable or Solitary unit testing.
Source: Jay Fields, "Working Effectively With Unit Tests"
A boundary is "a database, a queue,
another system, or even an ordinary class
if that class is 'outside' the area your trying
to work with or are responsible for"— William E. Caputo, www.williamcaputo.com
Point of confusion: these can all be written in PHPUnit.
Source: Cunningham Ward, c2.com
Drupal\Core\Batch\BatchStorage
public function load($id) {
$this->session->start();
$batch = $this->connection->query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
':bid' => $id,
':token' => $this->csrfToken->get($id),
))->fetchField();
if ($batch) {
return unserialize($batch);
}
return FALSE;
}
public function test_load_starts_session() { $statement = $this->getMock(StatementInterface::class); $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->getMock(); $connection->method('query')->willReturn($statement); $session = $this->getMock(SessionInterface::class); $session->expects($this->once())->method('start'); $csrf_token = $this->getMockBuilder(CsrfTokenGenerator::class) ->disableOriginalConstructor() ->getMock(); $batch_storage = new BatchStorage($connection, $session, $csrf_token); $batch_storage->load(123); }
public function test_load_starts_session() { $prophet = new Prophet(); $statement = $this->getMock(StatementInterface::class); $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->getMock(); $connection->method('query')->willReturn($statement); $session = $prophet->prophesize(SessionInterface::class); $csrf_token = $this->getMockBuilder(CsrfTokenGenerator::class) ->disableOriginalConstructor() ->getMock(); $batch_storage = new BatchStorage($connection, $session->reveal(), $csrf_token); $batch_storage->load(123); $session->start()->shouldHaveBeenCalled(); }
Drupal\Core\EventSubscriber\MaintenanceModeSubscriber
public function onKernelRequestMaintenance(GetResponseEvent $event) {
$route_match = RouteMatch::createFromRequest($event->getRequest());
if ($this->maintenanceMode->applies($route_match)) {
\Drupal::service('page_cache_kill_switch')->trigger();
if (!$this->maintenanceMode->exempt($this->account)) {
drupal_maintenance_theme();
$content = Xss::filterAdmin(SafeMarkup::format($this->config->get('system.maintenance')->get('message'), array(
'@site' => $this->config->get('system.site')->get('name'),
)));
$response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $content], $this->t('Site under maintenance'), 'maintenance_page');
$response->setStatusCode(503);
$event->setResponse($response);
}
else {
if ($route_match->getRouteName() != 'system.site_maintenance_mode') {
if ($this->account->hasPermission('administer site configuration')) {
$this->drupalSetMessage($this->t('Operating in maintenance mode. Go online.', array('@url' => $this->urlGenerator->generate('system.site_maintenance_mode'))), 'status', FALSE);
}
else {
$this->drupalSetMessage($this->t('Operating in maintenance mode.'), 'status', FALSE);
}
}
}
}
}
\Drupal::service
drupal_maintenance_theme()
Xss::filterAdmin
SafeMarkup::format
Drupal\Core\EventSubscriber\MaintenanceModePageOverrideSubscriber
Create an interface to implement later
Drupal\Core\Site\MaintenanceModePageInterface
namespace Drupal\Core\Site;
/**
* Defines the interface for a maintenance mode page.
*/
interface MaintenanceModePageInterface {
/**
* Returns a Response object.
*
* @return \Drupal\Core\Render\HtmlResponse
* The response object for a maintenance page.
*/
public function getResponse();
}
Create a subscriber to override page rendering
Drupal\Core\EventSubscriber\MaintenanceModePageOverrideSubscriber
public function onKernelRequestMaintenance(GetResponseEvent $event) {
$route_match = RouteMatch::createFromRequest($event->getRequest());
if ($this->maintenanceMode->applies($route_match)) {
if (!$this->maintenanceMode->exempt($this->account)) {
$response = $this->maintenancePage->getResponse();
$event->setResponse($response);
}
}
}
public function test_applicable_page_sets_maintenance_page_response() { $prophet = new Prophet(); $account = $this->getMock(AccountInterface::class); $maintenance_mode = $this->getMock(MaintenanceModeInterface::class); $maintenance_mode->method('applies')->willReturn(true); $maintenance_page = $this->getMock(MaintenanceModePageInterface::class); $response = $this->getMock(HtmlResponse::class); $maintenance_page->method('getResponse')->willReturn($response); $subscriber = new MaintenanceModePageOverrideSubscriber($maintenance_mode, $account, $maintenance_page); $request = $this->getMock(Request::class); $request->attributes = $this->getMock(ParameterBag::class); $event = $prophet->prophesize(GetResponseEvent::class); $event->setResponse(Argument::cetera())->willReturn(); $event->getRequest()->willReturn($request); $subscriber->onKernelRequestMaintenance($event->reveal()); $event->setResponse($response)->shouldHaveBeenCalled(); }
What is this test doing!?
class UserMother {
public function getJohnDoe() {
var $user = new User();
$user->setFirstName('John');
$user->setLastName('Doe');
return $user;
}
}
class UserBuilder {
protected $user;
public function __construct() {
$this->user = new User();
}
public function withNoPosts() {
foreach ($this->user->getPosts() as $post) {
$this->user->removePost($post);
}
return $this;
}
public function with10Posts() {
for (var $i = 0; $i < 10; $i++) {
$this->user->addPost(new Post());
}
return $this;
}
public function build() {
return $this->user;
}
}
class UserTest extends \PHPUnit_Framework_TestCase {
use UserAssertions;
public function test_something_complicated() {
// Assemble...
// Activate...
$this->assertHasNoPosts($user);
}
}
Bad:
$this->assertEquals('' . $something . '', $value);
Good:
$this->assertEquals('A string literal', $value);
Which assertion failed? I have no idea.
Bad:
$this->assertEquals('John', $user->getFirstName());
$this->assertEquals('Doe', $user->getLastName());
Good:
$this->assertEquals('John', $user->getFirstName());
Good:
$this->assertName('John', 'Doe', $user);
I didn't change what this CUT!? What happened?
Why am I repeating myself so much?
Writing these tests takes way too long.
I spend more time changing tests than code!!!
"If it stinks, change it."— Grandma Beck, discussing child-rearing philosophy, "Refactoring"
Solitary unit testing makes bad OOP stinky.
"Writing unit tests is reinventing
functional programming
in non-functional languages."— Christian S., noss.github.io
Solitary unit testing motivates us towards pure functions while allowing us to keep the benefits of OOP.
"It should not be underestimated how much easier these functions are to test than their state-filled counterparts."— Simon Honeywell, "Functional Programming in PHP"
means fewer merge conflicts (D8 feature branches?)
means higher collaboration
means faster development
means quicker innovation
means closer to "Drupal 2020" ready (Crell's session)
means cake + eating
Slides are available at
github.com/josephdpurcell/principles-of-solitary-unit-testing