用Mockery模拟您的测试依赖项

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

模拟对象可能有多种原因

  1. 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.

    执行单元测试时,最好隔离类。 您不希望其他类或服务干扰您的单元测试。
  2. The object doesn’t exist yet. You can first create the tests, then build the final objects.

    该对象尚不存在。 您可以首先创建测试,然后构建最终对象。
  3. 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.xmlPHPUnit配置文件。

<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/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值