【新手必看】Mock 基础知识及使用指南 ※PHP,Mockery

感谢大家的辛勤付出!

我是Eita,一名刚入职场的第一年的新晋工程师!

我开始工作已经六个月了,时间过得如此之快,让我感到害怕(第二年的压力、明年对新毕业生的培训、时间和技术发展速度差异带来的挫败感等等)。

总之,先放下我的焦虑和不耐烦,这次我要谈谈PHP 中运行测试时如何使用 `mock`。

我想解释一下什么是模拟,它在实际应用中的场景,以及它的各种使用方法!

谢谢!*这是一种使用 Mockery 的方法。

什么是模拟?

首先,让我解释一下什么是模拟。

已实现代码的行为进行单元测试涉及用模拟对象替换被测代码所依赖的类,以验证这些类和方法是否被正确调用

此外,其中一个测试替身指定方法的返回值,并验证调用参数和调用次数允许您

但如果仅此而已,似乎有点毫无意义,所以这里举一个具体的使用例子……

案例 1

我们已经实现了使用外部 API 进行支付的方法。

因此,如果您在运行测试时未使用模拟对象,则每次运行测试都会产生费用。此外,如果代码编写方式导致相关方法被频繁执行,则可能会因为对提供外部 API 的服务器进行大量访问而造成问题(例如无意的拒绝服务攻击)

情况 2:当所需方法未实现时

假设你想测试一个方法,该方法的行为会根据该方法返回的布尔值而改变。

但是,我们尚未完成返回布尔值的方法的实现。

我该怎么办?嗯,我可以先实现它,但假设由于某种原因我无法先实现它。

所以!模拟在上述两种情况下都很有用!

实际使用情况(附代码)

基本用法

例如,我们现在将测试一种获取支付信息并使用外部支付服务(API)进行支付的方法。

在这种情况下,您需要模拟使用外部支付服务(API)的部分。

我已经写出了一个用于模拟的代码的粗略大纲。

使用模拟的部分第 38 行

    // 使用外部支付服务的类 class PaymentDataFromExternal { // 如果支付成功完成,则返回 true 的方法 public function paymentImmediately(int $paymentId) { // 执行支付,如果成功则返回 true return true; } } class PaymentService { public function __construct( private PaymentDataFromExternal $paymentDataFromExternal ){} // 待测试的方法 public function execPayment() { $payment = fetchPaymet(); // 获取支付信息 // 根据获取的支付信息执行支付(与外部 API 通信) $isPayment = $this->paymentDataFromExternal->paymentImmediately($payment->id); return $isPayment; } } class Test { public function test_payment_was payment executed successfully?() { // 创建临时支付信息 $payment = Payment::factory(); // 包含要模拟的方法的类 $this->mock(PaymentDataFromExternal::class, function($mock) { $mock->shouldReceive('paymentImmediately') // 方法名 ->with($payment->id) // 参数 ->andReturn(true); // 返回值 }); // 用模拟对象替换模拟类 $paymentService = app(PaymentService::class); // 执行被替换为模拟对象的方法 $result = $paymentService->execPayment(); $this->assertTrue($result); } }
    

因此,我想列出一些我实际遇到的模拟模式(其中大多数在官方模拟文档中没有直接描述)。

需要注意的是,为了在测试期间使用模拟对象,必须有适当的依赖注入(DI)似乎

1. 我想将模拟方法的返回值作为继承自某个类的对象返回。

我想模拟一个方法(返回一个集合),该方法从用户表中检索所有用户。

但是,如果在模拟时直接将 User 模型作为返回值传递,则会收到一个错误,提示返回值不同,并且无法正确模拟。

模拟对象需要具有与被模拟的实际方法相匹配的参数和返回类型发现

因此,在这种情况下,需要使用 PHP 的匿名类来继承指定的类。

实际代码如下:

示例:模拟一个方法,根据 user_id 获取所有用户信息集合。

    $mock->shouldReceive('fetchUsersById') ->once() ->with($user->id) ->andReturn(new class extends Illuminate\Database\Eloquent\Collection([$userMock]));
    

如上所示,通过编写“new class extends 你想继承的类”,你可以指定类型并成功模拟它。

2. 当模拟方法被多次调用时,我希望它根据调用次数返回不同的值。

这次,在被测试的方法中,有一个方法被调用了两次,我想模拟这个方法。

如果两次调用都接受相同的返回值,你可以写成 `andReturn(true)`,这样就不会有问题,因为模拟方法每次调用都会返回 `true`。但是,在这个例子中,我们希望它第一次返回 `true`,第二次返回 `false`

在这种情况下,andReturn(1st time, 2nd time)将其指定为

例如:如果你想在一个判断某个东西是否可用的方法中,第一次返回 true,第二次返回 false。

 
    $mock->shouldReceive('isEnabled')->twice()->andReturn(true, false);
    

3. 我想为模拟对象的类属性设置一个值。

这次,我们要模拟从用户表中检索用户信息的方法。

但,

我想验证该值是否设置正确,因为我正在方法中为类属性设置值。

我遇到了麻烦,因为我不能直接模拟它并给类属性设置值。

在这种情况下,andSet(property, value to set)为类属性设置值

例如:模拟一个返回用户信息的方法,并将 name 属性设置为“佐藤先生”。

       $mock->shouldReceive('getUserInfo') ->once() ->andReturn($user) ->andSet('name', 'Mr. Sato');
    

4. 我想模拟被测类的一些方法。

在这种情况下,需要在与被测方法相同的类中模拟一个类。

如果只是简单地模拟,那么除了被测方法之外的其他方法也会被模拟对象替换。

因此,你只需要模拟你正在测试的方法。

在这种情况下,“部分模型”似乎是有效的。

但是,如果您只是使用部分模拟对象进行模拟,则它不会正确地被模拟对象替换。

经调查,我发现……

看来部分模拟对象不能简单地通过实例化来替换为模拟对象;看来我们需要显式地注入依赖项。

    class SampleClass { public function getName() { 等等... } public function fetchDataFromExternal() { // 用于执行外部通信的代码 } } class Test { $partialMock = Mockery::mock(SampleClass::class)->makePartial(); $mock->shouldReceive('fetchDataFromExternal') ->once(); // 通过编写以下代码,当调用该类时,它将被替换为 $partialMock $this->instance(SampleClass::class, $partialMock); }
    

如上所述,您可以使用 makePartial 来使用部分模拟对象,并且$this->instance(ClassName::class, Partial Mock Object)似乎您还可以通过这样编写来执行依赖注入

5. 我想模拟静态方法

这次,我想模拟创建支付历史记录的静态方法。

然而,正常的模拟涉及模拟实例方法来改变对象的行为,而静态方法与类本身绑定,而不是与已实例化的类的实例绑定,因此似乎有必要使用特殊方法。

为了解决这个问题,alias smock可以使用

示例:模拟一个静态方法,该方法根据付款 ID 和价格创建付款历史记录。

    Mockery::mock('alias:'.History::class)->shouldReeive('makePurchase')->with($payment->id, $payment->price)->andReturn($purchaseHistory);
    

如上所示('alias:'.classname::class),静态方法似乎可以通过将其写成

然而..

虽然使用上述方法在技术上支持模拟静态方法,但官方文档不建议这样做指出

原因是,如果两个或多个测试用例的类名相同,就会发生错误,因此似乎有必要将每个测试用例分开。

解决方案:您需要在文档中添加以下内容(但这仍然存在许多其他问题,因此不建议这样做):

您可以通过查看下面的描述找到它。

    /** * @runInSeparateProcess * @preserveGlobalState 禁用 */
    

以上信息参考:kaihttps://docs.phpunit.de/en/11.4/annotations.html

以上就是本次介绍的全部内容。

官方文档对上述方法进行了解释,包括 once()、with() 和 Return()(还有许多其他方法),请查阅该文档。

Mockery 文档:https://docs.mockery.io/en/latest/

概括

你觉得怎么样?对你有帮助吗?

我本人也写关于模型制作的文章,每次遇到想要模拟的新模式时,我都会想,“我该怎么做呢?!”但我每天都尽力做到最好(大概吧……)。

所以,这次我们研究了模拟对象,它是一种测试替身,但还有其他类型的测试替身:桩对象、间谍对象、伪对象和虚拟对象。

我只了解一些简单的行为,所以如果您感兴趣,请您研究一下并告诉我,我将不胜感激。

未来我会以一名初级工程师的视角发布很多信息,所以一定要把我添加到你的收藏夹里哦!

如果您觉得这篇文章对您有帮助,请点个“赞”!
8
加载中...
8票,平均分:1.00/18
2,474
X Facebook Hatena书签 口袋

这篇文章的作者

关于作者

瑛塔

一名网页开发工程师,
只想出去闯闯。