Watch Erik's presentation on PHP Unit Testing to gain familiarity with unit tests and unit testing here at Tagged, with the testing framework currently in place and also learn how to write (better) unit tests. Download his slides here or email him at ejohannessen@tagged.com.
1 of 26
Downloaded 33 times
More Related Content
PHP Unit Testing
1. Development Workshops PHP Unit Testing @ Tagged 油 We enable anyone to meet and socialize with new people 2011.10.26 Erik Johannessen
2. PHP Unit Testing @ Tagged Goal: - Gain familiarity with unit tests and unit testing here at Tagged - Gain familiarity with the testing framework currently in place - Learn to write (better) unit tests
3. Agenda General Unit Testing Attributes Unit Tests in PHP @ Tagged Running Tests Assertions Mocking Objects Mocking Static Functions Getting Real Objects Regression Testing with Hudson A practical demo : Wink! Effective Testing Strategies Test-Driven Development
4. What is a Unit Test? - A unit is the smallest testable part of an application. 油 - Exercises a particular piece of code in isolation, ensuring correctness. 油 - Good for regression testing.油 Once we have a test that passes, the test should continue to pass on each successive change to the codebase.
5. Unit Test Attributes - Each test should be independent of all other tests (including itself!) 油 - The number of times/order in which they're run shouldn't matter. 油 - This is achieved by beginning with a controlled state, and feeding in controlled inputs. 油 - Controlled inputs should produce expected outputs. 油 - State should change in predictable ways, given inputs. 油 - External dependencies (DB, Cache, other external services) should be mocked out.
6. Unit Tests in PHP - All tests are found in the directory /cooltest/unit/tests/ 油 - Each test file should end in *Test.php 油 - Each test should be a public methods with name prefixed with test. 油 - Tests are run in an unspecified order; do not depend on one test running before another. 油 - Before each test is run, the setUp() method is invoked, if it exists. 油 - After each test is run, the tearDown() method is invoked, if it exists.
7. myclassTest.php Tests shared/class/tag/myclass.php class tag_myclassTest extends test_base { 油油油 public function setUp() { 油油油 油油油 parent::setUp(); 油油油 油油油 // do setup stuff before every test 油油油 } 油油油 油油油 public function testMethodA() { 油油油 油油油 // call method a() on an instance of myclass 油油油 油油油 // assert some conditions 油油油 } 油油油 油油油 public function testMethodB() { 油油油 油油油 // call method b() on an instance of myclass 油油油 油油油 // assert some conditions 油油油 } 油油油 public function tearDown() { 油油油 油油油 parent::tearDown(); 油油油 油油油 // perform cleanup 油油油 油油油 // in most cases, don't need this, as test_base::tearDown() will 油油油 油油油 // take care of almost everything for you 油油油 } }
8. Running the tests using PHPUnit > pwd /home/html/cooltest/unit/tests/shared/class/tag # run all tests in this directory > phpunit . # run all tests in myclassTest.php > phpunit myclassTest.php # run testMethodA > phpunit -filter testMethodA myclassTest.php # run all tests that begin with testMethod* > phpunit -filter testMethod myclassTest.php
9. Assertions Testing framework comes with several built-in assertion functions. If the optional $msgOnFailure is given, it will be included in the output when the test fails.油 I highly recommend including descriptive failure messages, as that not only helps the debugger find out what failed, but also what the intention of the test author was. public function test() { 油油油 $this->assertTrue($value, $msgOnFailure = ''); 油油油 $this->assertFalse($value, $msgOnFailure = ''); 油油油 $this->assertEquals($expected, $actual, $msgOnFailure = ''); 油油油 $this->assertNotEquals($expected, $actual, $msgOnFailure = ''); 油油油 $this->assertType($expected, $actual, $msgOnFailure = ''); 油油油 $this->assertGreaterThan($expected, $actual, $msgOnFailure = ''); }
10. Mocking Objects in PHP - Almost all classes in our codebase have dependencies on other classes. 油 - To eliminate those dependencies as a variable in a unit test, we replace those objects that we would normally fetch from the global loader ($_TAG) with mock objects. 油 - Mock objects are just like the real objects they substitute for, except that we override the values of methods, properties and constants of that object to produce dependable, controlled results when the object is invoked.
11. Mocking Objects in PHP // in the API file public function getGoldBalance($params) { 油油油 $userId = $this->_requestUserId(true); 油油油 // here, $_TAG->gold[$userId] returns our mock object 油油油 $userGold = $_TAG->gold[$userId]->getGoldBalance(true); 油油油 $results = array( 油油油 油油油 'gold_bal' => $userGold, 油油油 油油油 'gold_bal_string' => number_format($userGold, 0) 油油油 ); 油油油 return $this->generateResult($results); } // in the test file $userId = 9000; $balance = 500; $goldGlobalMock = GlobalMockFactory::getGlobalMock('tag_user_gold', 'gold', $userId); $goldGlobalMock->override_method('getGoldBalance', function($getFromDB=false) use ($balance) { 油油油 return $balance; }); $goldGlobalMock->mock(); $result = tag_api::call('tagged.gold.getBalance', array(), $userId); $this->assertEquals($balance, $result['gold_bal'], 'Wrong balance returned!');
12. Mocking Objects in PHP $globalMock->override_property('myProp', 1000); $mockObj = $globalMock->mock(); // prints 1000 echo $mockObj->myProp; $globalMock->override_constant('MY_CONST', 5); $mockObj = $globalMock->mock(); // prints 5 echo $mockObj::MY_CONST; 油 Can also be used to add methods/properties to objects that don't already have them.
13. Mocking Static Functions in PHP $commMock = new StaticMock('tag_privacy', 'can_communicate', true); $userId = 9000; $otherUserId = 9001; // always returns true! $canCommunicate = tag_privacy::can_communicate($userId, $otherUserId); $this->assertTrue($canCommunicate, Users can't communicate!); // a more dynamic example $goodUserId = 9002 $badUserId = 9003; $boxedMock = new StaticMock('tag_user_auth', 'is_boxed_user', function ($userId) use ($badUserId) { 油油油 return $userId == $badUserId; }); $this->assertTrue(tag_user_auth::is_boxed_user($badUserId), 'Bad user not boxed!'); $this->assertFalse(tag_user_auth::is_boxed_user($goodUserId), 'Good user boxed!');
14. Testing for Errors - Not found often in our codebase, but we can test for and trap specific errors within the PHP. 油 - Specify file and error level, then verify number of errors trapped by testErrorHandler. $errorHandler = new testErrorHandler(); $errorHandler->suppressError('shared/class/tag/user/invites.php', E_USER_NOTICE); $result = $invites->removeOutgoingInvite($connId); $this->assertEquals(1, $errorHandler->numErrorsSuppressed(), 'Notice not triggered.'); $errorHandler->restorePreviousHandler();
15. Getting Real Objects in PHP - Most times, we don't want a mock object for the object under test - we want the real thing. - However, if we just go and get an object via our global system (i.e.油 $_TAG->contacts[$userId]), our test will be dependent on whatever object might be found in memcache. - test_base::get_global_object() solves this by figuring out how to create an object directly, and returning a new one, with a mock loader to avoid touching memcache. // assume this is a test class that inherits from test_base $userId = 9000; // returns a fresh instance of tag_user_contacts // but with a mock loader // normally accessed like $_TAG->contacts[$userId]; $userContacts = self::get_global_object('contacts', $userId);
16. Framework Limitations Can't pass use variables by reference to overridden methods. Can't mock static functions that contain static variables. public static function is_school_supported($userId) { 油油油 static $country_supported = array('US', 'CA', 'GB', 'IE', 'NZ', 'AU'); 油油油 $userObj = $_TAG->user[$userId]; 油油油 if (empty($userObj) || !$userObj->isValidUser()) return false; 油油油 $countryCode = 'US'; 油油油 $address = $userObj->getAddressObj(); 油油油 if ($address){ 油油油油油油油 $countryCode = $address->getCountryCode(); 油油油 } 油油油 if (in_array($countryCode, $country_supported)) 油油油油油油油 return true; 油油油 else 油油油油油油油 return false; } $balance = 5000; $mock->override_method('credit', function($amt) use (&$balance) { 油油油 $balance += $amt; 油油 油 return $balance; });
17. Hudson - Our unit testing suite (currently >900 tests) is also very useful for regression testing. 油 - Our continuous integration system, Hudson, runs every test after every SVN submission to web. 油 - If any test fails, our codebase has regressed, and the commit author that broke the build is notified (as is proddev@tagged.com, so it's nice and public). 油 - If you break the build, please respond promptly to fix it; we can't ship with a broken build.
20. Let's do an example - Wink! class tag_apps_winkTest extends test_base { 油油油 public function setUp() { 油油油油油油油 parent::setUp(); 油油油油油油油 $this->_userId = 9000; 油油油油油油油 油油油油油油油 $winkDaoGlobalMock = GlobalMockFactory::getGlobalMock('tag_dao_wink', 'dao', array('wink', $this->_userId)); 油油油油油油油 $winkDaoGlobalMock->override_method('getWinks', function() { 油油油油油油油油油油油 return array( 油油油油油油油油油油油油油油油 0 => array( 油油油油油油油油油油油油油油油油油油油 'other_user_id' => 9001, 油油油油油油油油油油油油油油油油油油油 'time' => $_SERVER['REQUEST_TIME'], 油油油油油油油油油油油油油油油油油油油 'type'油 => 'R', 油油油油油油油油油油油油油油油油油油油 'is_viewed' => 'N' 油油油油油油油油油油油油油油油 ), 油油油油油油油油油油油 ); 油油油油油油油 }); 油油油油油油油 $winkDaoGlobalMock->mock(); 油油油油油油油 $this->_mockUser(9001); 油油油油油油油 $this->_wink = self::get_global_object('wink', $this->_userId); 油油油 } 油油油 油油油 public function testCountWink() { 油油油油油油油 $numWinks = $this->_wink->countWinks(); 油油油油油油油 $this->assertEquals(1, $numWinks, "wrong number of winks!"); 油油油 } }
21. Other Testing Strategies Corner Cases Call functions under test with corner case inputs 油油油 - 0 油油油 - null 油油油 - '' 油油油油油油油油油 (an empty string) 油油油 - array()油油油油油 (an empty array) 油油油 - Big numbers油 (both positive & negative) 油油油 - Long strings 油油油 - Other large inputs (esp. where constants like MAX_SIZE are defined)
22. Other Testing Strategies Negative Testing Bad/illegal inputs should throw exceptions, raise errors, or otherwise alert the programmer of bad input // test that an exception is thrown try { 油油油 $result = tag_api::call('tagged.apps.gifts.getGiftRecipients', array('goldTxnId' => 0), $this->_userId); 油油油 $this->fail('getGiftRecipients did not throw an exception for invalid id'); } catch (Exception $e) { 油油油 $this->assertEquals(107, $e->code(), 'Wrong exception code'); } // test that an error is triggered $errorHandler = new testErrorHandler(); $errorHandler->suppressError('shared/class/tag/user/invites.php', E_USER_NOTICE); $result = $invites->removeOutgoingInvite($connectionId); $this->assertEquals(1,$errorHandler->numErrorsSuppressed(),'Notice not triggered'); $errorHandler->restorePreviousHandler();
23. Other Testing Strategies Differing Initial States Set up tests to begin with differing initial states // test that you can get a friend id from a user's friend list public function testGetRandomFriend() { 油油油 $friendList = array(34, 55, 88); 油油油 $friends = new Friends($friendList); 油油油 $randomFriend = $friends->getRandomFriend(); 油油油 $this->assertTrue(in_array($randomFriend, $friendList), 'Got non-friend!'); } // test that you can't get a friend id when a user has no friends public function testGetRandomFriendWithNoFriends() { 油油油 $friendList = array(); 油油油 $friends = new Friends($friendList); 油油油 $randomFriend = $friends->getRandomFriend(); 油油油 $this->assertTrue(is_null($randomFriend), 'Got a friend from user with no friends!'); }
24. Other Testing Strategies Tests should be as granular as possible -- each test should be its own function. // BAD $this->assertEquals(10, $objUnderTest->resultsPerPage()); // BETTER $this->assertEquals($objUnderTest::RESULTS_PER_PAGE, $objUnderTest->resultsPerPage()); Test assertions should be implementation-agnostic.油 Changing the internal implementation of a method should not break the test. public function testAddAndRemoveFriend() { 油油油 $friendId = 54; 油油油 $friends = new Friends(); 油油油 $friends->add($friendId); 油油油 $this->assertTrue($friends->isFriend($friendId)); 油油油 // you should stop here, below this should be a separate test 油油油 $friends->remove($friendId); 油油油 $this->assertFalse($friends->isFriend($friendId)); }
25. Test-Driven Development Stub out the methods for your class first, then write unit tests for that class. - At first, all tests will fail. - Write your class methods. - When all tests pass, you're done! 油 Also good for bug fixes. If you find a bug caused by unintended code behaviour,油 write a test that asserts the correct behaviour.油 When the test passes, the bug is fixed!
26. Writing Testable Code Testing a unit code involves sealing off the seams with mock objects and canned results. Introduce seams in your code to help with testing: - Modularize methods - Use setters/getters - Pass objects to a class' constructor 油 The following common coding practices make testing very difficult: - Creating monolithic functions that handle more than one responsibility - Using global variables - Creating objects within methods instead of asking for them tag_email::send(new tag_email_options($userId, 'cafe_convertgold', $data));