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

Dependency Injection as a Pattern to Better Testin

Justin Yost
December 04, 2015

Dependency Injection as a Pattern to Better Testin

Justin Yost

December 04, 2015
Tweet

More Decks by Justin Yost

Other Decks in Programming

Transcript

  1. Definition • Inject the Dependencies a Class/Object Depends Upon at

    Runtime Dependency injection means giving an object its instance variables. Really. That's it. — James Shore "Dependency Injection Demystified"
  2. class Table { public function save(array $data) { $DB =

    new Database(); return $DB->save($data); } }
  3. Problems? • What happens when Database changes? • Table is

    dependent upon a specific Database object • Table is no longer loosely coupled, it's now tightly coupled to Database • Table has a responsibility to ensure Database is managed (ie how it's constructed). • How would you unit test Table::save?
  4. Unit Testing Problem • This is the key to DI

    • Testing Table::save requires having a mock of DB • The mock enables DB::save to pass or fail independently • Mock also prevents actually writing to the database
  5. DI Solves These Problems DI says inject your dependencies so

    your class doesn't depend on a specific instance of the Database object. Instead you depend on being provided a Database to operate upon.
  6. class Table { public function __construct(Database $database) { $this->DB =

    $database; } public function save(array $data) { return $this->DB->save($data); } }
  7. Problems (From Before) • What happens when Database changes? •

    Table is dependent upon a specific Database object • Table is no longer loosely coupled, it's now tightly coupled to Database • Table has a responsibility to ensure Database is managed • How would you unit test Table::save?
  8. Solutions • Table only depends on Database having a method

    ::save with a certain signature. • Table is dependent upon being provided a specific Database instance. • Table is now loosely coupled to Database. • Table has no responsibility to Database's constructor. • Table tests can use a mocked instance of Database, for unit testing.
  9. Unit Testing Solutions • DB is now a mocked instance

    we pass to the constructor • Our Mock can pass or fail the save method independently • Mock also prevents actually writing to the database
  10. class TableTest extends PHPUnit_Framework_TestCase { public function setUp() { $this->DatabaseMock

    = $this->getMock('Database', ['save']); $this->TableTest = new Table($this->DatabaseMock); } public function testSaveFail() { $this->DatabaseMock->expects($this->once())->method('save') ->with($data)->will($this->returnValue(false)); $this->assertEquals(false, $this->TableTest->save($data)); } public function testSaveSuccess() { $this->DatabaseMock->expects($this->once())->method('save') ->with($data)->will($this->returnValue(true)); $this->assertEquals(true, $this->TableTest->save($data)); } }
  11. (Some) Ways To Do DI • Constructor Injection - Require

    as a Constructor Arg • Setter Injection - Setter Method for the Dependency
  12. class Table { public function __construct(Database $database) { $this->DB =

    $database; } public function save(array $data) { return $this->DB->save($data); } }
  13. class Table { public function setDatabase(Database $database) { $this->DB =

    $database; } public function save(array $data) { return $this->DB->save($data); } }
  14. DI Overview • Dependency Injection is most useful when unit

    testing • But not exclusively to that • DI lets you design software that talks to an interface and thus abstract away details • Enables easier swapping of pieces of software • Also extremely useful for APIs
  15. Tangent To Interfaces • Classes that define an Object's public

    API without implementation details • You can typehint to an Interface • Code to an Interface provides for easy swapping of dependencies
  16. interface PaymentInterface { public function chargeCard($amount, $email, $cc, $csc); }

    class PayPal implements PaymentInterface { public function chargeCard($amount, $email, $cc, $csc) { $this->chargeCard($amount, $email, $cc, $csc); } } class Stripe implements PaymentInterface { public function chargeCard($amount, $email, $cc, $csc) { $this->charge($amount, $cc, $csc); } }
  17. Interfaces + DI = ! • Interface means we have

    a reliable public API for a set of objects • DI means other programmers can swap out the objects in use at run time • Modern PHP Frameworks/Projects use this pattern
  18. class Order { public function new(array $data) { $PayPal =

    new PayPal(PAYPAL_ID, PAYPAL_CODE); return $PayPal->chargeCard( $data['amount'], $data['email'], $data['cc'], $data['csc'] ); } }
  19. API Problems • Calling Order::new() calls the PayPal API •

    Swapping PayPal for Stripe is hard • Method names aren't the same • Method params aren't the same • Unit Tests have to be updated
  20. class Order { public function __construct(PaymentInterface $Payment) { $this->Payment =

    $Payment; } public function new(array $data) { return $this->Payment->chargeCard( $data['amount'], $data['email'], $data['cc'], $data['csc'] ); } }
  21. API Problems (From Before) • Calling Order::new() calls the PayPal

    API • Swapping PayPal for Stripe is hard • Method names aren't the same • Method params aren't the same • Unit Tests have to be updated
  22. API Solutions • Calling Order::new calls our middleware • Swapping

    PayPal for Stripe becomes a single location for changes (hopefully) • Method names are the same • Method params are (hopefully) the same • Unit Tests stay the same
  23. DI Is A Design Pattern • DI is a Design

    Pattern for solving a particular class of problem • There is no one "best" way to do DI • Most Frameworks have a Dependency Injector Container • Container is a fancy word for basically a DI framework
  24. Dependency Injector Containers • Straight PHP - PHP-DI 5, Pimple

    • Laravel - Service Container • Symfony - DependencyInjection Component • CakePHP - Roll your own, Community, not-needed (maybe?)