Principles of
Solitary Unit Testing

– DrupalCon Barcelona 2015 –

by Joseph D. Purcell

Who am I?

  • Engineer at Palantir
  • Part of web dev since 2003
  • <3 learning and teaching

Who are you?

  • You have some idea of unit testing.
  • You want to be better at it.
  29513736
x 92842033
  _________

  ?
            
write a bubble sort
            
write Drupal 8
            

We need processes
to help us do things well.

  • Algorithms help you do arithmetic.
  • Test Driven Development (TDD) helps you write software.
  • Solitary Unit Testing helps you write well designed software.

Overview

  1. Definition
  2. Principles
  3. Test Smells
  4. Motivations

I. Definition

What is a "unit test"?

"[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...

  • a function (e.g. t)
  • a method on a class (e.g. __construct)
  • a script (.e.g update.php)

...one thing

There are two challenges with testing a unit:

  1. indirect inputs (shared state)
  2. indirect outputs (side effects)


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

Test Doubles

Dummy
never called
Fake
is called
Stub
provide indirect inputs
Spy
provide indirect inputs, capture indirect outputs
Mock
provide indirect inputs, verify indirect outputs

Dummy

              
  $dummy = $this->getMockBuilder('SomeClass')->getMock();
  $cut = new ClassUnderTest($dummy);

  $response = $cut->methodDoesntNeedDummy();

  // Make an assertion.
              
            

... or just use null?

Fake

              
  $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

              
  $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.

Spy

              
  $user = $this->prophet->prophesize('User');
  $cut = new ClassUnderTest($user->reveal());

  $cut->getFullName();

  $user->getFirstName()->shouldHaveBeenCalled();
  $user->getLastName()->shouldHaveBeenCalled();
              
            

Like a stub, but captures calls.

Mock

              
  $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.

Recap of Test Doubles

Dummy
never called
Fake
is called
Stub
provide indirect inputs
Spy
provide indirect inputs, capture indirect outputs
Mock
provide indirect inputs, verify indirect outputs

What should you double?

It depends if you're doing
Sociable or Solitary unit testing.

Sociable

  1. Cross some boundaries
  2. One or more concrete classes during test

vs

Solitary

  1. Never cross boundaries
  2. One concrete class during test

Source: Jay Fields, "Working Effectively With Unit Tests"

What is a boundary?

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

What does "one concrete class" mean?

  • Use a double for dependencies.
  • Collaborators that return objects
    should return doubles.
  • No statics.
  • Caveat: value objects don't count.

Consider three levels of testing:

  1. Solitary (e.g. UnitTestBase)
  2. Sociable (e.g. KernelTestBase)
  3. Functional (e.g. BrowserTestBase)


Point of confusion: these can all be written in PHPUnit.

II. Principles

Layout of a Unit Test

Arrange
Arrange all necessary preconditions and inputs.
Act
Act on the object or method under test.
Assert
Assert that the expected results have occurred.

Source: Cunningham Ward, c2.com

Example 1

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;
  }
              
            

Let's test using a mock

  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);
  }
            

Problem: we fail to assert last.

Let's test using a spy

  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();
  }
            

Example 2

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);
          }
        }
      }
    }
  }
              
            

We can't solitary test this.

  1. \Drupal::service
  2. drupal_maintenance_theme()
  3. Xss::filterAdmin
  4. SafeMarkup::format

Let's...

  1. Delete the untestable code and re-implement later.
  2. Break the subscriber in two:
    • setting response
    • showing a message

Example 2: Revision 1, Part 1

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);
      }
    }
  }
              
            

Improvements:

  • Now our class does one thing
  • Which means we can test one thing
  • The class now has 3 dependencies instead of 6
  • It's more readable
  • It's easier to test
  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();
  }
            

III. Test Smells

Obscure Test

What is this test doing!?

Try: Object Mother

              
              class UserMother {
                public function getJohnDoe() {
                  var $user = new User();
                  $user->setFirstName('John');
                  $user->setLastName('Doe');
                  return $user;
                }
              }
              
            

Try: Data Builder

              
              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;
                }
              }
              
            

Try: Custom Assertion

              
              class UserTest extends \PHPUnit_Framework_TestCase {

                use UserAssertions;

                public function test_something_complicated() {
                  // Assemble...

                  // Activate...

                  $this->assertHasNoPosts();
                }
              }
              
            

Try: Expect Literals

Bad:

              
                $this->assertEquals('
' . $something . '
', $value);

Good:

              
                $this->assertEquals('
A string literal
', $value);

Assertion Roulette

Which assertion failed? I have no idea.

Try: One assertion per test

Bad:

              
                $this->assertEquals('John', $user->getFirstName());
                $this->assertEquals('Doe', $user->getLastName());
              
            

Good:

              
                $this->assertEquals('John', $user->getFirstName());
              
            

Good:

              
                $this->assertName('John', 'Doe', $user);
              
            

Fragile Test

I didn't change what this CUT!? What happened?

Try:

  • Eliminate dependencies
  • Eliminate side effects
  • Eliminate shared state
  • Avoid test over-specification

Test Code Duplication

Why am I repeating myself so much?

Writing these tests takes way too long.

Try:

  • Object Mother
  • Data Builder
  • Add abstractions where there is high afferent coupling

High Test Maintenance Cost

I spend more time changing tests than code!!!

Try:

  • Use the right test strategy for the job
    Solitary unit testing isn't a testing panacea!
  • Remove negative or low ROI tests
  • Object Mother / Data Builder
  • Add abstractions where there is high coupling
  • Avoid test over-specification
"If it stinks, change it."

— Grandma Beck, discussing child-rearing philosophy, "Refactoring"

Solitary unit testing makes bad OOP stinky.

IV. Motivations

Solitary unit testing enables
higher code coverage.

Solitary unit testing promotes decoupling.

Solitary unit testing is faster
than sociable unit testing.

Solitary unit testing enables
better software designs.

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

Ok, but where do I start?

Drupal 8 code metrics generated by phpmetrics.org, see results.

Why is Symfony so successful for re-use?

  • It's good object oriented code.
  • No surprise: the tests are mostly (entirely?) solitary.

Recap

  • Solitary Unit Testing:
    • Never cross boundaries
    • One concrete class during test
  • A boundary is an indirect input or output.
  • Use Test Doubles to draw the boundary.

Solitary testable code means lower coupling

  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

Come Sprint on Friday!

  • All skillsets welcome!
  • First-timers: come find a mentor in the morning.

Feedback is Welcome!

events.drupal.org/barcelona2015/sessions/principles-solitary-unit-testing

References

  1. "Mental Calculation World Ranking Lists", Marc Jornet Sanz multiplying 8-digit numbers
  2. "UnitTest", Martin Fowler
  3. "Working Effectively With Unit Tests", Jay Fields
  4. "TDD Pattern: Do not cross boundaries", William E. Caputo
  5. "Arrange Act Assert", Cunningham Ward
  6. "Refactoring", Martin Fowler
  7. "Arrange Act Assert", Ward Cunningham

  8. "Writing unit tests is reinventing functional programming in non-functional languages", Christian S.
  9. "Functional Programming in PHP", Simon Honeywell
  10. Drupal 8 code metrics generated by www.phpmetrics.org, see results.

Further Reading

  1. See list of references.
  2. "Clean Code", Robert C. Martin
  3. "The Principles of OOD", Robert C. Martin
  4. "Object-Oriented Programming Revisited", Mike Bland
  5. "Working Effectively with Legacy Code", Michael C. Feathers
  6. "Goto Fail, Heartbleed, and Unit Testing Culture", Martin Fowler
  7. "How to Prevent the next Heartbleed", David A. Wheeler
  8. "Why Most Unit Testing is Waste", James O. Coplien
  9. "Object Mother", Martin Fowler
  10. "Test Data Builders: an alternative to the Object Mother pattern", Nat Pryce
  11. "Mocks Aren't Stubs", Martin Fowler
  12. "Google testing blog"
  13. "Improving developers enthusiasm for unit tests, using bubble charts", Jonathan Andrew Wolter
  14. "Don't Mock Concrete Classes", Steve Shogren
  15. "XUnit Test Patterns", George Mezaros
  16. "How Is Critical 'Life or Death' Software Tested?", Michael Byrne
  17. "Pure functions have side-effects", John D. Cook
  18. "Writing Great Unit Tests: Best and Worst Practices", Steve Sanderson

Thank You!

Slides are available at
github.com/josephdpurcell/principles-of-solitary-unit-testing