Laravel 4 - 使用 PHPUnit 和 Mockery 进行存储库模式测试
Laravel 4 - Repository Pattern Testing with PHPUnit and Mockery
我正在为客户开发应用程序,但在测试存储库时遇到问题。
要将存储库绑定到模型,我有以下代码:
<?php
namespace FD\Repo;
use App;
use Config;
/**
* Service Provider for Repository
*/
class RepoServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register()
{
$app = $this->app;
$app->bind('FD\Repo\FactureSst\FactureSstInterface', function ($app) {
return new FactureSst\EloquentFactureSst(App::make('FactureSst'), new \FD\Service\Cache\LaravelCache($app['cache'], 'factures_sst', 10));
});
}
}
存储库然后扩展了一个抽象 class,其中包含来自 eloquent ORM 的函数(查找、位置、所有等)。存储库的代码如下所示:
<?php
namespace FD\Repo\FactureSst;
use Illuminate\Database\Eloquent\Model;
use FD\Repo\AbstractBaseRepo;
use FD\Repo\BaseRepositoryInterface;
use FD\Service\Cache\CacheInterface;
use Illuminate\Support\Collection;
class EloquentFactureSst extends AbstractBaseRepo implements BaseRepositoryInterface, FactureSstInterface
{
protected $model;
protected $cache;
public function __construct(Model $resource, CacheInterface $cache)
{
$this->model = $resource;
$this->cache = $cache;
}
/**
* Retrieve factures with the given SST and BDC IDs.
*
* @param int $sst_id
* @param int $bdc_id
* @return \Illuminate\Support\Collection
*/
public function findWithSstAndBdc($sst_id, $bdc_id)
{
$return = new Collection;
$factures = $this->model->where('id_sst', $sst_id)
->whereHas('facture_assoc', function ($query) use ($bdc_id) {
$query->where('id_bdc', $bdc_id);
})
->get();
$factures->each(function ($facture) use (&$return) {
$data = [
'facture_id' => $facture->id,
'facture_name' => $facture->num_facture,
'total_dsp' => $facture->total_dsp(),
'total_tradi' => $facture->total_tradi()
];
$return->push($data);
});
return $return;
}
}
为了测试对数据库的调用,我正在使用 Mockery,因为对数据库的调用会太长。这是我的测试 class:
<?php namespace App\Tests\Unit\Api\FactureSst;
use App;
use FactureSst;
use Illuminate\Database\Eloquent\Collection;
use Mockery as m;
use App\Tests\FdTestCase;
class FactureSstTest extends FdTestCase
{
/**
* The primary repository to test.
*/
protected $repo;
/**
* Mocked version of the primary repo.
*/
protected $mock;
public function setUp()
{
parent::setUp();
$this->repo = App::make('FD\Repo\FactureSst\FactureSstInterface');
$this->mock = $this->mock('FD\Repo\FactureSst\FactureSstInterface');
}
public function tearDown()
{
parent::tearDown();
m::close();
}
public function mock($class)
{
$mock = m::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
public function testFindingBySstAndBdc()
{
$this->mock->shouldReceive('where')->with('id_sst', 10)->once()->andReturn($this->mock);
$this->mock->shouldReceive('whereHas')->with('facture_assoc')->once()->andReturn($this->mock);
$this->mock->shouldReceive('get');
$result = $this->repo->findWithSstAndBdc(30207, 10);
$this->assertEquals($result, new \Illuminate\Support\Collection);
$this->assertEquals($result->count(), 0);
}
}
如您在测试中所见,我只是尝试调用该函数并确保函数正确链接。但是我不断收到一条错误消息:
App\Tests\Unit\Api\FactureSst\FactureSstTest::testFindingBySstAndBdc
Mockery\Exception\InvalidCountException: Method where("id_sst", 10) from Mockery_0_FD_Repo_FactureSst_FactureSstInterface should be called
exactly 1 times but called 0 times.
请有人帮助我理解为什么这不起作用以及如何解决它。对不起,代码是法语的,该应用程序是针对法国客户的。
提前致谢。
如果我没记错这行
$result = $this->repo->findWithSstAndBdc(30207, 10);
其实应该是
$result = $this->mock->findWithSstAndBdc(30207, 10);
另外请记住,您可以模拟整个调用链(对于像这样的流畅查询很有用):
$this->mock->shouldReceive('where->whereHas->get')
->once()->andReturn(/*MAKE A FAKE OBJECT HERE*/);
我还会使用 PHP 的内置 foreach() 而不是使用 $factures->each,因为这样可以少测试一件事。
当您真正应该模拟该存储库的依赖项时,您似乎在模拟该存储库本身,即 Illuminate\Database\Eloquent\Model
您将要访问数据库的地方。
更改 setUp()
以便它创建 Illuminate\Database\Eloquent\Model
的模拟对象,然后让它在实例化您的存储库时注入该模拟对象。
public function setUp()
{
parent::setUp();
$this->mock = m::mock('Illuminate\Database\Eloquent\Model'); // Or better if you mock 'FactureSst'
$this->app->instance('FactureSst', $this->mock);
}
这有点令人困惑,因为您声明抽象 class 包含 ORM 方法,但是当您调用这些方法时,您是在注入的 Model
依赖项上调用它们,而不是在抽象上class。这可能就是混淆所在。
此外,如果您的模型扩展 Illuminate\Database\Eloquent\Model
,通常最好只将您的模型注入存储库而不是 Illuminate\Database\Eloquent\Model
。这样,您还可以利用您在存储库中的模型中设置的任何关系函数。
我正在为客户开发应用程序,但在测试存储库时遇到问题。
要将存储库绑定到模型,我有以下代码:
<?php
namespace FD\Repo;
use App;
use Config;
/**
* Service Provider for Repository
*/
class RepoServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register()
{
$app = $this->app;
$app->bind('FD\Repo\FactureSst\FactureSstInterface', function ($app) {
return new FactureSst\EloquentFactureSst(App::make('FactureSst'), new \FD\Service\Cache\LaravelCache($app['cache'], 'factures_sst', 10));
});
}
}
存储库然后扩展了一个抽象 class,其中包含来自 eloquent ORM 的函数(查找、位置、所有等)。存储库的代码如下所示:
<?php
namespace FD\Repo\FactureSst;
use Illuminate\Database\Eloquent\Model;
use FD\Repo\AbstractBaseRepo;
use FD\Repo\BaseRepositoryInterface;
use FD\Service\Cache\CacheInterface;
use Illuminate\Support\Collection;
class EloquentFactureSst extends AbstractBaseRepo implements BaseRepositoryInterface, FactureSstInterface
{
protected $model;
protected $cache;
public function __construct(Model $resource, CacheInterface $cache)
{
$this->model = $resource;
$this->cache = $cache;
}
/**
* Retrieve factures with the given SST and BDC IDs.
*
* @param int $sst_id
* @param int $bdc_id
* @return \Illuminate\Support\Collection
*/
public function findWithSstAndBdc($sst_id, $bdc_id)
{
$return = new Collection;
$factures = $this->model->where('id_sst', $sst_id)
->whereHas('facture_assoc', function ($query) use ($bdc_id) {
$query->where('id_bdc', $bdc_id);
})
->get();
$factures->each(function ($facture) use (&$return) {
$data = [
'facture_id' => $facture->id,
'facture_name' => $facture->num_facture,
'total_dsp' => $facture->total_dsp(),
'total_tradi' => $facture->total_tradi()
];
$return->push($data);
});
return $return;
}
}
为了测试对数据库的调用,我正在使用 Mockery,因为对数据库的调用会太长。这是我的测试 class:
<?php namespace App\Tests\Unit\Api\FactureSst;
use App;
use FactureSst;
use Illuminate\Database\Eloquent\Collection;
use Mockery as m;
use App\Tests\FdTestCase;
class FactureSstTest extends FdTestCase
{
/**
* The primary repository to test.
*/
protected $repo;
/**
* Mocked version of the primary repo.
*/
protected $mock;
public function setUp()
{
parent::setUp();
$this->repo = App::make('FD\Repo\FactureSst\FactureSstInterface');
$this->mock = $this->mock('FD\Repo\FactureSst\FactureSstInterface');
}
public function tearDown()
{
parent::tearDown();
m::close();
}
public function mock($class)
{
$mock = m::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
public function testFindingBySstAndBdc()
{
$this->mock->shouldReceive('where')->with('id_sst', 10)->once()->andReturn($this->mock);
$this->mock->shouldReceive('whereHas')->with('facture_assoc')->once()->andReturn($this->mock);
$this->mock->shouldReceive('get');
$result = $this->repo->findWithSstAndBdc(30207, 10);
$this->assertEquals($result, new \Illuminate\Support\Collection);
$this->assertEquals($result->count(), 0);
}
}
如您在测试中所见,我只是尝试调用该函数并确保函数正确链接。但是我不断收到一条错误消息:
App\Tests\Unit\Api\FactureSst\FactureSstTest::testFindingBySstAndBdc Mockery\Exception\InvalidCountException: Method where("id_sst", 10) from Mockery_0_FD_Repo_FactureSst_FactureSstInterface should be called exactly 1 times but called 0 times.
请有人帮助我理解为什么这不起作用以及如何解决它。对不起,代码是法语的,该应用程序是针对法国客户的。
提前致谢。
如果我没记错这行
$result = $this->repo->findWithSstAndBdc(30207, 10);
其实应该是
$result = $this->mock->findWithSstAndBdc(30207, 10);
另外请记住,您可以模拟整个调用链(对于像这样的流畅查询很有用):
$this->mock->shouldReceive('where->whereHas->get')
->once()->andReturn(/*MAKE A FAKE OBJECT HERE*/);
我还会使用 PHP 的内置 foreach() 而不是使用 $factures->each,因为这样可以少测试一件事。
当您真正应该模拟该存储库的依赖项时,您似乎在模拟该存储库本身,即 Illuminate\Database\Eloquent\Model
您将要访问数据库的地方。
更改 setUp()
以便它创建 Illuminate\Database\Eloquent\Model
的模拟对象,然后让它在实例化您的存储库时注入该模拟对象。
public function setUp()
{
parent::setUp();
$this->mock = m::mock('Illuminate\Database\Eloquent\Model'); // Or better if you mock 'FactureSst'
$this->app->instance('FactureSst', $this->mock);
}
这有点令人困惑,因为您声明抽象 class 包含 ORM 方法,但是当您调用这些方法时,您是在注入的 Model
依赖项上调用它们,而不是在抽象上class。这可能就是混淆所在。
此外,如果您的模型扩展 Illuminate\Database\Eloquent\Model
,通常最好只将您的模型注入存储库而不是 Illuminate\Database\Eloquent\Model
。这样,您还可以利用您在存储库中的模型中设置的任何关系函数。