我正在努力寻找正确的方法来对使用教条或其他常见服务的symfony 2服务进行单元测试。
到目前为止我做了什么:
为了实现轻量级操作,我尝试将逻辑封装到一个单独的服务中,该服务被注入控制器。
这很适合测试所有内容。
这是我目前的代码:
控制器
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;
}
}
我的第一个想法是模拟依赖项(实际上这是分离依赖项的主要方面之一),但您不仅要模拟doctrine对象,还要实体管理器和存储库。
$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('Doctrine\Bundle\DoctrineBundle\Registry', array('getManager'), array(), '', false);
}
protected function getSessionMock()
{
return $this->getMock('Symfony\Component\HttpFoundation\Session\Session', array('getFlashBag'), array(), '', false);
}
protected function getRouterMock()
{
return $this->getMock('Symfony\Component\Routing\Router', array('generate'), array(), '', false);
}
}
希望社区可以帮助我,编写经过良好测试的代码:)
欢呼声
答案 0 :(得分:0)
对于您的具体示例,我认为$ searchterm的验证并不真正属于您的服务 - 至少服务不应该依赖于会话。有一些方法可以将会话移出服务并保留验证,但我个人会使用symfony验证,即对于使用自身作为数据类的表单有一个SampleSearchType,并在validation.yml中挂起验证(或酌情使用注释)。
一旦验证完成,你的问题还剩下另一个findX()方法被添加到存储库(没有理由为什么存储库方法不能相互调用和构建)你已经知道如何测试
话虽如此,我仍然同意Symfony存在如何与注入服务隔离测试服务的一般问题。关于与持久层隔离的测试,我到目前为止避免尝试这样做。我的业务层服务与持久层紧密耦合,试图独立测试它们的成本是不值得的(主要包括进行相关的数据库更新或发送电子邮件,其中symfony提供了自己的解耦机制)。我不确定这是因为我做错了还是因为我正在处理的应用程序对业务逻辑很轻松!
要将服务测试与持久性以外的依赖关系隔离,我尝试过:
关于从持久层中隔离,对我来说唯一有意义的方法是将它从要测试的服务中抽象出一个不包含其他逻辑的包装器服务。然后可以使用上述方法之一来模拟包装器服务(或者希望是其他人建议的更好的解决方案?!)
编辑:解决模拟依赖关系的复杂性问题 - 偶尔这可能是不可避免的,但总的来说这表明设计需要重新审视。这是TDD的优势之一 - 它强烈鼓励简化设计和组件分离: