【初学者必看】从Mock的基础知识到如何使用*PHP、Mockery

目录
感谢大家的辛勤付出!
我是Eita,一名刚入职场的第一年的新晋工程师!
我开始工作已经六个月了,时间过得如此之快,让我感到害怕(第二年的压力、明年对新毕业生的培训、时间和技术发展速度差异带来的挫败感等等)。
好吧,我先把焦虑和不耐烦放在一边,来谈谈在PHP 测试中使用 mock 函数的问题。
我想解释一下什么是模拟,它在实际应用中的场景,以及它的各种使用方法!
谢谢!*这是一种使用 Mockery 的方法。
什么是模拟?
首先,让我解释一下什么是模拟。
已实现代码的操作进行单元测试,它会替换测试对象所依赖的类,并验证这些类和方法是否被正确调用!
可以使用其中一个测试替身指定方法的返回值,以及验证调用参数和调用次数
但如果仅此而已,似乎有点毫无意义,所以这里举一个具体的使用例子……
案例 1
我们已经实现了使用外部 API 进行支付的方法。
因此,如果您决定在不使用模拟对象的情况下运行测试,每次运行测试时都会访问外部 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, 2nd)
例如:如果你想在一个判断某个东西是否可用的方法中,第一次返回 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(class name::class, partial mock object)
5. 我想模拟静态方法
这次,我想模拟创建支付历史记录的静态方法。
然而,正常的模拟涉及模拟实例方法来改变对象的行为,而静态方法与类本身绑定,而不是与已实例化的类的实例绑定,因此似乎有必要使用特殊方法。
为了解决这个问题,有一种叫做别名模拟
示例:模拟一个静态方法,该方法根据付款 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/
概括
你觉得怎么样?对你有帮助吗?
我本人也写关于模型制作的文章,每次遇到想要模拟的新模式时,我都会想,“我该怎么做呢?!”但我每天都尽力做到最好(大概吧……)。
所以,这次我们研究了模拟对象,它是一种测试替身,但还有其他类型的测试替身:桩对象、间谍对象、伪对象和虚拟对象。
我只了解一些简单的行为,所以如果您感兴趣,请您研究一下并告诉我,我将不胜感激。
未来我会以一名初级工程师的视角发布很多信息,所以一定要把我添加到你的收藏夹里哦!
7