测试过程中,mock一个类的静态方法,如何释放这个mock?

在测试第一个方法中,mock一个对象后,在第二个方法中怎么释放这个mock对象?可能我说的有点抽象,我先列一下我需要用到的类文件,后面我会详细说明过程,以及遇到的问题。

接口类:

namespace Contracts;

interface Pay
{
    public static function getMoney();
}

ServiceProvider:

class AppServiceProvider extends \Illuminate\Support\ServiceProvider
{
    public function register()
    {
        // 这里绑定了支付渠道接口(Pay)的实现为微信支付(Wechat)
        $this->app->bind(\Contracts\Pay::class, \Instances\Wechat::class);
    }
}

被测试类:

namespace Controllers;
// 钱包相关的控制器
class WalletController
{
    public $pay;

    public function __construct(\Contracts\Pay $pay)
    {
        $this->pay = $pay;
    }
    // 获取当前支付渠道的余额
    public function getBalance()
    {
        return $this->pay->getMoney();
    }
}

测试过程:

class baseTest extends \Illuminate\Foundation\Testing\TestCase
{
    // 测试程序会按照顺序执行
    // 这个会是第一个执行的方法
    public function testOne()
    {
        // 第一步:
        // 伪造类中的静态方法需要这么写'alias:{namespace\className}'
        $mock_pay = \Mockery::mock('alias:\Contracts\Pay');
        // 这里对mock出的实例进行配置,在调用 getMoney() 方法后返回 60
        $mock_pay->shouldReceive('getMoney')->andReturn(60);

        // 第二步:
        // 为了测试中不触发真的金钱变动,这里绑定mock的实例
        $this->app->bind(\Contracts\Pay::class, function($app) use($mock_pay) {
            return $mock_pay;
        });

        // 第三步:
        // 接下来测试被测试的代码块
        $ctrl = $this->app->make(\Controllers\WalletController::class);

        /**
         * 多谢 @leo 和 @zhangrongwang 的指正,此处已做修改
         * 原【$ctrl = new \Controllers\WalletController; 】这种写法是错误的
         * 只有当 WalletController 中,构造函数的参数的个数为 0 时,才可以不加小括号
         * 本例中 WalletController 的构造函数有一个参数,则需要通过 Container 来帮忙
         * 创建一个包含 $pay 参数的 $ctrl。
         */

        // 调用被测试的方法
        $this->assertEquals(60, $ctrl->getBalance());// 成功
    }

    // 继续执行第二个测试
    // 这个是关键,这里会出问题
    public function testTwo()
    {
        // 我直接开始测试代码块
        // 这里会直接报错
        $ctrl = $this->app->make(\Controllers\WalletController::class);
        // 调用被测试的方法
        $this->assertEquals(60, $ctrl->getBalance());
    }
}

上面是我的测试过程,

  • 如果单独执行testOne()是可以通过的。
  • 单独执行testTwo()也是可以通过的。
  • 一旦两个都开启测试,那么testTwo()会直接报错:
    PHP Fatal error:  Instances\Wechat cannot implement Contracts\Pay - it is not an interface in ...

原因是testOne()中已经把\Contracts\Pay这个接口变为了mockery实例,不再是一个接口了。

  • 我尝试过在testTwo()中执行$this->refreshApplication();
      public function testTwo()
      {
          $this->refreshApplication();
          .
          .
      }
  • 也试过重新绑定$this->app->rebinding();
    public function testTwo()
    {
        $this->app->rebinding(\Contracts\Pay::class, \Instances\Wechat::class);
        .
        .
    }

但都没有用,所以我想问下:怎么才能在testTwo()中,恢复AppServiceProvider中的绑定关系?

运行环境

root@Aliyun-ECS / # php -v
PHP 7.1.33 (cli) (built: Oct 26 2019 10:16:23) ( NTS )

root@Aliyun-ECS / # php artisan --version
Laravel Framework version 5.3.22

root@Aliyun-ECS / # vendor/phpunit/phpunit/phpunit --version
PHPUnit 5.6.2 by Sebastian Bergmann and contributors.
再见了妈妈今晚我就要远航,别为我担心我有快乐和智慧的桨~
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
最佳答案

感谢 @zhangrongwang 哥哥的帮助,现在我们可以进行静态方法的模拟了,而且模拟的操作不会影响到后面的测试。

  • 注意phpunit 不支持模拟静态方法:

    请注意,finalprivatestatic 方法无法对其进行上桩(stub)或模仿(mock)。PHPUnit 的测试替身功能将会忽略它们,并维持它们的原始行为。

所以我们需要通过Composer引入Mockery扩展包:

composer require mockery/mockery

现在我们就可以生成\Contracts\Pay接口的 桩件,替换掉原接口的绑定,而且不会影响后面的测试。接下来我们修改下单元测试:

<?php

class baseTest extends \Illuminate\Foundation\Testing\TestCase
{
    public function testOne()  {
        // 第一步:
        // 仿造 Pay 接口,返回此接口的桩件
        $stub = \Mockery::mock(\Contracts\Pay::class);

        // 对桩件进行配置,在调用 getMoney() 方法时返回 60
        $stub->shouldReceive('getMoney')->andReturn(60);

        // 第二步:
        // 将原来的返回 Wechat 实例改为返回 $stub 桩件
        $this->app->instance(\App\Contracts\Pay::class, $stub);

        // 这种更改绑定关系的写法也可以,与上面效果一致
        // $this->app->bind(\App\Contracts\Pay::class, function($app) use ($stub) {
        //     return $stub;
        // });

        // 第三步:
        // 接下来测试被测试的代码块
        $ctrl = $this->app->make(\Controllers\WalletController::class);

        // 调用被测试的方法
        $this->assertEquals(60,  $ctrl->getBalance());// 成功
    }
}

此问题已解决,再次感谢 @zhangrongwang 的帮助。

3年前 评论
讨论数量: 5
leo

$ctrl = new \Controllers\WalletController;

确定是这么写的?不会报错?

3年前 评论
LiamHao (楼主) 3年前
LiamHao (楼主) 3年前

我想,你可能需要这个 测试模拟器 Mocking
不过我也很好奇直接new controller居然没报错?laravel和php都什么版本啊?

public function testOne()
{
    $this->mock(\Contracts\Pay::class, function ($mock) {
       $mock->shouldReceive('getMoney')->andReturn(60);
    });

    // 第三步:
    // 接下来测试被测试的代码块
    $ctrl = $this->app->make(\Controllers\WalletController:class);
    // 调用被测试的方法
    $this->assertEquals(60, $ctrl->getBalance());// 成功
}
3年前 评论
zhangrongwang (作者) 3年前
zhangrongwang (作者) 3年前
LiamHao (楼主) 3年前
LiamHao (楼主) 3年前

@zhangrongwang 确实可以了,我的问题,没有看全文档。新的Laravel项目自带的测试文件默认继承的是\PHPUnit\Framework\TestCase类,而文档 测试模拟器 Mocking 中继承的是\Tests\TestCase类。文档当前页没有提及,说明也都是在 HTTP 测试 这页通过代码隐式说明的。

3年前 评论

感谢 @zhangrongwang 哥哥的帮助,现在我们可以进行静态方法的模拟了,而且模拟的操作不会影响到后面的测试。

  • 注意phpunit 不支持模拟静态方法:

    请注意,finalprivatestatic 方法无法对其进行上桩(stub)或模仿(mock)。PHPUnit 的测试替身功能将会忽略它们,并维持它们的原始行为。

所以我们需要通过Composer引入Mockery扩展包:

composer require mockery/mockery

现在我们就可以生成\Contracts\Pay接口的 桩件,替换掉原接口的绑定,而且不会影响后面的测试。接下来我们修改下单元测试:

<?php

class baseTest extends \Illuminate\Foundation\Testing\TestCase
{
    public function testOne()  {
        // 第一步:
        // 仿造 Pay 接口,返回此接口的桩件
        $stub = \Mockery::mock(\Contracts\Pay::class);

        // 对桩件进行配置,在调用 getMoney() 方法时返回 60
        $stub->shouldReceive('getMoney')->andReturn(60);

        // 第二步:
        // 将原来的返回 Wechat 实例改为返回 $stub 桩件
        $this->app->instance(\App\Contracts\Pay::class, $stub);

        // 这种更改绑定关系的写法也可以,与上面效果一致
        // $this->app->bind(\App\Contracts\Pay::class, function($app) use ($stub) {
        //     return $stub;
        // });

        // 第三步:
        // 接下来测试被测试的代码块
        $ctrl = $this->app->make(\Controllers\WalletController::class);

        // 调用被测试的方法
        $this->assertEquals(60,  $ctrl->getBalance());// 成功
    }
}

此问题已解决,再次感谢 @zhangrongwang 的帮助。

3年前 评论

tearDown中 Mockery::close() 呢?

3年前 评论
LiamHao (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!