Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit Testing

Unit Testing

Unit testing talk for Twilio

Bulat Shakirzyanov

September 26, 2011
Tweet

More Decks by Bulat Shakirzyanov

Other Decks in Programming

Transcript

  1. Unit testing A guide to writing clean, testable code, that

    will be easy to maintain and extend Monday, September 26, 11
  2. •End to end, black box testing - tests are hardest

    to write and run, tests find bugs that are easiest to fix (bad markup, wrong css rule) Types of tests Monday, September 26, 11
  3. •Functional, wiring tests - easier to write tests, find easy

    to medium bugs (cannot connect to database, because server is misconfigured) Types of tests Monday, September 26, 11
  4. Types of tests •Unit tests - easiest to write, fix

    logic bugs and hard-to-spot design problems (violation of single responsibility principle, hard-coded function calls) Monday, September 26, 11
  5. “Unit testing is a method by which individual units of

    source code are tested to determine if they are fit for use. A unit is the smallest testable part of an application.” wikipedia.org Monday, September 26, 11
  6. Why and why not unit test To test or not

    to test? Monday, September 26, 11
  7. •Debugging is a time consuming process •When new functionality is

    added, how do we make sure the old one doesn't break •By looking at a unit test, you can see the class in action, which lets you easily understand its intent and proper use •Unit tests are the only real measure of project health and code quality Reasons Monday, September 26, 11
  8. •I never make mistakes •The functionality is trivial •Tests slow

    me down •Management won't let me Excuses Monday, September 26, 11
  9. •No need to build a car to test tires •Testing

    payment gateway should not affect my monthly statement •It runs faster Test isolation Monday, September 26, 11
  10. •Building cars to test tires is a waste of time

    and money Test isolation Monday, September 26, 11
  11. •Tests need to be run by every developer, no matter

    what stack he/she uses •Tests must not rely on the environment in which they are being run Repeatability Monday, September 26, 11
  12. •Time is money, the longer we wait for tests to

    run, the more money our clients or company loses Speed Monday, September 26, 11
  13. •Testable code is clear and easy to follow •No need

    to explain how a certain component works, they can just look at the test •No need to write documentation Self-documentation Monday, September 26, 11
  14. “[dependency injection is] … a technique for decoupling highly dependent

    software components” wikipedia.org Monday, September 26, 11
  15. •Can't use a test version of PaymentGateway •This test will

    end up in my monthly statement Hidden dependency <?php class BankAccount { //... public function charge($amount) { $gateway = new PaymentGateway(); // or $gateway = PaymentGateway::getInstance(); // or $gateway = Registry::get('payment_gateway'); //... $gateway->charge($amount, $this); } } Monday, September 26, 11
  16. •PaymentGateway can now be replaced •BankAccount shouldn't be responsible for

    charging itself Injected dependency <?php class BankAccount { //... public function charge($amount, PaymentGateway $g) { $g->charge($amount, $this); } } Monday, September 26, 11
  17. •Makes BankAccount class lighter, leaving it only responsibility of storing

    account balance Single responsibility <?php class BankAccount { //... } $gateway = new PaymentGateway(); $account = new BankAccount(); $gateway->charge(100, $account); Monday, September 26, 11
  18. •Testable code •No hidden dependencies = Better API = Maintainability

    •All common initialization logic can be extracted (Dependency Injection Container) DI benefits Monday, September 26, 11
  19. “Each unit should have only limited knowledge about other units:

    only units "closely" related to the current unit” “Each unit should only talk to its friends; don't talk to strangers” “Only talk to your immediate friends” wikipedia.org Monday, September 26, 11
  20. Bad Good Law Of Demeter <?php //... $sku = $product->getStockItem()->getSku();

    <?php //... $sku = $product->getItemSku(); //Product.php class Product { //... public function getItemSku() { return $this->stockItem->getSku(); } } Monday, September 26, 11
  21. “Programmers should avoid writing code that looks like: dog.getBody().getTail().wag(); …

    The solution is … [to] … rewrite our example as: dog.expressHappiness(); and let the implementation of the dog decide what this means.” ThoughtWorks UK Monday, September 26, 11
  22. “… mock objects are simulated objects that mimic the behavior

    of real objects in controlled ways” wikipedia.org Monday, September 26, 11
  23. •By mocking out external dependencies, we can achieve faster and

    more granular tests, e.g. mock filesystem, database, third party api, etc. Test independence Monday, September 26, 11
  24. Isolating filesystem •Now we actually test that FileLogger correctly decorates

    the log string before writing it to file, leaving the actual write to file part to the File class and test. <?php class FileLoggerTest extends PHPUnit_Framework_TestCase { //... public function testShouldFormatLogString() { $file = $this->getMock('File'); $file ->expects($this->once()) ->method('write') ->with( sprintf( '[ERR] %s some error', date(DATE_ISO8601) ) ) ; $logger = new FileLogger($file); $logger->err('some error'); } } Monday, September 26, 11
  25. Methods, not functions <?php $fp = fopen('/tmp/file.ext', 'w+'); fwrite($fp, 'content');

    <?php $file = new File('/tmp/file.ext', 'w+'); $file->write('content'); Monday, September 26, 11
  26. Mocking functions •An example of transforming a function into a

    method <?php class File { private $handle; public function __construct($path, $mode) { $this->handle = fopen($path, $mode); } public function write($content, $length = null) { fwrite($this->handle, $content, $length); } public function __destruct() { fclose($this->handle); } } Monday, September 26, 11
  27. •Right now you know the least about your problem domain

    •Your knowledge of the domain grows as you spend more time working in it •Why write something that you'll throw away if you can mock it and see if it makes sense? Interface discovery Monday, September 26, 11
  28. Interface discovery •Had the example been written before the BankAccount

    class, it would’ve given us better understanding of the problem and could’ve helped design the BankAccount class itself <?php class PaymentGatewayTest extends PHPUnit_Framework_TestCase { //... public function testShouldCreditAccountByAmountCharged() { $bankAccount = $this->getMock('BankAccount'); $bankAccount ->expects($this->once()) ->method('credit') ->with(100) ; $this->gateway->charge(100, $bankAccount); } } Monday, September 26, 11
  29. •So, it turns out, that the best use for mock

    objects is for iterative interface discovery Interface discovery Monday, September 26, 11
  30. •Write a failing test case, use mock objects to represent

    classes that are not implemented Red Monday, September 26, 11
  31. •When the test makes sense and the intent is clear,

    write method bodies Green Monday, September 26, 11
  32. •Now that your tests are green, see what needs to

    be extracted (create dependencies, remove duplication) Refactor Monday, September 26, 11
  33. <?php // file tests/PaymentGatewayTest.php require_once __DIR__.'/../lib/PaymentGateway.php'; require_once __DIR__.'/../lib/BankAccount.php'; class PaymentGatewayTest

    extends PHPUnit_Framework_TestCase { protected $gateway; public function setUp() { $this->gateway = new PaymentGateway(); } public function tearDown() { unset($this->gateway); } public function testShouldCreditAccountByAmountCharged() { $bankAccount = $this->getMock('BankAccount'); $bankAccount ->expects($this->once()) ->method('credit') ->with(100) ; $this->gateway->charge(100, $bankAccount); } } Red Monday, September 26, 11
  34. <?php // file lib/PaymentGateway.php class PaymentGateway { public function charge($amount,

    BankAccount $account) { } } <?php // file lib/BankAccount.php class BankAccount { } Red Monday, September 26, 11
  35. $ phpunit tests/ PHPUnit 3.5.0RC1 by Sebastian Bergmann. F Time:

    0 seconds, Memory: 3.25Mb There was 1 failure: 1) PaymentGatewayTest::testCharge Expectation failed for method name is equal to <string:credit> when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times. FAILURES! Tests: 1, Assertions: 1, Failures: 1. Red Monday, September 26, 11
  36. Green <?php // file lib/PaymentGateway.php class PaymentGateway { public function

    charge($amount, BankAccount $account) { $account->credit($amount); } } <?php // file lib/BankAccount.php class BankAccount { public function credit($amount) { } } Monday, September 26, 11
  37. $ phpunit tests/ PHPUnit 3.5.0RC1 by Sebastian Bergmann. . Time:

    0 seconds, Memory: 3.25Mb OK (1 test, 1 assertion) Green Monday, September 26, 11
  38. Proxy •Classes change, method names, signature, visibility changes. •What will

    you do if a class changed one of its public methods to final? •Doctrine MongoDB ODM is built using proxies <?php namespace My; class Mongo { private $mongo; public function __construct(\Mongo $mongo) { $this->mongo = $mongo; } public function selectDB($name) { return new MongoDB($this->mongo->selectDB($name)); } } class MongoDB { private $mongoDb; public function __construct(\MongoDB $mongoDb) { $this->mongoDb = $mongoDb; } } Monday, September 26, 11
  39. Repeatability •If a class or library that you test relies

    on an external component which might not be present during testing and the absence of which might break the tests, make sure to add checks and mark tests as skipped Monday, September 26, 11
  40. Repeatability <?php namespace Tests; class MongoTestCase extends \PHPUnit_Framework_TestCase { public

    function setUp() { if (!class_exists('Mongo')) { $this->markTestSkipped( 'Mongo extension not installed' ); } } } <?php namespace Tests; class MongoTest extends MongoTestCase { public function setUp() { parent::setUp(); //... } } Monday, September 26, 11
  41. “… refactoring is … changing … source code without modifying

    its external functional behavior in order to improve … the software.” wikipedia.org Monday, September 26, 11
  42. •Best test code coverage possible •Design is born out of

    necessity, not forecast TDD benefits Monday, September 26, 11
  43. “Premature optimization is the root of all evil” C. A.

    R. Hoare, Computer Scientist “"Premature optimization" is a phrase used to describe a situation where a programmer lets performance considerations affect the design of a piece of code.” wikipedia.org Monday, September 26, 11
  44. <?php class Product { protected $reviews; protected $reviewCount = 0;

    public function __construct(ArrayCollection $r = null) { $this->reviews = $r ?: new ArrayCollection(); } public function getReviewCount() { return $this->reviewCount; } public function incrementReviewCount() { $this->reviewCount++; } public function decrementReviewCount() { $this->reviewCount--; } public function addReview(Review $review) { $this->reviews->add($review); $this->incrementReviewCount(); } public function removeReview(Review $review) { $this->reviews->remove($review); $this->decrementReviewCount(); } } Monday, September 26, 11
  45. <?php class Product { private $reviews; public function __construct(ArrayCollection $r

    = null) { $this->reviews = $r ?: new ArrayCollection(); } public function getReviewCount() { return $this->reviews->count(); } public function addReview(Review $review) { $this->reviews->add($review); } public function removeReview(Review $review) { $this->reviews->remove($review); } } Monday, September 26, 11
  46. •Don’t Repeat Yourself (DRY) - if a piece of code

    appears more than two times, extract it into a dedicated function, method or class •You ain’t gonna need it (YAGNI) - there is no need to implement ACL if you were asked to write a forum board, there might never be a need Monday, September 26, 11
  47. •Don’t optimize the code before it’s been written in the

    most straightforward way possible, forecasts are usually wrong anyway Monday, September 26, 11
  48. •Avoid magic, its easier to code when the API is

    not lying, dependency injection helps to get rid of magic •Avoid boolean parameters, they kill readability Monday, September 26, 11
  49. <?php $service->processOrder(int $orderNumber, boolean $emailConfirmation = true); <?php $service->processOrder(int $orderNumber);

    $service->emailConfirmation(int $orderNumber); Removing booleans Monday, September 26, 11
  50. •Its probably not bad to have one if statement Conditional

    <?php class MongoCursor { public function sort(array $fields) { if ($this->loggerCallable) { $this->log(array( 'sort' => true, 'fields' => $fields, )); } $this->mongoCursor->sort($fields); return $this; } } Monday, September 26, 11
  51. <?php class MongoCollection { public function batchInsert(array &$a, array $options

    = array()) { //... if ($this->loggerCallable) { $this->log(array( 'batchInsert' => true, 'num' => count($a), 'data' => $a )); } $result = $this->mongoCollection->batchInsert($a, $options); return $result; } public function getDBRef(array $reference) { //... if ($this->loggerCallable) { $this->log(array( 'get' => true, 'reference' => $reference, )); } return $dbRef; } } Monday, September 26, 11
  52. •Every new method that needs to be logged, will have

    to add the if statement •There are plenty of if statements in this class Duplication Monday, September 26, 11
  53. •The class that has conditionals is much harder to understand

    and test, as there are several possible execution flows. •To test the previous example, you would need to instantiate the class with and without $loggerCallable and test each method twice. Conditionals Monday, September 26, 11
  54. •LoggableMongoCollection is responsible for logging collection interaction. •The regular MongoCollection

    class doesn’t need to know about logging Refactoring Monday, September 26, 11
  55. <?php class MongoCollection { public function batchInsert(array &$a, array $options

    = array()) { $result = $this->mongoCollection->batchInsert($a, $options); return $result; } public function getDBRef(array $reference) { return $dbRef; } } class LoggableMongoCollection extends MongoCollection { public function batchInsert(array &$a, array $options = array()) { $this->log(array( 'batchInsert' => true, 'num' => count($a), 'data' => $a )); return parent::batchInsert($a, $options); } public function getDBRef(array $reference) { $this->log(array( 'get' => true, 'reference' => $reference, )); return parent::getDBRef($reference); } } Monday, September 26, 11
  56. •Did my if statements disappear? •No •The conditional was actually

    moved to the instantiation part of the application, it is not part of the domain logic anymore •Testing such dedicated classes is much simpler, since there is only one execution flow Monday, September 26, 11
  57. <?php class MongoCollectionFactory { protected $loggerCallable; public function __construct(\Closure $loggerCallable

    = null) { $this->loggerCallable = $loggerCallable; } public function getMongoCollection() { return isset($this->loggerCallable) ? new LoggableMongoCollection($this->loggerCallable) : new MongoCollection(); } } Monday, September 26, 11
  58. •Dependency Injection helps to separate the “initialization” logic from “domain”

    logic •This allows for greater testability as each concern is tested in a dedicated environment Monday, September 26, 11
  59. • Consider the following controller: • It lets you disable,

    enable or list certain object type. • It uses switch statements to do so • It introduces tons of duplication • Each method will have at least three test case in order to achieve the necessary coverage • Why reinvent the type system if OOP already lets us use types (classes)? Switches <?php class DisablerController extends Controller { public function disableAction($type) { switch ($type) { case 'product': case 'seller': case 'supplier': } } public function enableAction($type) { switch ($type) { case 'product': case 'seller': case 'supplier': } } public function listAction($type) { switch ($type) { case 'product': case 'seller': case 'supplier': } } } Monday, September 26, 11
  60. •Extract common interface Switches <?php interface DisablerControllerInterface { function disableAction();

    function enableAction(); function listAction(); } Monday, September 26, 11
  61. Switches •Create your interface implementations – one implementation per switch

    condition •Classes can be tested in isolation Monday, September 26, 11
  62. <?php class ProductDisablerController extends Controller implements DisablerControllerInterface { public function

    disableAction() { } public function enableAction() { } public function listAction() { } } <?php class SellerDisablerController extends Controller implements DisablerControllerInterface { public function disableAction() { } public function enableAction() { } public function listAction() { } } <?php class SupplierDisablerController extends Controller implements DisablerControllerInterface { public function disableAction() { } public function enableAction() { } public function listAction() { } } Monday, September 26, 11
  63. Polymorphism •Lighter classes •Usage of the true type system •You

    could also use abstract classes and complicated inheritance trees instead of one interface and orphaned implementations Monday, September 26, 11
  64. •There might be cases when to receive an already-implemented functionality

    and keep it DRY, you extend the class encapsulating it •For example: a MongoCursor, that extends LoggableMongoCollection for its ->log() method Composition Monday, September 26, 11
  65. <?php class LoggableMongoCollection extends MongoCollection { //... public function log(array

    $log) { if ( ! $this->loggerCallable) { return; } $log['class'] = $this->class->name; $log['db'] = $this->class->db; $log['collection'] = $this->class->collection; call_user_func($this->loggerCallable, $log); } } class MongoCursor extends LoggableMongoCollection implements \Iterator, \Countable { //... public function sort($fields) { if ($this->loggerCallable) { $this->log(array( 'sort' => true, 'fields' => $fields, )); } $this->mongoCursor->sort($fields); return $this; } } Monday, September 26, 11
  66. •But what happens if we need the same functionality in

    parallel hierarchies? •Single inheritance doesn't allow it, we end up duplicating the ->log() method in both hierarchies •Composition to the rescue Monday, September 26, 11
  67. <?php class Logger { protected $loggerCallable; public function __construct(Closure $loggerCallable)

    { $this->loggerCallable = $loggerCallable; } public function log(array $log) { $log['class'] = $this->class->name; $log['db'] = $this->class->db; $log['collection'] = $this->class->collection; call_user_func($this->loggerCallable, $log); } } Monday, September 26, 11
  68. •Create an interface, that would define how the new component

    will compose the existing ones Composition Monday, September 26, 11
  69. <?php class MongoCursor implements Iterator, Countable { //... public function

    sort($fields) { $this->mongoCursor->sort($fields); return $this; } } <?php class MongoCollection { //... public function batchInsert(array &$a, array $options = array()) { //... $result = $this->mongoCollection->batchInsert($a, $options); return $result; } public function getDBRef(array $reference) { //... return $dbRef; } } Parent classes don’t log Monday, September 26, 11
  70. <?php class LoggableMongoCursor extends MongoCursor implements LoggerContainer { //... public

    function setLogger(Logger $logger) { $this->logger = $logger; } //... public function sort($fields) { $this->logger->log(array( 'sort' => true, 'fields' => $fields, )); return parent::sort($fields); } } <?php class LoggableMongoCollection extends MongoCollection implements LoggerContainer { //... public function setLogger(Logger $logger) { $this->logger = $logger; } public function batchInsert(array &$a, array $options = array()) { $this->logger->log(array( 'batchInsert' => true, 'num' => count($a), 'data' => $a )); return parent::batchInsert($a, $options); } public function getDBRef(array $reference) { $this->logger->log(array( 'get' => true, 'reference' => $reference, )); return parent::getDBRef($reference); } } Child classes that log Monday, September 26, 11
  71. •Don't decompose objects if there is no need for re-use,

    YAGNI Decomposition Monday, September 26, 11
  72. • Google Tech Talks - http://www.youtube.com/user/GoogleTechTalks • Inheritance, Polymorphism, &

    Testing • Unit Testing • Blogs • Invisible to the Eye – personal blog of Giorgio Sironi, Software Architect • Books • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John M. Vlissides, Addison-Wesley Professional, 1994 • Refactoring: Improving the Design of Existing Code by Martin Fowler, Kent Beck, John Brant, William Opdyke and Don Roberts, Addison-Wesley Professional, 1999 • Patterns of Enterprise Application Architecture by Martin Fowler, Addison-Wesley Professional, 2002 • xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros, Addison- Wesley, 2007 • Growing Object-Oriented Software Guided by Tests by Steve Freeman and Nat Resources for self- improvement Monday, September 26, 11