"[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
"The software development community simply hasn't managed to settle on well-defined terms around testing."— Martin Fowler, martinfowler.com
A unit test verifies a unit's correctness
against a specification.
A unit is...
t()
)function build()
)update.php
)...one thing
Think of it as a single frame in a call stack...
function increment($a) {
return $a + 1;
}
A Unit Test
use PHPUnit\Framework\TestCase;
class IncrementTest extends TestCase {
public function testIncrementAddsOne() {
$result = increment(1);
$this->assertEquals(2, $result);
}
}
Point of confusion: these can all be written in PHPUnit.
There are two challenges with testing a unit:
... anything that makes it not a pure function.
function get_product($id) {
$product_storage = \Drupal::service('entity.manager')->getStorage('product');
return $product_storage->load($id);
}
A Unit Test
public function testGetProductRetrievesId() {
$product_id = 1;
$result = get_product($product_id);
$this->assertEquals($product_id, $result->id());
}
Exception: no database to read from!
function increment_product_favorite_amount(Product $product) {
$product->favorite_count = $product->favorite_amount + 1;
$product->save();
return $product;
}
A Unit Test
public function testIncrementProductFavoriteAmountAddsOne() {
$product = new Product();
$product->favorite_count = 1;
$result = increment_product_favorite_amount($product);
$this->assertEquals(2, $result->favorite_count);
}
Exception: no database to write to!
Test doubles are the tools we use
to test units that have shared state or side effects.
$token_dummy = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$object_dummy = new \stdClass();
$voter = new Voter();
$this->assertEquals(VoterInterface::ACCESS_GRANTED, $voter->vote($token_dummy, $object_dummy, ['CREATE']));
A dummy is never called, it has no state.
... just use NULL
?
$file_fake = new FakeFile($path);
$response = new BinaryFileResponse($file_fake, 200);
$response->prepare();
$this->assertEquals($path, $response->headers->get('X-Accel-Redirect'));
A concrete class that fakes another concrete class.
$authorization_checker = $this->getMock('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface');
$authorization_checker->method('isGranted')->willReturn(true);
$controller = new TestController($authorization_checker);
$this->assertTrue($controller->isGranted('foo'));
Like a dummy, but is called and returns values.
$user_spy = $this->prophesize(User::class);
$profile = new Profile($user_spy->reveal());
$profile->getFullName();
$user_spy->getFirstName()->shouldHaveBeenCalled();
$user_spy->getLastName()->shouldHaveBeenCalled();
Like a stub, but captures calls.
$user_mock = $this->getMockBuilder('User')->getMock();
$user_mock->expects($this->once())->method('getFirstName')->willReturn('First');
$user_mock->expects($this->once())->method('getLastName')->willReturn('Last');
$profile = new Profile($user_mock);
$profile->getFullName();
Like a spy, but runs assertions on execution of Class Under Test (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
"If it stinks, change it."
— Grandma Beck, discussing child-rearing philosophy, "Refactoring"
getInactiveUser()
isInactive()->hasPosts(10)->build()
assertValidLink($link)
$this->assertEquals('text', $output);
Source: Cunningham Ward, c2.com
class IncrementTest extends TestCase {
public function testIncrement() {
// Arrange.
$product = new Product();
$product->favorite_count = 1;
// Act.
$result = increment($product);
// Assert.
$this->assertEquals(2, $result->favorite_count);
}
}
This suggests mocks are obscure and should be avoided!
Photo of two-level bascule bridge in Chicago, IL, photo courteousy of Lori Babcock.
See also unit vs kernel tests.
"It should not be underestimated how much easier these functions are to test than their state-filled counterparts."— Simon Honeywell, "Functional Programming in PHP"
Slides are available:
github.com/josephdpurcell/principles-of-unit-testing
Let's talk! @josephdpurcell