虽然并非每个人都在这样做,但测试你的应用程序是作为开发人员最基本的部分之一。单元测试是最常见的测试。通过单元测试,你可以检查一个类是否完全按照你的预期那样运行。有时,你在你的应用程序中使用的是第三方服务,很难设置好所有内容来进行单元测试。这正是模拟发挥作用的时候。
关键要点
模拟对象只不过是创建一个替代对象,它在单元测试中替换真实对象。如果你的应用程序严重依赖依赖注入,模拟是可行的方法。
模拟对象可能有几个原因:
运行单元测试时,你可能正在使用 PHPUnit。PHPUnit 带有一些默认的模拟功能,如文档中所示。你可以在 Jeune Asuncion 撰写的这篇文章中阅读更多关于模拟的常规信息以及 PHPUnit 的模拟功能。
在本文中,我们将深入探讨由 Pádraic Brady 创建的库 Mockery。我们将创建一个温度类,该类将注入当前不存在的天气服务。
让我们从设置项目开始。我们从包含以下内容的 composer.json 文件开始。这将确保我们拥有 mockery 和 PHPUnit。
<code>{ "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.*" } }</code>
我们还创建一个名为 phpunit.xml 的 PHPUnit 配置文件
<phpunit> <testsuite name="SitePoint Weather"> <directory>./tests</directory> </testsuite> <listeners> <listener class="\Mockery\Adapter\Phpunit\TestListener" file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php"/> </listeners> </phpunit>
定义这个监听器很重要。如果没有监听器,如果方法 once()
、twice()
和 times()
使用不正确,则不会引发错误。稍后将详细介绍。
我还创建了 2 个目录。src 目录用于保存我的类,tests 目录用于存储我们的测试。在 src 目录中,我创建了路径 SitePointWeather。
我们首先创建 WeatherServiceInterface。我们不存在的天气服务将实现此接口。在这种情况下,我们只提供一个方法,该方法将为我们提供摄氏温度。
<code>{ "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.*" } }</code>
因此,我们有一个服务可以为我们提供摄氏温度。我想获得华氏温度。为此,我创建了一个名为 TemperatureService 的新类。此服务将注入天气服务。除此之外,我们还定义了一个方法,该方法将摄氏温度转换为华氏温度。
<phpunit> <testsuite name="SitePoint Weather"> <directory>./tests</directory> </testsuite> <listeners> <listener class="\Mockery\Adapter\Phpunit\TestListener" file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php"/> </listeners> </phpunit>
我们已经准备好设置单元测试了。我们在 tests 目录中创建一个 TemperatureServiceTest 类。在这个类中,我们创建方法 testGetTempFahrenheit()
,它将测试我们的华氏方法。
此方法中要做的第一步是创建一个新的 TemperatureService 对象。就在我们这样做的时候,我们的构造函数将请求一个实现了 WeatherServiceInterface 的对象。由于我们还没有这样的对象(我们也不想要),我们将使用 Mockery 为我们创建一个模拟对象。让我们看看完成后的方法是什么样的。
namespace SitePoint\Weather; interface WeatherServiceInterface { /** * 返回摄氏温度 * * @return float */ public function getTempCelsius(); }
我们首先创建模拟对象。我们告诉 Mockery 我们想要模拟哪个对象(或接口)。第二步是描述将在此模拟对象上调用的方法。在 shouldReceive()
方法中,我们定义将调用的方法的名称。
我们定义此方法将调用的次数。我们可以使用 once()
、twice()
和 times(X)
。在这种情况下,我们预计它只会调用一次。如果未调用或调用次数过多,单元测试将失败。
最后,我们在 andReturn()
方法中定义将返回的值。在这种情况下,我们返回 25。Mockery 还具有 andReturnNull()
、andReturnSelf()
和 andReturnUndefined()
等返回方法。如果这是你的预期,Mockery 也能够抛出异常。
我们现在有了模拟对象,可以创建我们的 TemperatureService 对象并像往常一样进行测试。25 摄氏度是 77 华氏度,因此我们检查是否从我们的 getTempFahrenheit()
方法中收到 77。
如果你在你的根目录中运行 vendor/bin/phpunit tests/
,你将从 PHPUnit 获得绿灯,表明一切都很完美。
上面的例子相当简单。没有参数,只是一个简单的调用。让我们让事情变得复杂一些。
假设我们的天气服务还有一个方法可以在确切的小时获取温度。我们将以下方法添加到我们当前的 WeatherServiceInterface。
namespace SitePoint\Weather; class TemperatureService { /** * @var WeatherServiceInterace $weatherService 保存天气服务 */ private $weatherService; /** * 构造函数。 * * @param WeatherServiceInterface $weatherService */ public function __construct(WeatherServiceInterface $weatherService) { $this->weatherService = $weatherService; } /** * 获取当前华氏温度 * * @return float */ public function getTempFahrenheit() { return ($this->weatherService->getTempCelsius() * 1.8000) + 32; } }
我们想知道,晚上 0:00 到 6:00 之间的平均温度是多少。为此,我们在 TemperatureService 中创建一个新方法来计算平均温度。为此,我们从 WeatherService 中检索 7 个温度并计算平均值。
<code>{ "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.*" } }</code>
让我们看看我们的测试方法。
<phpunit> <testsuite name="SitePoint Weather"> <directory>./tests</directory> </testsuite> <listeners> <listener class="\Mockery\Adapter\Phpunit\TestListener" file="vendor/mockery/mockery/library/Mockery/Adapter/Phpunit/TestListener.php"/> </listeners> </phpunit>
我们再次模拟接口,并定义将调用的方法。接下来,我们定义此方法将调用的次数。我们在前面的示例中使用了 once()
,现在我们使用 times(7)
来指示我们期望此方法被调用 7 次。如果该方法没有被精确调用 7 次,则测试将失败。如果你没有在 phpunit.xml 配置文件中定义监听器,你将不会收到关于此的通知。
接下来,我们定义 with()
方法。在 with
方法中,你可以定义你期望的参数。在这种情况下,我们期望 7 个不同的小时。
最后,我们有 andReturn()
方法。在这种情况下,我们指示了 7 个返回值。如果你定义的返回值较少,则每次都会重复最后一个可用的返回值。
当然,Mockery 可以做更多的事情。有关完整的指南和文档,我建议你查看 Github 页面。
如果你对上面项目的代码感兴趣,你可以查看这个 Github 页面。
使用 PHPUnit,你已经可以模拟对象了。但是,你也可以像上面示例中解释的那样使用 Mockery。如果你正在对你的类进行单元测试,并且你不想让任何其他类影响你的测试,mockery 可以轻松地帮助你。如果你真的想进行功能测试,最好看看你是否可以集成真正的测试。你目前是否正在使用 PHPUnit 模拟并考虑切换到 Mockery?你想在后续文章中看到更多更大的 Mockery 示例吗?请在下面的评论中告诉我。
Mockery 是一个强大而灵活的 PHP 模拟对象框架,用于单元测试。它被设计为 PHPUnit 模拟对象功能的直接替代品。Mockery 允许开发人员隔离被测代码并创建测试替身,这些测试替身模拟复杂对象的行为。这在单元测试中至关重要,因为它确保被测代码不依赖于任何外部因素或状态。
要安装 Mockery,你需要拥有 Composer,这是一个 PHP 的依赖项管理器。你可以通过运行命令 composer require --dev mockery/mockery
来安装 Mockery。安装后,你可以在测试文件中通过在测试拆卸方法中调用 Mockery::close()
来设置 Mockery,以清理模拟对象。
在 Mockery 中创建模拟对象很简单。你可以使用 mock()
方法来创建一个模拟对象。例如,$mock = Mockery::mock('MyClass');
将创建一个 MyClass 的模拟对象。
在 Mockery 中,你通过将方法链接到模拟对象来定义期望。例如,$mock->shouldReceive('myMethod')->once()->andReturn('mocked value');
此代码告诉 Mockery 预期“myMethod”将被调用一次,并且应该返回“mocked value”。
在 Mockery 中,模拟是我们可以在其上设置期望的对象,而存根是预先编程了响应的模拟对象。当响应是唯一重要的事情时,通常使用存根,而当测试交互本身时,则使用模拟。
不建议直接测试私有方法,因为它违反了封装原则。但是,如果需要,你可以使用 Mockery 中的 shouldAllowMockingProtectedMethods()
方法来允许模拟受保护和私有方法。
如果要模拟的类具有带参数的构造函数,可以将它们作为数组传递给 mock()
方法。例如,$mock = Mockery::mock('MyClass', [$arg1, $arg2]);
将将 $arg1 和 $arg2 传递给 MyClass 的构造函数。
Mockery 提供了一种使用 alias:
前缀模拟静态方法的方法。例如,$mock = Mockery::mock('alias:MyClass');
将创建一个可以用来对 MyClass 的静态方法设置期望的模拟对象。
你可以在测试拆卸方法中使用 Mockery::close()
方法来验证是否已满足所有期望。如果任何期望未满足,Mockery 将引发异常。
你可以使用 andThrow()
方法设置模拟对象以抛出异常。例如,$mock->shouldReceive('myMethod')->andThrow(new Exception);
将在调用“myMethod”时抛出异常。
以上是用嘲笑嘲笑您的测试依赖性的详细内容。更多信息请关注PHP中文网其他相关文章!