单元测试

3.3 version
维护中的版本

你可能已经注意到,我们在前面章节构建的框架中,有一些微小但却重要的bugs。当创建一个框架时,你必须确保其行为符合宣传。否则,基于它的所有程序都会展示相同的bugs。好的一面是,只要你修复一个bug,你同时也能修复大量程序。

今天的任务,是通过使用PHPUnit,来为我们的框架写一个单元测试(unit tests)。创建一个PHPUnit的配置文件到example.com/phpunit.xml.dist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.1/phpunit.xsd"
    backupGlobals="false"
    colors="true"
    bootstrap="vendor/autoload.php"
>
    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
 
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./src</directory>
        </whitelist>
    </filter>
</phpunit>

这个配置文件对一些PHPunit选项进行了必要的默认定义;有趣的是,自动加载器被用于启动测试,而测试文件将被存放在example.com/tests/目录下。

现在,我们为“not found”资源写一个测试。为了避免在编写测试代码时引入全部依赖,同时真正做到“仅测试想要的部分”,我们得使用test Doubles。Test Doubles在我们依赖接口而非具体类时更易被创建。幸运的是,Symfony为核心对象比如UrlMatcher和控制器解析器(controller resolver)等,提供了这样的接口。修改我们的框架文件以利用之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// example.com/src/Simplex/Framework.php
namespace Simplex;
 
// ...
 
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
 
class Framework
{
    protected $matcher;
    protected $resolver;
    protected $argumentResolver;
 
    public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver, ArgumentResolverInterface $argumentResolver)
    {
        $this->matcher = $matcher;
        $this->resolver = $resolver;
        $this->argumentResolver = $argumentResolver;
    }
 
    // ...
}

现在我们已经准备好编写第一个测试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// example.com/tests/Simplex/Tests/FrameworkTest.php
namespace Simplex\Tests;
 
use Simplex\Framework;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 
class FrameworkTest extends \PHPUnit_Framework_TestCase
{
    public function testNotFoundHandling()
    {
        $framework = $this->getFrameworkForException(new ResourceNotFoundException());
 
        $response = $framework->handle(new Request());
 
        $this->assertEquals(404, $response->getStatusCode());
    }
 
    private function getFrameworkForException($exception)
    {
        $matcher = $this->createMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface');
        $matcher
            ->expects($this->once())
            ->method('match')
            ->will($this->throwException($exception))
        ;
        $matcher
            ->expects($this->once())
            ->method('getContext')
            ->will($this->returnValue($this->createMock('Symfony\Component\Routing\RequestContext')))
        ;
        $controllerResolver = $this->createMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface');
        $argumentResolver = $this->createMock('Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface');
 
        return new Framework($matcher, $controllerResolver, $argumentResolver);
    }
}

这个测试模拟了一个请求,它并不匹配任何路由。因此,match()方法返回了ResourceNotFoundException异常,我们要测试的就是框架把这个异常给转换为一个404响应。

执行这个测试很简单,只要在example.com根目录下运行phpunit命令就行了:

1
$  phpunit

如果你还不能理解上面的测试代码“到底是什么”,参考PHPUnit文档的test doubles部分。

运行完测试,你应该看到一个绿条子。如果没有,你可能遇到bug,它存在于你的测试代码或是框架代码中!

为控制器中抛出的任何异常而添加一个单元测试是极其简单的:

1
2
3
4
5
6
7
8
public function testErrorHandling()
{
    $framework = $this->getFrameworkForException(new \RuntimeException());
 
    $response = $framework->handle(new Request());
 
    $this->assertEquals(500, $response->getStatusCode());
}

最后,我们再为“正确的响应”来编写一个测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
 
public function testControllerResponse()
{
    $matcher = $this->createMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface');
    $matcher
        ->expects($this->once())
        ->method('match')
        ->will($this->returnValue(array(
            '_route' => 'foo',
            'name' => 'Fabien',
            '_controller' => function ($name) {
                return new Response('Hello '.$name);
            }
        )))
    ;
    $matcher
        ->expects($this->once())
        ->method('getContext')
        ->will($this->returnValue($this->createMock('Symfony\Component\Routing\RequestContext')))
    ;
    $controllerResolver = new ControllerResolver();
    $argumentResolver = new ArgumentResolver();
 
    $framework = new Framework($matcher, $controllerResolver, $argumentResolver);
 
    $response = $framework->handle(new Request());
 
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertContains('Hello Fabien', $response->getContent());
}

在这个测试中,我们模拟了一个“匹配且返回了一个简易控制器”的路由。我们检查响应的状态码是200,而响应内容则是我们在控制器中设置好的。

要检查我们是否覆盖了所有可能的使用场景,运行PHPUnit的“test coverage”功能(你需要首先开启XDebug)功能:

1
$  phpunit --coverage-html=cov/

在浏览器中打开example.com/cov/src/Simplex/Framework.php.html,检查Framework类的所有行都是绿色(这意味着它们在测试执行时被访问到了)。

另一种方式,你可以直接在命令行中输出(覆盖率)结果:

1
$  phpunit --coverage-text

幸亏到目前为止我们写就的这些简单的面向对象代码,我们已经能够为框架编写单元测试来覆盖所有可能的使用场景;test doubles确保了我们所测试的真的是自己的代码而不是Symfony代码。

现在我们对已有代码充满自信(又一次),我们可以放心地思考“向框架内添加下一波功能”了。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)