Share
Embed
Mocking Dependencies in PHPUnit
Slides for talk that was given at Midwest PHP in Minneapolis, MN March 15, 2014
Frost
March 15, 2014
Transcript
55 slides
-
Mocking Dependencies in PHPUnit Matt Frost · IRC: mfrost503 ·
Feedback: http://joind.in/10547 · @shrtwhitebldguy Saturday, March 15, 14 -
We’ll be covering ✤ Defining dependencies ✤ Dependency Injection ✤
Test Doubles in Theory ✤ Test Doubles in Practice Saturday, March 15, 14 -
What’s a dependency ✤ Unit Test Context ✤ A unit(s)
of code that adds functionality to another unit of code ✤ Think system dependencies, but much smaller scale Saturday, March 15, 14 -
Why mock them? ✤ Unit tests should cover a single
unit of code in isolation ✤ A bug in a dependency makes your test a guessing game ✤ We only want to know that the code we’re testing works Saturday, March 15, 14 -
Dependencies in the wild class Auth { private $user; public
function __construct(User $user) { $this->user = $user; } public function authenticate() { $username = $this->user->getUserName(); $password = $this->user->getHash(); $this->checkLogin($username,$password); } } Saturday, March 15, 14 -
Dependencies in the wild class Auth { private $user; public
function __construct(User $user) { $this->user = $user; } public function authenticate() { $username = $this->user->getUserName(); $password = $this->user->getHash(); $this->checkLogin($username,$password); } } Dependency Alert! Saturday, March 15, 14 -
Dependencies in the wild class Auth { private $user; public
function __construct(User $user) { $this->user = $user; } public function authenticate() { $username = $this->user->getUserName(); $password = $this->user->getHash(); $this->checkLogin($username,$password); } } Dependency Alert! Saturday, March 15, 14 -
Don’t do this! class Auth { public function authenticate($username, $pass)
{ $user = new User($username, $pass); $username = $user->getUserName(); $password = $user->getHash(); $this->checkLogin($username,$password); } User cannot be mocked Saturday, March 15, 14 -
Dependency Injection ✤ Helps make code testable ✤ Helps make
code flexible ✤ Constructor/Accessor methods Saturday, March 15, 14 -
MOAR Dependency Injection ✤ Dependencies become properties in the object
in which they’re used ✤ Paramount for mocking in unit tests! Saturday, March 15, 14 -
Defining Test Doubles ✤ Stand in for actual objects (think
Stunt Doubles) ✤ Can simulate functionality from those objects ✤ Can fulfill the requirements of a type hinted method ✤ Can be used to make sure a method on the mock is called Saturday, March 15, 14 -
A few more points ✤ Can’t directly mock private methods
✤ Only mock what you need to test ✤ In a pinch, Reflection API can help test private Saturday, March 15, 14 -
Theory ✤ Unit Test shouldn’t be dependent on external data
source availability ✤ Unit Test vs. Integration Test ✤ “How do I know if my query is right?” ✤ You’re testing code, not network availability Saturday, March 15, 14 -
Types of Test Doubles ✤ Mock ✤ Stub ✤ Dummy
✤ Spy Saturday, March 15, 14 -
Mock ✤ Verifies that a method has been called correctly
✤ Doesn’t generate a response Saturday, March 15, 14 -
Anatomy of a Mock ✤ Expectation ✤ Method ✤ Parameters
(if applicable) Saturday, March 15, 14 -
Mock Example public function testSetUser() { $user = $this->getMock('User',array('setUserId')); $user->expects($this->once())
->method('setUserId') ->with(1); $post = new Post($user); $post->retrieve(10); $post->getUserInfo(); } Saturday, March 15, 14 -
Explanation ✤ Supposes $user->setUserId(1) will be called in the test
✤ Fails if $user->setUserId(1) is not called Saturday, March 15, 14 -
Mock Implementation public function getUserInfo() { // assume $this->data is
populated from // the $post->retrieve($id) method $userId = $this->data['user_id']; $this->user->setUserId($userId); return $this->user->retrieve(); } This is an example of code that would pass the previous test, it’s a fictional example...so I wouldn’t use the code :) Saturday, March 15, 14 -
Test Stub ✤ Ensures a method is a called correctly
✤ Generates a “fake response” ✤ Response allows for different cases to be tested ✤ Data Fixtures/Data Providers Saturday, March 15, 14 -
Data Providers public function NumberProvider() { array( array(2, 4), array(9,
81), array(4, 16), array(12, 144), array(13, 169), array(5, 25), ) } Saturday, March 15, 14 -
Using a data provider /** * @dataProvider NumberProvider */ public
function testSquares($integer, $value) { $mathOperations = new MathOperations(); $square = $mathOperations->square($integer); $this->assertEquals( $value, $square, 'Square value did not match expectation' ) } Saturday, March 15, 14 -
Response ✤ Declare a response for a particular case ✤
Doesn’t have to be a value, can throw Exceptions! ✤ Can show if your code is behaving/failing correctly Saturday, March 15, 14 -
Stub Example public function testGetUserInfo() { $userInfo = array( 'first_name'
=> 'Joe', 'last_name' => 'Strummer', 'id' => 1, 'email' => '[email protected]' ); $user = $this->getMock('User', array('retrieve')); $user->expects($this->once()) ->method('retrieve') ->will($this->returnValue($userInfo)); ... Saturday, March 15, 14 -
Stub Example Cont’d ... $post = new Post($user); $post->retrieve(10); $information
= $post->getUserInfo(); $this->assertEquals('Joe',$information['first_name']); $this->assertEquals('Strummer',$information['last_name']); } Here we’re asserting that retrieve is called correctly by validating that we get back what we expect Saturday, March 15, 14 -
Dummy ✤ It’s a place holder ✤ It has no
expectations or behavior ✤ It satisfies a parameter list... Saturday, March 15, 14 -
Dummy Example <?php class Comment { public function __construct($comment, User
$user) { ... } public function validateComment() { //doesn't rely on User at all } } Saturday, March 15, 14 -
Dummy Example public function testValidateComment() { $user = $this->getMock('User'); $commentText
= "<script></script>"; $comment = new Comment($commentText,$user); $this->assertFalse($comment->validateComment()); } Saturday, March 15, 14 -
Dummy Example public function testValidateComment() { $user = $this->getMock('User'); $commentText
= "<script></script>"; $comment = new Comment($commentText,$user); $this->assertFalse($comment->validateComment()); } User fulfills the method signature, but doesn’t get used Saturday, March 15, 14 -
Isolation! ✤ External Data Sources - don’t talk to em!
✤ APIs ✤ Database Responses Saturday, March 15, 14 -
Stubbing PDO ✤ Constructor is not serializable, we must adapt!
✤ PDO::prepare - returns a PDO Statement (which we can stub) ✤ We can easily cover a variety of outcomes Saturday, March 15, 14 -
Constructor <?php class PDOTestHelper extends PDO { public function __construct()
{ } } Saturday, March 15, 14 -
Constructor <?php class PDOTestHelper extends PDO { public function __construct()
{ } } Overridden constructor allows us to mock! Saturday, March 15, 14 -
Setup/TearDown public function setUp() { $this->pdo = $this->getMock('PDOTestHelper', array(‘prepare’); $this->statement
= $this->getMock('PDOStatement', array(‘execute’)); } public function tearDown() { unset($pdo); unset($statement); } Saturday, March 15, 14 -
Stubbing a prepared statement $this->pdo->expects($this->once()) ->method('prepare') ->with($this->stringContains('SELECT * from table'))
->will($this->returnValue($this->statement)) Prepare will return a PDOStatement when executed successfully, so in order to stub the preparation and execution of the query, this is how we need to start. Saturday, March 15, 14 -
Stubbing the execute call $this->statement->expects($this->once()) ->method('execute') ->with($this->isType('array')) ->will($this->returnValue($this->statement)); Since we’re
expecting this call to succeed, we need to return the statement again. Once we get the statement back, we’ve successfully simulated the preparation and execution of a query! Saturday, March 15, 14 -
Stubbing Fetch! $simData = array( ‘id‘ => 1, ‘firstName‘ =>
‘Lloyd’, ‘lastName‘ => ‘Christmas’, ‘occupation‘ => ‘Dog Groomer’ ); $this->statement->expects($this->once()) ->method('fetch') ->will($this->returnValue($simData)); Saturday, March 15, 14 -
Returning Data ✤ Data Fixtures ✤ Data Providers ✤ Data
should resemble what you expect to get back Saturday, March 15, 14 -
Mocking API Calls ✤ Wrap it up, not just for
testing for your own sanity! ✤ Once it’s wrapped it can be mocked like anything else ✤ Spies! ✤ Don’t talk to the API Saturday, March 15, 14 -
Spies ✤ Helpful in making sure your method was called
✤ Or called a certain number of times ✤ Not commonly used, but I’ve found good use in testing APIs Saturday, March 15, 14 -
Practical API Testing ✤ Generally, mocks suffice! ✤ If the
method is transforming data, stub it! ✤ Spies are good to track multiple calls in same method Saturday, March 15, 14 -
API Example public function testGetTweets() { //mock example $request =
$this->getMock('Request',array('get')); $request->expects($this->once()) ->method('get') ->with('statuses'); $twitter = new Twitter($request); $twitter->getTweets(); } Saturday, March 15, 14 -
Spy Example public function testComplicatedMethod() { //spy example $request =
$this->getMock('Request',array('get')); $request->expects($this->exactly(3)) ->method('get'); $twitter = new Twitter($request); $twitter->complicatedMethod(); } Saturday, March 15, 14 -
Helpful Tidbits - With() ✤ isType(String $type) - check by
type ✤ stringContains($value) - string parameter ✤ contains($value) - array parameter ✤ hasArrayKey($key) ✤ greaterThan($value) ✤ isInstanceOf($className) ✤ matchesRegularExpression($pattern) ✤ equalTo($value) Saturday, March 15, 14 -
XPMock ✤ Sits on top of PHPUnit ✤ Less work
to mock dependencies Saturday, March 15, 14 -
Example - PHPUnit <?php $mock = $this->getMockBuilder('MyClass') ->setMethods(['getBool', 'getNumber', 'getString'])
->disableOriginalConstructor() ->getMock(); $mock->expects($this->any()) ->method('getBool') ->will($this->returnValue(true)); $mock->expects($this->any()) ->method('getNumber') ->will($this->returnValue(1)); $mock->expects($this->any()) ->method('getString') ->will($this->returnValue('string')); Saturday, March 15, 14 -
Phake Example $this->item1 = Phake::mock('Item'); $this->item2 = Phake::mock('Item'); $this->item3 =
Phake::mock('Item'); Phake::when($this->item1)->getPrice()->thenReturn(100); Phake::when($this->item2)->getPrice()->thenReturn(200); Phake::when($this->item3)->getPrice()->thenReturn(300); Saturday, March 15, 14 -
Summary ✤ Injected Dependencies = Increased Testability ✤ Mock/Stub/Dummy ✤
Don’t do more than you need to! ✤ Practice makes perfect Saturday, March 15, 14 -
Victory! Mocking effectively leads to better tests and better tests
lead to better applications! Saturday, March 15, 14 -
Thank you! ✤ Freenode: mfrost503 ✤ Joind.in: http://joind.in/10547 ✤ Twitter:
@shrtwhitebldguy Saturday, March 15, 14