Key points of simulated object unit testing
If you are part of the development team, your code usually depends on the code written by your teammates. But what if their code is not available at the moment - for example, your teammates are not finished writing yet? Or, what if the code you need requires other external dependencies that are difficult to set? What if you can't test your code due to other factors that you can't control? Would you just hang out and do nothing, waiting for your team to finish or everything is ready? Of course not! In this article, I will explain how to write code to solve this dependency problem. Ideally, you need some background knowledge about unit testing, and there is already an excellent introductory article on unit testing written by Michelle Saver on SitePoint. Although this article does not require it, please check out my other articles on automated database testing.
Case of simulated object
As you may have guessed, mocking objects can solve the tricky situations I mentioned in the introduction. But what is a mock object? A mock object is a substitute object that replaces the actual implementation of the actual object. Why do you want a substitute object instead of a real object? Mock objects are used for unit testing to simulate the running behavior of real objects in test cases. By using them, the functionality of the objects you are implementing will be easier to test. Here are some useful situations when using mock objects:
object has not been implemented yet. Suppose your task is to do some processing of some data in the database. You might call some form of data access to an object or a data repository, but what if the database is not set yet? What if no data is available (I have encountered too many times) or the code to query the database has not been written yet? Simulate data access objects simulate real data access objects by returning some predefined values. This saves you from the burden of setting up a database, finding data, or writing code that queries a database.
Simulation objects in practice
Now that we know what a mock object is, let's take a look at some practical examples. We will implement the simple features mentioned earlier, such as counting likes and comments on Facebook posts. We will start with the following unit tests to define our expectations about how the object will be called and what the return value will be:
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { // test here } class MockFacebookLibrary { public function get($id) { return array( // mock return from Facebook here ); } }
<?php class StatusService { // other lines of code public function setFacebook($facebook) { $this->facebook = facebook; } }
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
The test still fails, but at least we no longer receive errors about calling methods on non-objects. More importantly, you just solved the need to meet this dependency. You can now start writing business logic for the tasks you are assigned and pass the test.
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
Go one step further: Use simulation framework
While you can use handmade mock objects when you first start, then, as I discovered myself, as your needs become more complex, you may need to use a real mock framework. In this article, I will show how to use the mock framework that comes with PHPUnit. In my experience, there are some benefits to using a mock framework compared to using a manually written mock object:
Simulation framework using PHPUnit
Let's focus now on the simulation framework using PHPUnit, the steps are actually very intuitive and once you get the grasp of it, it becomes second nature. In this section, we will use PHPUnit's simulation framework to create a mock object for our example case. However, before we do this, comment out or delete the line of code in the test that uses our handmade mock objects. We need to fail first so that we can pass. Later, we will inject a new simulation implementation.
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { // test here } class MockFacebookLibrary { public function get($id) { return array( // mock return from Facebook here ); } }
Verify that the test does fail when running PHPUnit. Now, think about how we can manually simulate an object and the method we want to call. What did we do?
The first step is to identify the object to be simulated. In the above analysis example function, we simulated the Facebook library. We are doing the same thing as in the first step.
Now that we have defined the class to be mocked, we must know the methods in the class to be mocked, and if any methods exist, specify the parameters and return values. The basic template I use in most cases is roughly as follows:
Let's apply the steps mentioned just now to our sample test.
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
After we create the mock facebook object again, inject it into our service again:
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
Now, you should pass the test again. Congratulations! You've started testing with mock objects! Hopefully you can program more effectively and most importantly get rid of the obstacles you encounter in the future dependencies.
Pictures from Fotolia
(The FAQ section about mock object testing should be added here, the content is consistent with the FAQ part in the input text, but it needs to be slightly rewrite and polished to avoid duplication)
The above is the detailed content of PHP Master | An Introduction to Mock Object Testing. For more information, please follow other related articles on the PHP Chinese website!