使用Doctrine测试Symfony2服务的正确方法

时间:2013-05-10 12:25:36

标签: symfony dependency-injection phpunit

我正在努力寻找正确的方法来对使用教条或其他常见服务的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;
    }
}

如何正确测试此代码?

  • 使用phpunit_db和inmemory sqlite数据库✓
  • 测试存储库
  • 可以通过简单的功能测试来测试该动作✓
  • 剩下的是搜索服务中的逻辑。例如findSamples方法

我的第一个想法是模拟依赖项(实际上这是分离依赖项的主要方面之一),但您不仅要模拟doctrine对象,还要实体管理器和存储库。

$em = $this->doctrine->getManager();
$results = $em->getRepository('AlbiSampleBundle:Sample')->findPossibleSamples($searchterm);

我认为必须有更好的解决方案。这种嘲弄不仅需要很多LOC,而且感觉也不对。测试将不必要地与SUT紧密耦合。

修改

这是我想出的一个示例测试。使用模拟对象。 测试不起作用。我意识到需要更多的模拟对象,我觉得这不是正确的方法。

测试失败,因为SessionMock->getFlashbag没有使用add方法返回一个flashbag。 doctrine->getManager不返回EntityManagerEntityManager没有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);
    }
}

希望社区可以帮助我,编写经过良好测试的代码:)

欢呼声

1 个答案:

答案 0 :(得分:0)

对于您的具体示例,我认为$ searchterm的验证并不真正属于您的服务 - 至少服务不应该依赖于会话。有一些方法可以将会话移出服务并保留验证,但我个人会使用symfony验证,即对于使用自身作为数据类的表单有一个SampleSearchType,并在validation.yml中挂起验证(或酌情使用注释)。

一旦验证完成,你的问题还剩下另一个findX()方法被添加到存储库(没有理由为什么存储库方法不能相互调用和构建)你已经知道如何测试

话虽如此,我仍然同意Symfony存在如何与注入服务隔离测试服务的一般问题。关于与持久层隔离的测试,我到目前为止避免尝试这样做。我的业务层服务与持久层紧密耦合,试图独立测试它们的成本是不值得的(主要包括进行相关的数据库更新或发送电子邮件,其中symfony提供了自己的解耦机制)。我不确定这是因为我做错了还是因为我正在处理的应用程序对业务逻辑很轻松!

要将服务测试与持久性以外的依赖关系隔离,我尝试过:

  1. 在配置中使用模拟版本覆盖服务类。问题 - 您不希望为功能测试执行此操作,这意味着您必须拥有更新配置和/或更改配置以运行单个测试的测试脚本。优点 - 您可以通过翻转配置
  2. 运行与隔离单元测试相同的测试和集成测试
  3. (警告:讨厌的黑客!)提供了一个setter方法,用测试程序中的模拟版本替换注入的服务。
  4. (尚未尝试)直接实例化正在测试的服务,在构造中传递模拟依赖项。
  5. 关于从持久层中隔离,对我来说唯一有意义的方法是将它从要测试的服务中抽象出一个不包含其他逻辑的包装器服务。然后可以使用上述方法之一来模拟包装器服务(或者希望是其他人建议的更好的解决方案?!)

    编辑:解决模拟依赖关系的复杂性问题 - 偶尔这可能是不可避免的,但总的来说这表明设计需要重新审视。这是TDD的优势之一 - 它强烈鼓励简化设计和组件分离:

    1. 任何服务都不需要依赖于会话对象。这不是好的做法,总是可以避免。最糟糕的情况是,示例方法可以返回混合值,如果结果不是数组,则假定它是一个错误消息,尽管有更好的替代方法。
    2. 有时依赖是不必要的(代码更自然地属于其他地方)或太笼统(我会质疑将高级对象(例如doctrine)或例如容器注入除测试助手以外的任何东西的必要性。)
    3. 如果存在对mock的复杂依赖(例如来自持久层的多个类),则将其抽象为一个包装器,它比复杂的依赖项更容易模拟。