Although not everyone is doing it yet, testing your application is one of the most essential parts of being a developer. Unit tests are the most common tests to run. With unit tests, you can check if a class behaves exactly like you intended it too. Sometimes, you are using a third party service within your application and it’s hard to get everything set up to get this unit tested. That’s exactly when mocking comes into play.
尽管还不是每个人都在做,但是测试您的应用程序是成为开发人员最重要的部分之一。 单元测试是最常见的运行测试。 使用单元测试,您可以检查类的行为是否也完全符合您的预期。 有时,您在应用程序中使用第三方服务,因此很难进行所有设置以测试此单元。 这正是嘲弄开始发挥作用的时候。
什么在嘲笑? (What is mocking?)
Mocking an object is nothing more than creating a stand-in object, which replaces the real object in a unit test. If your application heavily relies on dependency injection, mocking is the way to go.
模拟对象无非就是创建一个替代对象,该替代对象将在单元测试中替换实际对象。 如果您的应用程序严重依赖于依赖项注入,那么就可以采用模拟方法。
There can be several reasons to mock objects
模拟对象可能有多种原因
- When performing unit tests, it’s best to isolate the class. You don’t want another class or service to interfere with your unit test. 执行单元测试时,最好隔离类。 您不希望其他类或服务干扰您的单元测试。
- The object doesn’t exist yet. You can first create the tests, then build the final objects. 该对象尚不存在。 您可以首先创建测试,然后构建最终对象。
- A mock object is generally faster than preparing a whole database for your test. 模拟对象通常比为测试准备整个数据库要快。
When running unit tests, you are probably using PHPUnit. PHPUnit comes with some default mocking abilities as you can see in the documentation. You can read more about mocking in general and the mocking abilities from PHPUnit in this article written by Jeune Asuncion.
运行单元测试时,您可能正在使用PHPUnit。 PHPUnit带有一些默认的模拟功能,如您在文档中所见。 在Jeune Asuncion撰写的这篇文章中,您可以从PHPUnit中阅读更多有关一般的模拟和模拟功能的信息。
In this article, we will dive into Mockery, a library created by Pádraic Brady. We will create a temperature class which gets a currently non existing weather service injected.
在本文中,我们将深入研究由PádraicBrady创建的图书馆Mockery 。 我们将创建一个温度类,以获取当前不存在的天气服务。
建立 (Setup)
Let’s start by setting up our project. We start off with a composer.json
file which contains the following content. This will make sure we have mockery and PHPUnit available.
让我们从设置项目开始。 我们从一个composer.json
文件开始,该文件包含以下内容。 这将确保我们可以使用嘲讽和PHPUnit。
{
"name": "sitepoint/weather",
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-0": { "": "src/" }
},
"require-dev": {
"phpunit/phpunit": "4.1.*",
"mockery/mockery": "0.9.*"
}
}
We also create a PHPUnit config file named phpunit.xml
我们还创建了一个名为phpunit.xml
PHPUnit配置文件。
<phpunit>
<testsuite name="SitePoint Weather">
<directory>src</directory>
</testsuite>
<listeners>
<listener class="\Mockery\Adapter\Phpunit\TestListener"
file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php">
</listener>
</listeners>
</phpunit>
It’s important to define this listener. Without the listener, methods like once()
, twice()
and times()
won’t thrown an error if they are not used correctly. More on that later.
定义此侦听器很重要。 如果没有使用侦听器,则如果使用不正确,则twice()
once()
, twice()
和times()
不会引发错误。 以后再说。
I also created 2 directories. The src
directory to keep my classes in and a tests
directory to store our tests. Within the src
directory, I created the path SitePoint\Weather
.
我还创建了2个目录。 src
目录用于保存我的课程, tests
目录用于存储测试。 在src
目录中,我创建了路径SitePoint\Weather
。
We start off by creating the WeatherServiceInterface
. Our non existing weather service will implement this interface. In this case, we only provide a method which will give us a temperature in Celsius.
我们首先创建WeatherServiceInterface
。 我们不存在的天气服务将实现此接口。 在这种情况下,我们仅提供一种可以为我们提供摄氏温度的方法。
namespace SitePoint\Weather;
interface WeatherServiceInterface
{
/**
* Return the Celsius temperature
*
* @return float
*/
public function getTempCelsius();
}
So, we have a service which provides us with a temperature in Celsius. I would like to get the temperature in Fahrenheit. For that, I create a new class named TemperatureService
. This service will get the weather service injected. Next to that, we also define a method which will convert the Celsius temperature to Fahrenheit.
因此,我们提供的服务可为我们提供摄氏温度。 我想知道华氏温度。 为此,我创建了一个名为TemperatureService
的新类。 该服务将注入天气服务。 紧接着,我们还定义了一种将摄氏温度转换为华氏温度的方法。
namespace SitePoint\Weather;
class TemperatureService
{
/**
* @var WeatherServiceInterace $weatherService Holds the weather service
*/
private $weatherService;
/**
* Constructor.
*
* @param WeatherServiceInterface $weatherService
*/
public function __construct(WeatherServiceInterface $weatherService) {
$this->weatherService = $weatherService;
}
/**
* Get current temperature in Fahrenheit
*
* @return float
*/
public function getTempFahrenheit() {
return ($this->weatherService->getTempCelsius() * 1.8000) + 32;
}
}
创建单元测试 (Create the unit test)
We have everything in place to set up our unit test. We create a TemperatureServiceTest
class within the tests
directory. Within this class we create the method testGetTempFahrenheit()
which will test our Fahrenheit method.
我们已准备好一切来设置单元测试。 我们在tests
目录中创建一个TemperatureServiceTest
类。 在此类中,我们创建方法testGetTempFahrenheit()
来测试我们的Fahrenheit方法。
The first step to do within this method is to create a new TemperatureService
object. Right at the moment we do that, our constructor will ask for an object with the WeatherServiceInterface
implemented. Since we don’t have such an object yet (and we don’t want one), we are going to use Mockery to create a mock object for us. Let’s have a look at how the method would look when it’s completely finished.
在此方法中要做的第一步是创建一个新的TemperatureService
对象。 就在此时,我们的构造函数将要求一个实现了WeatherServiceInterface
的对象。 由于我们还没有这样的对象(并且我们不想要这样的对象),因此我们将使用Mockery为我们创建一个模拟对象。 让我们看一下该方法完全完成后的外观。
namespace SitePoint\Weather\Tests;
use SitePoint\Weather\TemperatureService;
class TemperatureServiceTest extends \PHPUnit_Framework_TestCase
{
public function testGetTempFahrenheit() {
$weatherServiceMock = \Mockery::mock('SitePoint\Weather\WeatherServiceInterface');
$weatherServiceMock->shouldReceive('getTempCelsius')->once()->andReturn(25);
$temperatureService = new TemperatureService($weatherServiceMock);
$this->assertEquals(77, $temperatureService->getTempFahrenheit());
}
}
We start off by creating the mock object. We tell Mockery which object (or interface) we want to mock. The second step is to describe which method will be called on this mock object. Within the shouldReceive()
method, we define the name of the method that will be called.
我们首先创建模拟对象。 我们告诉Mockery我们要模拟哪个对象(或接口)。 第二步是描述将在该模拟对象上调用哪种方法。 在shouldReceive()
方法中,我们定义将被调用的方法的名称。
We define how many times this method will be called. We can use once()
, twice()
, and times(X)
. In this case, we expect it will only be called once. If it’s not called or called too many times, the unit test will fail.
我们定义此方法将被调用多少次。 我们可以使用once()
, twice()
和times(X)
。 在这种情况下,我们希望它只会被调用一次。 如果未多次调用或多次调用,则单元测试将失败。
Finally we define in the andReturn()
method, what the value is that will be returned. In this case, we are returning 25
. Mockery also has return methods like andReturnNull()
, andReturnSelf()
and andReturnUndefined()
. Mockery is also capable of throwing back exceptions if that is what you expect.
最后,我们在andReturn()
方法中定义将返回的值。 在这种情况下,我们将返回25
。 Mockery还具有返回方法,例如andReturnNull()
, andReturnSelf()
和andReturnUndefined()
。 如果您期望的那样,Mockery还可以抛出异常。
We have our mock object now and can create our TemperatureService
object and do a test as usual. 25 Celsius is 77 Fahrenheit, so we check if we receive 77 back from our getTempFahrenheit()
method.
现在,我们有了模拟对象,可以像往常一样创建我们的TemperatureService
对象并进行测试。 25摄氏度是77华氏度,因此我们检查是否从getTempFahrenheit()
方法返回77。
If you run vendor/bin/phpunit tests/
within your root, you will get a green light from PHPUnit, indicating everything is perfect.
如果您在根目录中运行vendor/bin/phpunit tests/
,则PHPUnit会亮起绿灯,表明一切都完美。
高级 (Advanced)
The example above was fairly simple. No parameters, just one simple call. Let’s make things a bit more complicated.
上面的示例非常简单。 没有参数,只有一个简单的调用。 让我们做些复杂的事情。
Let’s say our weather service also has a method to get the temperature on an exact hour. We add the following method to our current WeatherServiceInterface
.
假设我们的气象服务也提供了一种获取精确小时温度的方法。 我们将以下方法添加到当前的WeatherServiceInterface
。
/**
* Return the Celsius temperature by hour
*
* @param $hour
* @return float
*/
public function getTempByHour($hour);
We would like to know, what the average temperature is between 0:00 and 6:00 at night. For that, we create a new method in our TemperatureService
which calculates the average temperature. For that, we are retrieving 7 temperatures from our WeatherService
and calculating the average.
我们想知道晚上0:00和6:00之间的平均温度是多少。 为此,我们在TemperatureService
创建一个新方法来计算平均温度。 为此,我们要从WeatherService
检索7个温度并计算平均值。
/**
* Get average temperature of the night
*
* @return float
*/
public function getAvgNightTemp() {
$nightHours = array(0, 1, 2, 3, 4, 5, 6);
$totalTemperature = 0;
foreach($nightHours as $hour) {
$totalTemperature += $this->weatherService->getTempByHour($hour);
}
return $totalTemperature / count($nightHours);
}
Let’s have a look at our test method.
让我们看看我们的测试方法。
public function testGetAvgNightTemp() {
$weatherServiceMock = \Mockery::mock('SitePoint\Weather\WeatherServiceInterface');
$weatherServiceMock->shouldReceive('getTempByHour')
->times(7)
->with(\Mockery::anyOf(0, 1, 2, 3, 4, 5, 6))
->andReturn(14, 13, 12, 11, 12, 12, 13);
$temperatureService = new TemperatureService($weatherServiceMock);
$this->assertEquals(12.43, $temperatureService->getAvgNightTemp());
}
Once again we mock the interface and we define the method which will be called. Next, we define how many times this method will be called. We used once()
in the previous example, now we are using times(7)
to indicate we expect this method to be called 7 times. If the method is not called exactly 7 times, the test will fail. If you didn’t define the listener in the phpunit.xml
config file, you wouldn’t receive a notice about this.
再次模拟接口,并定义将被调用的方法。 接下来,我们定义此方法将被调用多少次。 在上一个示例中,我们使用once()
,现在我们使用times(7)
来表示我们希望此方法被调用7次。 如果未正确调用该方法7次,则测试将失败。 如果未在phpunit.xml
配置文件中定义侦听器,则不会收到有关此消息的通知。
Next, we define the with()
method. In the with method, you can define the parameters you are expecting. In this case, we are expecting the 7 different hours.
接下来,我们定义with()
方法。 在with方法中,您可以定义所需的参数。 在这种情况下,我们期望7个不同的小时。
And lastly, we have the andReturn()
method. In this case, we indicated the 7 values returned. In case you define fewer return values, the last return value available will be repeated each time.
最后,我们有andReturn()
方法。 在这种情况下,我们指出了返回的7个值。 如果定义的返回值较少,则每次都会重复最后可用的返回值。
Of course, Mockery can do a lot more. For a complete guide and documentation, I recommend you take a look at the Github page.
当然,Mockery可以做更多的事情。 有关完整的指南和文档,建议您查看Github页面 。
If you are interested in the code from the project above, you can take a look on this Github page.
如果您对上面项目中的代码感兴趣,可以在Github页面上查看 。
结论 (Conclusion)
With PHPUnit, you can already mock objects. However, you can also use Mockery as explained in the examples above. If you are unit testing your classes and you don’t want any other classes affecting your test, mockery can help you out with ease. If you really want to do functional tests, it’s better to have a look if you can integrate the real deal of course. Are you currently using PHPUnit mocking and are thinking about switching to Mockery? Would you like to see more and bigger examples of Mockery in a follow up article? Let me know in the comments below.
使用PHPUnit,您已经可以模拟对象。 但是,您也可以按照上述示例中的说明使用Mockery。 如果您正在对类进行单元测试,并且不希望任何其他类影响您的测试,那么嘲笑可以轻松地帮助您。 如果您真的想进行功能测试,那么最好可以看看是否可以集成真正的功能。 您当前正在使用PHPUnit模拟,并且正在考虑切换到Mockery吗? 您想在后续文章中看到更多和更大的Mockery示例吗? 在下面的评论中让我知道。
翻译自: https://www.sitepoint.com/mock-test-dependencies-mockery/