使用桩件 (Stub) 解决 Laravel 单元测试中的依赖

发表于:2017-8-11 08:27

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:daryl    来源:博客

  Laravel 中的依赖
  我们都知道,Laravel 使用了 IoC,各个模块之间也因此解耦了。而正是因为这一点,我们在 Laravel 中编写单元测试的时候,变得更加轻松了。
  举个栗子
  考虑以下场景。我们在开发中,可能会在控制器和模型之间加一个 Repository 来处理数据。那么我们的 Controller 就会依赖 Respository。利用 Laravel 的 IoC,我们可以定义一个 Service Provider 来集中将 Respository 注入到容器中。
  假设我们现在有这样一个 Repository,里面记录了商品的信息,我们想要在 Controller 中获取某件商品信息,然后执行一些业务逻辑。
  Class GoodRepository
  {
      public function getGoodById($goodId)
      {
          // TODO: Get good by its id.
      }
  }
  class GoodController extends Controller
  {
      public function show($id, GoodRepository $goodRepository)
      {
          // TODO: Do something with good info from that repository.
      }
  }
  // In route/api.php
  Route::get('/api/good/{id}', 'GoodController@show');
  // Create a RepositoriesServiceProvider in Provider/RepositoriesServiceProvider.php。
  // And inject the GoodRepository into Container.
  class RepositoriesServiceProvider extends ServiceProvider
  {
      public function boot()
      {
      
      }
      
      public function register()
      {
          $this->app->singleton(GoodRepository::class);
      }
  }
  好了,我们可以发现,GoodController 是依赖 GoodRepository 的,而 GoodRepository 是依赖数据库中的数据的。可是我们在做单元测试的时候,希望尽可能少的产生依赖。所以,我们应该希望能够掌控 GoodRepository 所返回的数据。
  在 Laravel,提供了 $this->get('/path/to/route'); 的方法来对 HTTP 请求进行测试。这个测试必然会涉及到刚才所提到的那些依赖,如何解决这个依赖的问题,我们可以请出我们的主角————桩件。
  桩件
  将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。
  这是 PHPUnit 文档上 的解释。那我的理解呢,所谓的桩件,就是模拟一个依赖的类的行为,使得这个行为所做的事情在我们自己的掌控之中。比如上面的这种情况,我们希望模拟 GoodRepository 的 getGoodById 方法返回与真正的返回结构相同的值,而不需要依赖外部数据源。
  在 Laravel 中使用桩件
  我们通过 Service Provider 注册了 GoodRepository 单例,那么按照这个思路,我们在写单元测试的时候,就可以将我们定义的桩件,注册为 GoodRepository 单例。
  class GoodControllerTest extends TestCase
  {
      public function testShow()
      {
          $data = []; // The data returns from GoodRepository::getGoodById.
          
          $stub = $this->createMock(GoodRepository::class);
          
          $stub->method('getGoodById')->will($this->returnValue($data));
          
          $this->app->singleton(GoodRepository::class, function () use ($stub) {
              return $stub;
          });
          
          $response = $this->get('/api/good/1');
          
          // Some assertions.
      }
  }
  我们通过在这里将桩件 $stub 用单例模式注册给了 Container,在调用 $this->get('/api/good/1'); 时原本在 Controller 中的 GoodRepository 依赖就变成了我们自定义的桩件 $stub。我们将 $data 定义为和返回值相同的结构,注册到桩件中。这样,所有的数据都在我们可控的范围了。
  如果我们在这里不使用桩件,而是直接依赖外部(数据库)中的数据,那么如果 id 为 1 的数据被删除了,我们是不是就要改成 2 了呢?我们是不是就要重新计算数据了匹配断言了呢?这样的测试,可靠性便大大降低。
  后记
  任何一个可靠的系统,单元测试都是必不可少的。庆幸的是,PHPUnit 帮我们提供了好用的单元测试。本文所讲的,也只是 PHPUnit 的九牛一毛。而我自己也在慢慢摸索慢慢实践中。与君共勉。
  最后还是推荐去听一下 @vimac 的讲堂 PHP单元测试与测试驱动开发,受益匪浅。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号