1

我正在努力寻找正确的方法来对我的 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)));
    }
}

搜索服务

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 数据库测试存储库 ✓</li>
  • 可以通过简单的功能测试来测试该操作 ✓</li>
  • 剩下的是搜索服务中的逻辑。例如 findSamples 方法

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

$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);
    }
}

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

干杯

4

1 回答 1

0

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

取消验证后,您的问题剩下的就是要添加到存储库中的另一个 findX() 方法(存储库方法没有理由不能相互调用和构建),您已经知道如何测试。

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

为了将服务测试与持久性以外的依赖项隔离开来,我尝试过:

  1. 在配置中使用模拟版本覆盖服务类。问题 - 您不想对功能测试执行此操作,这意味着您必须拥有更新配置和/或更改配置以运行单个测试的测试脚本。优势 - 您可以通过翻转配置运行与独立单元测试和集成测试相同的测试
  2. (警告:讨厌的 hack!)提供一个 setter 方法来用来自测试程序的模拟版本替换注入的服务。
  3. (尚未尝试)直接实例化正在测试的服务,在构造中传递模拟依赖项。

关于与持久层隔离,对我来说唯一有意义的方法是将其从要测试的服务中抽象出来,放入不包含额外逻辑的包装服务中。然后可以使用上述方法之一来模拟包装服务(或者希望其他人会建议更好的解决方案?!)

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

  1. 任何服务都不应该依赖于会话对象。这不是好的做法,总是可以避免的。在最坏的情况下,示例方法可能返回混合值,如果结果不是数组,则假定为错误消息,尽管有更好的选择。
  2. 有时依赖关系是不必要的(代码更自然地属于其他地方)或过于笼统(我会质疑将高级对象如教义或容器注入测试助手以外的任何东西的必要性)。
  3. 如果要模拟一个复杂的依赖项(例如来自持久层的多个类),则将其抽象为一个包装器,该包装器比复杂的依赖项更容易模拟。
于 2013-05-10T13:32:31.927 回答