我正在努力寻找正确的方法来单元测试我的symfony 2服务,它使用doctrine或其他通用服务。
我已经做了什么:
- 在我的理解控制器动作应该:
- 尽量短
- 接受请求
- 从注入的服务执行所需的方法
- 根据 构建响应
- 本身就是一个服务
为了完成轻量级的操作,我尝试将逻辑封装到一个单独的服务中,该服务被注入到控制器中。
除了测试所有内容外,这工作得很好。
这里是我当前的代码:
控制器class SearchController
{
// search_helper, request and templating are controller-injected
protected $search_helper;
protected $request;
protected $templating;
// ...
public function searchAction()
{
$searchterm = strtolower($this->request->query->get('q'));
$result = $this->search_helper->findSamples($searchterm);
// Found a single result. Redirect to this page
if (is_string($result))
{
return new RedirectResponse($result, 301);
}
return new Response($this->templating->render('AlbiSampleBundle:Search:index.html.twig', array('results' => $result)));
}
}
SearchService
class SearchHelper
{
// doctrine, session and min_query_len are controller-injected
protected $doctrine;
protected $session;
protected $min_query_len;
// ...
public function findSamples($searchterm)
{
if (strlen($searchterm) < $this->min_query_len)
{
$msg = 'Your search must contain at least 3 characters!';
$this->session->getFlashBag()->add('error', $msg);
return false;
}
$em = $this->doctrine->getManager();
$results = $em->getRepository('AlbiSampleBundle:Sample')->findPossibleSamples($searchterm);
// Execute a more advanced search, if std. search don't delivers a result
// ...
return $results;
}
}
如何正确测试此代码?
- 使用phpunit_db和内存中的sqlite数据库测试存储库。
- 可通过简单的功能测试测试动作。✓ 剩下的就是搜索服务中的逻辑了。例如findSamples方法
我的第一个想法是模拟依赖关系(事实上,这是分离依赖关系的主要方面之一),但是您不仅要模拟原则对象,还要模拟实体管理器和存储库。
$em = $this->doctrine->getManager();
$results = $em->getRepository('AlbiSampleBundle:Sample')->findPossibleSamples($searchterm);
我想一定有更好的解决办法。这种嘲弄不仅需要许多loc,而且感觉也不对。测试将不必要地与SUT紧密耦合。
编辑
这是我想出的一个样本测试。使用模拟对象。测试不会成功的。我意识到这需要更多的模拟对象,我有一种感觉,这不是正确的方式。
测试失败,因为SessionMock->getFlashbag
没有返回具有add
方法的flashbag。doctrine->getManager
不返回EntityManager
。EntityManager
没有getRepository
方法。库中缺少findPossibleSamples
.
class SearchHelperTest extends PHPUnit_Framework_TestCase
{
private $router;
private $session;
private $doctrine;
public function setUp()
{
parent::setUp();
// ...
}
public function testSearchReturnValue()
{
$search_service = $this->createSearchHelper();
$this->assertFalse($search_service->findSamples('s'));
}
protected function createSearchHelper()
{
return new SearchHelper($this->doctrine, $this->router, $this->session, 3);
}
protected function getDoctrineMock()
{
return $this->getMock('DoctrineBundleDoctrineBundleRegistry', array('getManager'), array(), '', false);
}
protected function getSessionMock()
{
return $this->getMock('SymfonyComponentHttpFoundationSessionSession', array('getFlashBag'), array(), '', false);
}
protected function getRouterMock()
{
return $this->getMock('SymfonyComponentRoutingRouter', array('generate'), array(), '', false);
}
}
希望社区可以帮助我,编写良好的测试代码:)
欢呼
对于您的特定示例,我认为$searchterm的验证实际上不属于您的服务—至少服务不应该依赖于会话。有一些方法可以将会话移出服务并保留验证,但我个人会使用symfony验证,即为使用自身作为数据类的表单提供一个SampleSearchType,并在验证中挂起验证。(或酌情使用注释)。
一旦验证被取消,你的问题剩下的是另一个要添加到存储库的findX()方法(没有理由为什么存储库方法不能相互调用和构建),你已经知道如何测试。
话虽如此,我仍然同意Symfony存在一个普遍的问题,即如何在与注入服务隔离的情况下测试服务。关于与持久性层隔离的测试,到目前为止我一直避免尝试这样做。我的业务层服务与持久层耦合得如此紧密,以至于尝试独立测试它们的成本是不值得的(其中的逻辑主要包括进行相关的数据库更新或发送电子邮件,symfony为此提供了自己的解耦机制)。我不确定这是因为我做错了,还是因为我正在开发的应用程序在业务逻辑上很轻!
将服务测试与持久性以外的依赖项隔离,我尝试过:
- 重写配置中带有模拟版本的服务类。问题——你不希望在功能测试中这样做,这意味着你必须有测试脚本来更新配置和/或更改配置以运行单独的测试。优势-你可以运行相同的测试作为一个独立的单元测试和作为一个集成测试通过翻转配置
- (警告:讨厌的黑客!)提供一个setter方法,用测试程序的模拟版本替换注入的服务。
- (尚未尝试)直接实例化正在测试的服务,在构造中传递模拟依赖项。
关于从持久层隔离,对我来说唯一有意义的方法是将其从要测试的服务中抽象到不包含额外逻辑的包装器服务中。然后可以使用上述方法之一(或者希望是其他人建议的更好的解决方案?!)来模拟包装器服务
编辑:为了解决模拟依赖关系的复杂性问题——偶尔这可能是不可避免的,但通常这表明需要重新审视设计。这是TDD的优势之一——它强烈鼓励组件的简化设计和解耦:
- 没有服务需要依赖于会话对象。这不是一个好的做法,总是可以避免的。最坏的情况下,示例方法可能返回混合值,如果结果不是数组,则认为是错误消息,尽管有更好的替代方案。
- 有时依赖关系是不必要的(代码更自然地属于其他地方)或太一般(我质疑注入高级对象的必要性,如教条或例如容器到除测试助手之外的任何东西)。
- 如果有一个复杂的依赖要模拟(比如在持久化层的多个类上),那么将其抽象到一个包装器中,这比模拟复杂的依赖要简单得多。