支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
你可能已经注意到,我们在前面章节构建的框架中,有一些微小但却重要的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 创作共用授权。