[原创中文翻译]symfony askeet24:第十五天,单元测试。

October 8, 2007 – 12:39 am

[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]

Simple test

简单测试

There are many unit test frameworks in the PHP world, mostly based on Junit. We didn’t develop another one for symfony, but instead we integrated the most mature of them all, Simple Test. It is stable, well documented, and offers tons of features that are of considerable value for all PHP projects, including symfony ones. If you don’t know it already, you are strongly advised to browse their documentation, which is very clear and progressive.

PHP世界里有许多单元测试框架,几乎都是基于Junit的。但是我们在symfony里都不用,我们可以总结这些框架的基本特点,简单测试。很稳定,全面的文档,提供了非常强大的功能,适合所有的PHP项目,包括symfony。如果你还是不清楚,强烈建议你看看这些框架的手册,文档很清楚,写得非常好。

[译者:这部分大家可以略过,目前中国还没有哪家软件企业真正能用上这个技术。或许这就是中国软件业和外国软件业的差距,是中国人笨蛋么?不是;是测试工程师笨蛋么?也不是;是老总笨蛋么?更不是。那是什么呢?呵呵,原因有很多,不过有一点很类似,中国大部分企业的开发模式很像抗美援朝战争前的中国军队,野路子做事,骄傲自大,自以为是。或许只有经历了惨痛的教训后,才会出现刘伯承元帅那样英明人物,开设南京钟山军事学院把那些大老粗指挥官全部训练成具有高素质的军事人才。中国的软件企业早晚会走这一步的… …]

Simple Test is not bundled with symfony, but very simple to install. First, download the Simple Test PEAR installable archive at SourceForge. Install it via pear by calling:

简单测试没有包含在symfony发行包里,但安装起来很简单。首先,用PEAR下载安装:

$ pear install simpletest_1.0.0.tgz

If you want to write a batch script that uses the Simple Test library, all you have to do is insert these few lines of code on top of the script:

如果想写个批处理脚本来使用简单测试库,你所需要做的是把这几行代码插到脚本顶部:

<?php

require_once(’simpletest/unit_tester.php’);
require_once(’simpletest/reporter.php’);

?>

Symfony does it for you if you use the test command line; we will talk about it shortly.

symfony允许使用命令行来进行测试;我们简单谈一下。

Due to non backward-compatible changes in PHP 5.0.5, Simple Test is currently not working if you have a PHP version higher than 5.0.4. This should change shortly (an alpha version addressing this problem is available), but unfortunately the rest of this tutorial will probably not work if you have a later version.

由于PHP 5.0.5不兼容后续版本,如果你使用高于5.0.4版本的PHP,那么简单测试就无法工作了。这该有些变化(有个alpha版本解决了这个问题,我们可以利用一下)。如果你使用了新发布的PHP版本,那么不幸的是本教程剩下的部分你无法使用了。

Unit tests in a symfony project

symfony里的单元测试

Default unit tests

默认的单元测试

Each symfony project has a test/ directory, divided into application subdirectories. For askeet, if you browse to the askeet/test/frontend/ directory, you will see that a few files already exist there:

每个symfony项目都有test/目录,深入到程序子目录。对askeet来说,如果你展开askeet/test/frontend/,你会看到已经存在的几个文件:

answerActionsTest.php
feedActionsTest.php
mailActionsTest.php
sidebarActionsTest.php
userActionsTest.php

They all contain the same initial code:

他们都包括同样的代码:

<?php

class answerActionsWebBrowserTest extends UnitTestCase
{
private
$browser = null;

public function setUp ()
{
// create a new test browser
$this->browser = new sfTestBrowser();
$this->browser->initialize(’hostname’);
}

public function tearDown ()
{
$this->browser->shutdown();
}

public function test_simple()
{
$url = ‘/answer/index’;
$html = $this->browser->get($url);
$this->assertWantedPattern(’/answer/’, $html);
}
}

?>

The UnitTestCase class is the core class of the Simple Test unit tests. The setUp() method is run just before each test method, and tearDown() is run just after each test method. The actual test methods start with the word ‘test’. To check if a piece of code is behaving as you expect, you use an assertion, which is a method call that verifies that something is true. In Simple Test, assertions start by assert. In this example, one unit test is implemented, and it looks for the word ‘user’ in the default page of the module. This autogenerated file is a stub for you to start.

UnitTestCase类是简单单元测试的核心部分。setUp()方法在每次测试前运行,tearDown()在每次测试后运行。真实的测试方法开始于文字“test”。检查一下是否每个代码都按照预期设计运行,使用assertion方法验证一下什么是对的。例子里,assertions从assert开始。在这个例子里,单元测试程序被执行,程序在默认模块页面里寻找“user”。自动生成的文件是你开始的基础。

As a matter of fact, every time you call a symfony init-module, symfony creates a skeleton like this one in the test/[appname]/ directory to store the unit tests related to the created module. The trouble is that as soon as you modify the default template, the stub tests don’t pass anymore (they check the default title of the page, which is ‘module $modulename’). So for now, we will erase these files and work on our own test cases.

事实上,每次你使用symfony init-module命令时,symfony创建一个骨架,就像是在test/[程序名]/目录,此目录储存相关模块的单元测试程序。麻烦是你一修改默认模板,基础测试就不会传递给任何程序了(程序检查默认页的title,就是“模块$modulename”)。所以现在,我们清除掉这些文件来开始我们自己的测试程序。

Add a unit test

增加单元测试程序

During day 13, we created a Tag.class.php file with two functions dedicated to tag manipulation. We will add a few unit tests for our Tag library.

在第十三天教材里,我们使用了两个函数创建了Tag.class.php文件,实现了标签。我们给标签库加几个单元测试程序。

Create a TagTest.php file (all the test case files must end with Test for Simple Test to find them):

创建TagTest.php文件(所有的文件必须以Test结束,以便于简单测试按照规则能找到):

<?php

require_once(’Tag.class.php’);

class TagTest extends UnitTestCase
{
public function test_normalize()
{
$tests = array(
‘FOO’ => ‘foo’,
‘ foo’ => ‘foo’,
‘foo ‘ => ‘foo’,
‘ foo ‘ => ‘foo’,
‘foo-bar’ => ‘foobar’,
);

foreach ($tests as $tag => $normalized_tag)
{
$this->assertEqual($normalized_tag, Tag::normalize($tag));
}
}
}

?>

The first test case that we will implement concerns the Tag::normalize() method. Unit tests are supposed to test one case at a time, so we decompose the expected result of the text method into elementary cases. We know that the Tag::normalize() method is supposed to return a lower-case version of its argument, without any spaces - either before or after the argument - and without any special character. The five test cases defined in the $test array are enough to test that.

第一个测试用例我们执行Tag::normalize()函数。单元测试一次执行一个用例,所以我们分解预期结果到测试函数里。我们知道Tag::normalize()函数被希望返回lower-case版本的参数,没有空格——参数前后都是——没有特殊字符。5个测试用例定义在$test数组里,足够测试的了。

For each of the elementary test cases, we then compare the normalized version of the input with the expected result, with a call to the ->assertEqual() method. This is the heart of a unit test. If it fails, the name of the test case will be output when the test suite is run. If it passes, it will simply add to the number of passed tests.

对每一个基本测试用例来说,我们比较规格化的输入和预期结果,靠->assertEqual()函数。这是单元测试的核心。如果失败了,测试用例的名字会输出。如果通过了,通过测试数量就加1。

We could add a last test with the word ‘ FOo-bar ‘, but it mixes elementary cases. If this test fails, you won’t have a clear idea of the precise cause of the problem, and you will need to investigate further. Keeping to elementary cases gives you the insurance that the error will be located easily.

我们在“FOo-bar”后加一个测试,但它混合了基本测试。如果未通过,你不会得到精确结果,你需要在后面调查。专注于基本测试给你加了保险,错误将会被很容易发现。

The extensive list of the assert methods can be found in the Simple Test documentation.

assert函数列表可以在简单测试手册里发现。

Running unit tests

运行单元测试

The symfony command line allows you to run all the tests at once with a single command (remember to call it from your project root directory):

symfony命令行允许你用单独命令来运行所有测试(在站点根目录使用):

$ symfony test frontend

Calling this command executes all the tests of the test/frontend/ directory, and for now it is only the ones of our new TagTest.php set. These tests will pass and the command line will show:

命令执行test/frontend/下所有的测试程序,现在仅是我们新TagTest.php设置中的几个。测试会通过而且命令行会有提示:

$ symfony test frontend
Test suite in (test/frontend)
OK
Test cases run: 1/1, Passes: 5, Failures: 0, Exceptions: 0

Tests launched by the symfony command line don’t need to include the Simple Test library (unit_tester.php and reporter.php are included automatically).

symfony命令行载入测试不需要包含简单测试库(unit_tester.php和reporter.php自动包含了)。

The other way around

其他的方法

The greatest benefit of unit tests is experienced when doing test-driven development. In this methodology, the tests are written before the function is written.

单元测试最大的好处是在做测试驱动开发时很方便。在开发中,测试在函数写以前先写好。

With the example above, you would write an empty Tag::normalize() method, then write the first test case (’Foo’/'foo’), then run the test suite. The test would fail. You would then add the necessary code to transform the argument into lowercase and return it in the Tag::normalize() method, then run the test again. The test would pass this time.

根据以上例子,你需要写个空Tag::normalize()函数,然后写第一个测试用例(’Foo’/'foo’),后面运行测试程序。测试失败了。你需要填加必须的代码来转换参数为小写字母并返回Tag::normalize()函数,重新运行。这次就通过了。

So you would add the tests for blanks, run them, see that they fail, add the code to remove the blanks, run the tests again, see that they pass. Then do the same for the special characters.

测试空格,运行,看到失败了,加代码去掉空格,再次运行,好了。测试特殊字符也一样。

Writing tests first helps you to focus on the things that a function should do before actually developing it. It’s a good practice that others methodologies, like eXtreme Programming, recommend as well. Plus it takes into account the undeniable fact that if you don’t write unit tests first, you never write them.

写单元测试程序帮助你专注于函数在真正被写以前,你需要明白它到底是干什么的。这是个好习惯,有点像极限编程,也是推荐的。另外不可否认的是如果你没有写单元测试程序,那么就别进行开发。

One last recommendation: keep your unit tests as simple as the ones described here. An application built with a test driven methodology ends up with roughly as much test code as actual code, so you don’t want to spend time debugging your tests cases…

最后的建议:保持你的单元测试程序像这里描述的那样简单。写程序就够烦人的了,别浪费时间去调试你的测试程序… …

When a test fails

当测试程序失败后

We will now add the tests to check the second method of the Tag object, which splits a string made of several tags into an array of tags. Add the following method to the TagTest class:

我们现在进行Tag对象中第二个函数的测试,函数功能是分离一个由几个标签组成的数组为几个标签数组。给TagTest类加几个函数:

public function test_splitPhrase()
{
$tests = array(
'foo' => array('foo'),
'foo bar' => array('foo', 'bar'),
' foo bar ' => array('foo', 'bar'),
'"foo bar" askeet' => array('foo bar', 'askeet'),
"'foo bar' askeet" => array('foo bar', 'askeet'),
);

foreach ($tests as $tag => $tags)
{
$this->assertEqual($tags, Tag::splitPhrase($tag));
}
}

As a good practice, we recommend to name the test files out of the class they are supposed to test, and the test cases out of the methods they are supposed to test. Your test/ directory will soon contain a lot of files, and finding a test might prove difficult in the long run if you don’t.

好方法,我们建议不要把测试文件命名为想测试的类,而且测试程序要从被测试的函数中分离出来。你的test/目录会很快包含很多文件,你肯定不想运行老半天完不了。

If you try to run the tests again, they fail:

如果再次运行,那么失败了:

$ symfony test frontend
Test suite in (test/frontend)
1) Equal expectation fails as key list [0, 1] does not match key list [0, 1, 2] at line [35]
in test_splitPhrase
in TagTest
in /home/production/askeet/test/frontend/TagTest.php
FAILURES!!!
Test cases run: 1/1, Passes: 9, Failures: 1, Exceptions: 0

All right, one of the test cases of test_splitPhrase fails. To find which one it is, you will need to remove them one at at time to see when the test passes. This time, it’s the last one, when we test the handling of simple quotes. The current Tag::splitPhrase() method doesn’t translate this string properly. As part of your homework, you will have to correct it for tomorrow.

当然,test_splitPhrase中的一个测试用例失败了。想找到哪一个是,你需要一次删除一个,看看啥时候能通过了。那么这次,最后一个,当我们测试简单引号处理时。当前Tag::splitPhrase()函数没有恰当得翻译字符串。你今天要做的部分是,你明天需要修正一下。

This illustrates the fact that if you pile up too much elementary test cases in an array, a failure is harder to locate. Always prefer to split long test cases into methods, since Simple Test mentions the name of the method where a test failed.

事实上如果你堆积太多的基本测试用例在一个数组里,错误就难以找出了。总是喜欢把长测试用例分离到函数里,因为简单测试能列出失败的函数名。

Simulating a web browsing session

模拟web浏览器seesion

Web applications are not all about objects that behave more or less like functions. The complex mechanisms of page request, HTML result and browser interactions require more than what’s been exposed before to build a complete set of unit tests for a symfony web app.

web程序并不全是和对象有关的,行为或多或少很像函数。综合的页面原理调用,HTML需求和浏览器交互需求,更多的内容,在给symfony web程序创建一套全面的单元测试程序前你该知道

We will examine three different ways to implement a simple web app test. The test has to do a request to the first question detail, and assume that some text of the answer is present. We will put this test into a QuestionTest.php file, located in the askeet/test/frontend/ directory.

我们需要检查三个不同的方法来执行一个简单的web app测试。测试需要给第一个question细节发送一个请求,假定answer的内容显示了。我们会把测试放入QuestionTest.php文件,在askeet/test/frontend/下。

The sfTestBrowser object

sfTestBrowser对象

Symfony provides an object called sfTestBrowser, which allows to simulate browsing without a browser and, more important, without a web server. Being inside the framework allows this object to bypass completely the http transport layer. This means that the browsing simulated by the sfTestBrowser is fast, and independent of the server configuration, since it does not use it.

symfony提供了叫做sfTestBrowser的对象,可以模拟浏览器,不需要真的浏览器,不需要web服务器。框架允许对象完全绕过http传输层。这意味着用sfTestBrowser模拟速度会很快,独立于服务器配置,因为用不着啊。

Let’s see how to do a request for a page with this object:

那来看看这是怎么工作的:

$browser = new sfTestBrowser();
$browser->initialize();
$html = $browser->get('uri');

// do some test on $html

$browser->shutdown();

The get() request takes a routed URI as a parameter (not an internal URI), and returns a raw HTML page (a string). You can then proceed to all kinds of tests on this page, using the assert*() methods of the UnitTestCase object.

get()把路径URL当作参数(不是上网的那个URL),返回raw HTML页(一个字符串)。你可以在此页继续进行所有的测试,使用UnitTestCase对象的assert*()函数。

You can pass parameters to your call as you would in the URL bar of your browser:

你可以像在浏览器地址栏里输入URL那样传递参数:

$html = $browser->get('/frontend_test.php/question/what-can-i-offer-to-my-stepmother');

The reason why we use a specific front controller (frontend_test.php) will be explained in the next section.

为什么我们用具体的前台控制(frontend_test.php),原因我们在下节解释。

The sfTestBrowser simulates a cookie. This means that with a single sfTestBrowser object, you can require several pages one after the other, and they will be considered as part of a single session by the framework. In addition, the fact that sfTestBrowser uses routed URIs instead of internal URIs allows you to test the routing engine.

sfTestBrowser模拟cookie。这意味着用单独的sfTestBrowser对象,你可以一个接个得请求几个页面,框架会把他们当成单独的session。另外,sfTestBrowser使用了路由URL代替互联网URL,可以测试路由协议。

To implement our web test, the test_QuestionShow() method must be built as follows:

执行web测试,test_QuestionShow()函数必须这么搭建:

<?php

class QuestionTest extends UnitTestCase
{
public function test_QuestionShow()
{
$browser = new sfTestBrowser();
$browser->initialize();
$html = $browser->get(’frontend_test.php/question/what-can-i-offer-to-my-step-mother’);
$this->assertWantedPattern(’/My stepmother has everything a stepmother is usually offered/’, $html);
$browser->shutdown();
}
}

Since almost all the web unit tests will need a new sfTestBrowser to be initialized and closed after the test, you’d better move part of the code to the ->setUp() and ->tearDown() methods:

因为几乎所有单元测试程序需要新sfTestBrowser初始化和在测试后关闭,你最好调整->setUp()和->tearDown()函数的部分代码:

<?php

class QuestionTest extends UnitTestCase
{
private $browser = null;

public function setUp()
{
$this->browser = new sfTestBrowser();
$this->browser->initialize();
}

public function tearDown()
{
$this->browser->shutdown();
}

public function test_QuestionShow()
{
$html = $this->browser->get(’frontend_test.php/question/what-can-i-offer-to-my-step-mother’);
$this->assertWantedPattern(’/My stepmother has everything a stepmother is usually offered/’, $html);
}
}

Now, every new test method that you add will have a clean sfTestBrowser object to start with. You may recognize here the auto-generated test cases mentioned at the beginning of this tutorial.

现在,每一个你加的新测试函数都将开始于整洁的sfTestBrowser对象。你可以确认一下教材开始提到的自动生成的测试类。

The WebTestCase object

WebTestCase对象

Simple Test ships with a WebTestCase class, which includes facilities for navigation, content and cookie checks, and form handling. Tests extending this class allow you to simulate a browsing session with a http transport layer. Once again, the Simple Test documentation explains in detail how to use this class.

简单测试用WebTestCase类运行,可以进行导航,内容和cookie检查,还有表单处理。类扩展的测试允许你用http模拟浏览器session。再一次,简单测试手册解释了如何使用class。

The tests built with WebTestCase are slower than the ones built with sfTestBrowser, since the web server is in the middle of every request. They also require that you have a working web server configuration. However, the WebTestCase object comes with numerous navigation methods on top of the assert*() ones. Using these methods, you can simulate a complex browsing session. Here is a subset of the WebTestCase navigation methods:

WebTestCase构建的测试比用sfTestBrowser构建的慢。他们也需要你有一个有效的web服务器配置。然而,WebTestCase对象带来了assert*()打头的几个函数。用这些函数,你可以模拟完全的浏览器session。这是WebTestCase函数的子集:

- - -
get($url, $parameters) setField($name, $value) authenticate($name, $password)
post($url, $parameters) clickSubmit($label) restart()
back() clickImage($label, $x, $y) getCookie($name)
forward() clickLink($label, $index) ageCookies($interval)
- - -

We could easily do the same test case as previously with a WebTestCase. Beware that you now need to enter full URIs, since they will be requested to the web server:

我们可以用WebTestCase像以前那样来实现简单的测试用例。记得你现在需要输入整个URL了,因为请求会发送到web服务器上:

require_once('simpletest/web_tester.php');

class QuestionTest extends WebTestCase
{
public function test_QuestionShow()
{
$this->get(’http://askeet/frontend_test.php/question/what-can-i-offer-to-my-step-mother’);
$this->assertWantedPattern(’/My stepmother has everything a stepmother is usually offered/’);
}
}

The additional methods of this object could help us test how a submitted form is handled, for instance to unit test the login process:

这些函数可以帮助测试提交表单怎么处理的,例如单元测试login过程:

public function test_QuestionAdd()
{
$this->get('http://askeet/frontend_dev.php/');
$this->assertLink('sign in/register');
$this->clickLink('sign in/register');
$this->assertWantedPattern('/nickname:/');
$this->setField('nickname', 'fabpot');
$this->setField('password', 'symfony');
$this->clickSubmit('sign in');
$this->assertWantedPattern('/fabpot profile/');
}

It is very handy to be able to set a value for fields and submit the form as you would do by hand. If you had to simulate that by doing a POST request (and this is possible by a call to ->post($uri, $parameters)), you would have to write in the test function the target of the action and all the hidden fields, thus depending too much on the implementation. For more information about form test with Simple Test, read the related chapter of the Simple Test documentation.

手动做表单提交测试太费劲了。如果你模拟用POST(靠->post($url,$parameters)实现),你还要在测试函数里写,还要在hidden里写。用简单测试实现表单测试的更多信息,看看简单测试手册吧。

Selenium

(译者:由于时间紧迫,再加上本人一直使用watir来完成此部分测试。所以关于Selenium就省略翻译了。呵呵!)

A few words about environments

写给环境的几句话

Web tests have to use a front controller, and as such can use a specific environment (i.e. configuration). Symfony provides a test environment to every application by default, specifically for unit tests. You can define a custom set of settings for it in your application config/ directory. The default configuration parameters are (extract from askeet/apps/frontend/config/settings.yml):

web测试必须使用前台控制,也要用具体环境(也就是说,配置)。symfony默认提供了测试环境给每个应用程序,特别是为单元测试。你可以自定义一套设置,就在config/下。默认配置参数是(askeet/apps/frontend/config/settings.yml):

test:
.settings:
# E_ALL | E_STRICT & ~E_NOTICE = 2047
error_reporting: 2047
cache: off
stats: off
web_debug: off

The cache, the stats and the web_debug toolbar are set to off. However, the code execution still leaves traces in a log file (askeet/log/frontend_test.log). You can have specific database connection settings, for instance to use another database with test data in it.

缓存,状态和web调试工具栏都关闭了。然而,代码执行仍然保存在日志里(askeet/log/frontend_test.log)。需要有详细数据库连接配置,例如使用其他数据库。

This is why all the external URIs mentioned above show a frontend_test.php: the test front controller has to be specified - otherwise, the default index.php production controller will be used in place, and you won’t be able to use a different database or to have separate logs for your unit tests.

这就是为什么上面提到的外部URL展现了frontend_test.php:测试前台控制必须详细——否则,默认index.php开发控制会被换用,而且你不能用不同的数据库或者给单元测试用不同的日志。

Web tests are not supposed to be launched in production. They are a developer tool, and as such, they should be run in the developer’s computer, not in the host server.

web测试并非在开发中载入的。他们是开发工具,进一步讲,他们应该运行在开发者的计算机上,而不是在服务器上运行。

Post a Comment