sergiovierFantástico, clases magistrales de tipicos problemas que te encontras al intentar implementar TTD. Nos muestra como hacer código testeable.2 years ago
Unit Testing“Unit testing is a method by which individualunits of source code are tested to determine ifthey are fit for use. A unit is the smallest testablepart of an application.” wikipedia.org
Why and Why not Unit TestTO TEST OR NOT TOTEST?
Why Unit Test
Why Unit Test• Debugging is a time consuming process
Why Unit Test• Debugging is a time consuming process• When new functionality is added, how do we make sure the old one doesnt break
Why Unit Test• Debugging is a time consuming process• When new functionality is added, how do we make sure the old one doesnt break• By looking at a Unit Test, you can see the class in action, which lets you easily understand its intent and proper use
Why Unit Test• Debugging is a time consuming process• When new functionality is added, how do we make sure the old one doesnt 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
Why not Unit Test
Why not Unit Test• I never make mistakes
Why not Unit Test• I never make mistakes• The functionality is trivial
Why not Unit Test• I never make mistakes• The functionality is trivial• Tests slow me down
Why not Unit Test• I never make mistakes• The functionality is trivial• Tests slow me down• Management wont let me
Why not Unit Test• I never make mistakes• The functionality is trivial• Tests slow me down• Management wont let me• I dont know how to test
Properties of a Unit TestHOW DO WE KNOW IT ISA TEST?
Properties of a Unit Test• Isolated• Repeatable• Fast• Self-Documenting
Test Isolation
Test Isolation• No need to build a car to test tires
Test Isolation• No need to build a car to test tires• Testing payment gateway should not affect my monthly statement
Test Isolation• No need to build a car to test tires• Testing payment gateway should not affect my monthly statement• It runs faster
Test Isolation• No need to build a car to test tires• Testing payment gateway should not affect my monthly statement• It runs fasterBuilding cars to test tires is a waste of time andmoney
Repeatability
Repeatability• Tests need to be ran by every developer, no matter what stack he/she uses
Repeatability• Tests need to be ran by every developer, no matter what stack he/she uses• Tests must not rely on the environment in which they are being run
Repeatability• Tests need to be ran by every developer, no matter what stack he/she uses• Tests must not rely on the environment in which they are being runLower the project entry costs/barriers
Repeatability• Tests need to be ran by every developer, no matter what stack he/she uses• Tests must not rely on the environment in which they are being runLower the project entry costs/barriers – Getting every developer the same stack and educating them on how to use it is not efficient
Speed
Speed• Time is money, the longer we wait for tests to run, the more money our clients or company looses
Self-documentation
Self-documentation• Testable code is clear and easy to follow
Self-documentation• Testable code is clear and easy to follow• No need to explain how a certain component works, they can just look at the test
Self-documentation• 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• 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 documentationWe usually look for usage examples indocumentation anyway
Decouple componentsPROPERTY 1: ISOLATION
Dependency Injection“… a technique for decoupling highlydependent software components” wikipedia.org
Dependency InjectionHidden 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); } } Cant use a test version of PaymentGateway This test will end up in my monthly statement
Dependency InjectionInjected Dependency <?php class BankAccount { //... public function charge($amount, PaymentGateway $g) { $g->charge($amount, $this); } } PaymentGateway can now be replaced BankAccount shouldnt be responsible for charging itself 18
Dependency InjectionSingle Responsibility Principle <?php class BankAccount { //... } $gateway = new PaymentGateway(); $account = new BankAccount(); $gateway->charge(100, $account); Makes BankAccount class lighter, leaving it only responsibility of storing account balance 19
Dependency InjectionWhy:• Testable code• No hidden dependencies = Better API = Maintainability• All common initialization logic can be extracted (Dependency Injection Container)What to avoid:• Long method call chains
Law Of DemeterMETHOD CALL CHAINSARE BAD
Law Of Demeter“Each unit should have only limited knowledgeabout other units: only units "closely" related to thecurrent unit”“Each unit should only talk to its friends; dont talkto strangers”“Only talk to your immediate friends” wikipedia.org
Law Of Demeter <?php //... Bad $sku = $product->getStockItem()->getSku(); <?php //... $sku = $product->getItemSku(); //Product.php class Product { Good //... public function getItemSku() { return $this->stockItem->getSku(); } }
Law Of DemeterMakes code resistant to changeIf the Law Of Demeter was followed, the ->getSku() calls wouldnt need tobe changed.
Law Of Demeter <?php //... $logger->getWriter()->getFile()->writeLine(Some Log); <?php //... $logger->log(Some Log);
Law Of Demeter“Programmers should avoid writing code thatlooks like: dog.getBody().getTail().wag();… The solution is … [to] … rewrite our exampleas: dog.expressHappiness(); and let theimplementation of the dog decide what this means.” ThoughtWorks UK 26
Control the interactionPROPERTY 2: SPEEDPROPERTY 3: REPEATABILITY
Mock Objects“… mock objects are simulated objects that mimicthe behavior of real objects in controlled ways” wikipedia.org
Mock Objects Mock Test A S S Test independence Mocking external dependencies Interface discovery Mocking not-yet-existent classes and interaction
Mock ObjectsTest independence:• By mocking out external dependencies, we can achieve faster and more granular tests, e.g. mock filesystem, database, third party api, etc.
Mock Objects. Example: 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); } }Isolating file system accessNow we actually test that FileLogger correctly decorates the log string before writing it to file, leavingthe actual write to file part to the File class and test.
Mock Objects <?php $fp = fopen(/tmp/file.ext, w+); fwrite($fp, content); Functions must become methods <?php $file = new File(/tmp/file.ext, w+); $file->write(content); 32
Mock Objects <?php class File { private $handle; public function __construct($path, $mode) { An example of $this->handle = fopen($path, $mode);transforming a function } into a method public function write($content, $length = null) { fwrite($this->handle, $content, $length); } public function __destruct() { fclose($this->handle); } } 33
Mock Objects
Mock ObjectsInterface Discovery:
Mock ObjectsInterface Discovery:• Right now you know the least about your problem domain
Mock ObjectsInterface Discovery:• Right now you know the least about your problem domain• Your knowledge of the domain grows as you spend more time working in it
Mock ObjectsInterface Discovery:• 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 youll throw away if you can mock it and see if it makes sense?
Mock Objects. Example: <?php class PaymentGatewayTest extends PHPUnit_Framework_TestCase { //... public function testShouldCreditAccountByChargeAmount() { $bankAccount = $this->getMock(BankAccount); $bankAccount ->expects($this->once()) ->method(credit) ->with(100) ; $this->gateway->charge(100, $bankAccount); } }The above example if written before the BankAccount class, would havegiven us better understanding of the problem and could help design theBankAccount class 35
Mock Objects Mock T T Test B S Mock U U So, it turns out, that the best use for Mock Objects is for Iterative interface discovery
Test-Driven DevelopmentWAIT, WHAT?
Test Driven Development
Test Driven DevelopmentRed
Test Driven DevelopmentRed• write a failing test case, use mock objects to represent classes that are not implemented
Test Driven DevelopmentRed• write a failing test case, use mock objects to represent classes that are not implementedGreen
Test Driven DevelopmentRed• write a failing test case, use mock objects to represent classes that are not implementedGreen• when the test makes sense and the intent is clear, write method bodies
Test Driven DevelopmentRed• write a failing test case, use mock objects to represent classes that are not implementedGreen• when the test makes sense and the intent is clear, write method bodiesRefactor
Test Driven DevelopmentRed• write a failing test case, use mock objects to represent classes that are not implementedGreen• when the test makes sense and the intent is clear, write method bodiesRefactor• now that your tests are green, see what needs to be extracted (create dependencies, remove duplication)
TDD: Example Step 1 (Red)// tests/PaymentGatewayTest.php // lib/PaymentGateway.phprequire_once __DIR__./../lib/PaymentGateway.php; class PaymentGatewayrequire_once __DIR__./../lib/BankAccount.php; {class PaymentGatewayTest extends PHPUnit_Framework_TestCase public function charge($amount, BankAccount $account){ { } protected $gateway; } public function setUp() { $this->gateway = new PaymentGateway(); } // lib/BankAccount.php class BankAccount public function tearDown() { { } unset($this->gateway); } public function testCharge() $ phpunit tests/ PHPUnit 3.5.0RC1 by Sebastian Bergmann. { $bankAccount = $this->getMock(BankAccount); F $bankAccount Time: 0 seconds, Memory: 3.25Mb ->expects($this->once()) ->method(credit) There was 1 failure: ->with(100) 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. $this->gateway->charge(100, $bankAccount); FAILURES! } Tests: 1, Assertions: 1, Failures: 1.} 39
TDD: Example Step 2 (Green) // lib/BankAccount.php // lib/PaymentGateway.php class BankAccount class PaymentGateway { { public function credit($amount) public function charge($amount, BankAccount $account) { { } $account->credit($amount); } } }$ phpunit tests/PHPUnit 3.5.0RC1 by Sebastian Bergmann..Time: 0 seconds, Memory: 3.25MbOK (1 test, 1 assertion) 40
Mock only what you own...And what you don’t own, own namespace My; class Mongo { private $mongo;Classes change, method names, public function __construct(Mongo $mongo) {signature, visibility changes. $this->mongo = $mongo; }What will you do if a class public function selectDB($name)changed one of its public { return new MongoDB($this->mongo->selectDB($name));methods to final? } } class MongoDBDoctrine MongoDB ODM is built {using proxies private $mongoDb; public function __construct(MongoDB $mongoDb) { $this->mongoDb = $mongoDb; } } 41
Optional External Dependenciesand repeatabilitynamespace Tests;class MongoTestCase extends PHPUnit_Framework_TestCase{ public function setUp() namespace Tests; { if ( ! class_exists(Mongo)) { class MongoTest extends MongoTestCase $this->markTestSkipped( { Mongo extension not installed public function setUp() ); Text { } parent::setUp(); } //...} } } If your class or library that you test rely on an external component, that might not be present and the absence of which might break the tests, make sure to add checks and mark tests as skipped
RefactoringTHE CODE EVOLUTION
Refactoring“… refactoring is … changing … sourcecode without modifying itsexternal functional behavior in order to improve… the software.” wikipedia.org
Refactoring 45
Test Driven DevelopmentWhy• Best test code coverage possible• Design is born out of necessity, not forecastWhat to avoid• Shared fixtures – dont use the same objects in multiple tests, dont use persistent objects• External Dependencies – they are slow and uncontrollable.• Test code duplication – test code needs to be maintained, keep it DRY. Extract creation methods, create test cases per fixture setup (BDD)• Over-mocking – leads to fragile code, usually is a sign of violation of the Law Of Demeter
Premature OptimizationWHY NOT OPTIMIZEBEFORE CODING?
Premature Optimization“Premature optimization is the root of all evil” C. A. R. Hoare, Computer Scientist“"Premature optimization" is a phrase used todescribe a situation where a programmer letsperformance considerations affect the designof a piece of code.” wikipedia.org
Premature Optimization class Product { protected $reviews; protected $reviewCount = 0; public function __construct(ArrayCollection $r = null) { $this->reviews = $r ?: new ArrayCollection();Counters cache is part of the model }design public function getReviewCount() { return $this->reviewCount;Every new related collection brings }five methods public function incrementReviewCount() { $this->reviewCount++;Counters cache logic is duplicated }with every new collection public function decrementReviewCount() { $this->reviewCount--;Intuitive and readable operations like }count($product->getReviews()) public function addReview(Review $review)cannot be performed { $this->reviews->add($review); $this->incrementReviewCount(); } public function removeReview(Review $review) { $this->reviews->remove($review); $this->decrementReviewCount(); } }
Premature Optimization class Product {Cache logic is removed from Product private $reviews;responsibility public function __construct(ArrayCollection $r = null)Additional methods are eliminated { $this->reviews = $r ?: new ArrayCollection();Duplication with introduction of new collections }is minimized public function getReviewCount() {Further optimization path (if necessary): return count($this->reviews); }• Replace ArrayCollection instance with ReviewCollection, that has a better public function addReview(Review $review) optimized ->count() method. { $this->reviews->add($review);• Remove the ->getReviewCount() method } public function removeReview(Review $review)• Avoid code duplication for other Product { relationships by introducing $this->reviews->remove($review); CachedCollection and replacing } ReviewCollection with it }
Keep your code cleanPROPERTY 4:SELF-DOCUMENTATION
Clean code techniquesDont Repeat Yourself (DRY)• If a piece of code appears more than two times, extract it into a dedicated function, method or classYou aint 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• Dont optimize the code before its written, forecasts are usually wrong anyway
Clean code techniquesAvoid magic• its easier to code when the API is not lying – Magento is a good example of abusing magic and ruining maintainability – Dependency Injection helps to get rid of magicAvoid boolean parameters, they kill readability• Example – ->processOrder(int $orderNumber, boolean $emailConfirmation = true)• It should really be – ->processOrder(int $orderNumber) – ->emailConfirmation(int $orderNumber)
Clean code techniquesInheritance and Polymorphism over conditionals• Replace the switch with polymorphismComposition over Inheritance• Extract/inject functionality instead of inheriting it
If statements only to be used with primitive types, switch statementsare not object orientedINHERITANCE ANDPOLYMORPHISMOVER CONDITIONALS
Inheritance and Polymorphism overconditionals class MongoCursor { public function sort(array $fields) { if ($this->loggerCallable) { $this->log(array( sort => true, fields => $fields, )); } $this->mongoCursor->sort($fields); return $this; } }Example: Its probably not bad to have one if statement
Inheritance and class MongoCollection {Polymorphism public function batchInsert( array &$a, array $options = array())over { //... if ($this->loggerCallable) {conditionals $this->log(array( batchInsert => true, num => count($a), data => $aExample: Duplication. )); } $result = $thisEvery new method that needs to ->mongoCollectionbe logged, will have to add the ->batchInsert($a, $options) ;if statement return $result;There are plenty of if } public function getDBRef(array $reference)statements in this class { //... if ($this->loggerCallable) { $this->log(array( get => true, reference => $reference, )); } return $dbRef; } } 57
Inheritance and Polymorphism overconditionalsThe class that has conditionals is much harder tounderstand and test, as there are several possibleexecution flows.To test the previous example, you would need toinstantiate the class with and without$loggerCallable and test each function twice.
Inheritance and class MongoCollection { public function batchInsert(Polymorphism { array &$a, array $options = array())over $result = $this ->mongoCollection ->batchInsert($a, $options);conditionals return $result; } public function getDBRef(array $reference) { return $dbRef;Solution: Use polymorphism to } }transfer behavior class LoggableMongoCollection extends MongoCollection {Now public function batchInsert( array &$a,LoggableMongoCollection is array $options = array()) {the class responsible for $this->log(array(logging collection interaction. batchInsert => true, num => count($a), data => $aThe regular MongoCollection )); return parent::batchInsert($a, $options);needs not know about logging } public function getDBRef(array $reference) { $this->log(array( get => true, reference => $reference, )); return parent::getDBRef($reference); } }
Inheritance and Polymorphism overconditionals
Inheritance and Polymorphism overconditionals—Did my if statements disappear?
Inheritance and Polymorphism overconditionals—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.
Inheritance and Polymorphism overconditionals—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
Inheritance and Polymorphism overconditionals 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(); } } 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
Inheritance and class DisablerController extends Controller Polymorphism { public function disableAction($type) over { switch ($type) { conditionals case product: case seller: case supplier: } }switches are just public function enableAction($type)grouped ifs { switch ($type) {Consider the following controller: case product: case seller:• It let’s you disable, enable or list certain case supplier: object type. } }• It uses switch statements to do so public function listAction($type)• It introduces tons of duplication { switch ($type)• Each method will have at least three test { case in order to achieve the necessary case product: converage case seller: case supplier:Why reinvent the type system if } }OOP already let’s us use types }(classes)?
Inheritance and Polymorphism overconditionals interface DisablerControllerInterface { public function disableAction(); public function enableAction(); public function listAction(); } Define your object interface
Inheritance and Polymorphism over conditionalsCreate your interfaceimplementations – oneimplementation per switchcondition• Classes can be tested in isolation• Lighter classes• True usage of the type systemYou could also use abstract classes andcomplicated inheritance trees
Inheritance and Polymorphism ProductDisablerController extends Controller over implements DisablerControllerInterface { public function disableAction() conditionals { } public function enableAction() {Create your interface }implementations – oneimplementation per switch public function listAction() {condition } }• Classes can be tested in isolation• Lighter classes• True usage of the type systemYou could also use abstract classes andcomplicated inheritance trees
Inheritance and Polymorphism ProductDisablerController extends Controller over implements DisablerControllerInterface { SellerDisablerController public function disableAction() conditionals { extends Controller } implements DisablerControllerInterface { public function disableAction() public function enableAction() { {Create your interface } }implementations – one public function enableAction()implementation per switch public function listAction() { {condition } } }• Classes can be tested in isolation public function listAction() {• Lighter classes } }• True usage of the type systemYou could also use abstract classes andcomplicated inheritance trees
Inheritance and Polymorphism ProductDisablerController extends Controller over implements DisablerControllerInterface { SellerDisablerController public function disableAction() conditionals { extends Controller } implements DisablerControllerInterface { SupplierDisablerController public function disableAction() public function enableAction() { extends Controller {Create your interface } } implements DisablerControllerInterface {implementations – one public function disableAction() public function enableAction()implementation per switch public function listAction() { { { }condition } } } public function enableAction()• Classes can be tested in isolation public function listAction() { { } }• Lighter classes } public function listAction()• True usage of the type system { }You could also use abstract classes and }complicated inheritance trees
Extract re-used componentsinstead of inheriting functionalityCOMPOSITION OVERINHERITANCE
Composition over InheritanceThere might be cases when to receive an already-implemented functionality and keep it DRY, youextend the class encapsulating itFor example: a MongoCursor, that extends LoggableMongoCollection forits ->log() method
Composition over Inheritance <?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; } } 67
Composition over InheritanceBut what happens if we need the same functionalityin parallel hierarchies?• Single Inheritance doesnt allow it, we end up duplicating the ->log() method in both hierarchiesCOMPOSITION to the rescue
Composition over Inheritance 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); } } The solution is to extract common functionality into a dedicated class
Composition over Inheritance interface LoggerContainer { //... public function setLogger(Logger $logger); } Create an Interface, that would define how the new component will compose the existing ones
Composition over Inheritanceclass MongoCursor implements Iterator, <?phpCountable class MongoCollection{ { //... //... public function sort($fields) public function batchInsert(array &$a, array $options = array()) { { $this->mongoCursor->sort($fields); //... return $this; $result = $this->mongoCollection->batchInsert($a, $options); } return $result;} } public function getDBRef(array $reference) { //... return $dbRef; } } Regular classes in both hierarchies
Composition over Inheritanceclass LoggableMongoCursor extends MongoCursor class LoggableMongoCollection extends MongoCollection implements LoggerContainer implements LoggerContainer{ { //... //... public function setLogger(Logger $logger) public function setLogger(Logger $logger) { { $this->logger = $logger; $this->logger = $logger; } } //... public function sort($fields) public function batchInsert(array &$a, array $options = array()) { { $this->logger->log(array( $this->logger->log(array( sort => true, batchInsert => true, fields => $fields, num => count($a), )); data => $a return parent::sort($fields); )); } return parent::batchInsert($a, $options);} } public function getDBRef(array $reference) { $this->logger->log(array( get => true, reference => $reference, ));Classes, that can log operations return parent::getDBRef($reference); } }
Composition over InheritanceNOTE: dont decomposeobjects if there is no needfor re-use, YAGNI
Resources for self-improvementWHATS NEXT?
Resources for self-improvement•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 Pryce, Addison-Wesley Professional, 2009
1–4 of 4 previous next Post a comment