10.完整的应用程序测试
现在,我们已经结束了博客引擎的编码工作,但对项目来说还没有完成,为了让
我们的代码能够完全正确的工作,我们还需要对项目进行测试。
当然,我们之前已经为 yabe 的模型层功能书写的单元测试,并且确信博客引擎
的核心功能已经进行了完好的测试,但是对于一个 web 应用程序来说模型层只
是其中的一部分,我们还需要确定 web 接口能否按预期的目标一样正常工作。
也就是说还需要测试 yabe 博客引擎的控制器层,甚至需要对 UI 自身进行测试,
比如我们的 JavaScript 代码。
测试控制器部分
Play 提供了一种功能,就是使用 JUnit 来直接测试应用程序的控制器。我们把这
些测试叫做‘功能性测试’,这是因为我们打算测试 web 应用程序的完整功能。
基本上,一个功能性测试将直接调用 Play 的 ActionInvoker,和一个 HTTP 请求
相似。因此我们需要给出一个 HTTP 方法、一个 URI 和多个 HTTP 参数。 Play
之后会路由这些请求,调用相应的 action,并且回发到填写的响应(filled
response)。之后,你就可以对之进行分析,以检查响应内容是否你所预期的。
接下来让我们书写第一个功能性测试代码,打开 yabe/test/ 单
元测试:
import .*;
import .*;
import .*;
import .*;
import models.*;
public class ApplicationTest extends FunctionalTest {
@Test
public void testThatIndexPageWorks() {
Response response = GET("/");
assertIsOk(response);
assertContentType("text/html", response);
assertCharset("utf-8", response);
}
}
现在看,它还是一个标准的 JUnit 测试。请注意,在这里我们使用 Play 的
FunctionalTest 超类,主要是为了得到所有可用的工具。这个测试只对应用程序
的主页进行了测试(/ URL 渲染一个 HTML 响应,以‘200 OK’作为状态代码)。
接下来,我们将检查管理区域(administration area)的安全工作能否正常工作。
在 里添加下面这个新测试:
…
@Test
public void testAdminSecurity() {
Response response = GET("/admin");
assertStatus(302, response);
assertHeaderEquals("Location", "/login", response);
}
…
现在,用 play test 命令把 yabe 应用程序运行于测试模式,打开
http://localhost:9000/@tests, 选择 测试并运行。
是绿色的吗?
http://localhost:9000/@tests
当然!通过这种方式,我们可以对所有的应用程序功能性进行测试,但把这用于
测试一个基于 html 的 web 应用程序时,这并不是最好的方式。对于我们的博客
引擎项目来说,直接在真实的浏览器进行测试可能会更好。这就是 play 的
‘Selenium tests’测试要干的事。
这种基于“功能性测试”的 JUnit 仍旧很有用,特别是用于测试一个返回非 html 响
应(比如 JSON 或 XML)的 Web services 时。
书写 Selenium 测试代码
Selenium 是一个专用于测试 web 应用程序的测试工具。这个工具最酷的就是
Selenium 允许我们在一个浏览器里直接运行测试套件,由于它使用的是真实的浏
览器,因此,我们可以确定测试通过后,项目就可以在生产环境下完美的运行。
一个 Selenium 测试套件就是一个特殊的 html 文件。HTML syntax required by
Selenium 必须使用的 HTML 语句比较单调(使用 HTML 表格元素进行数据格式
化显示),好消息是 play 将使用 play 模板引擎和一系列支持简单 Selenium 表示语
法的标签来帮助你生成这些元素)。使用模板最有趣的特点是你根本不需要‘static
scenarios’,并且可以使用 play 模板强大的功能(如循环、条件块)来书写更复
杂的测试。
然而,你仍旧可以继续在模板里使用原始的 HTML Selenium 语法,如果需要的
话,还可以忘记特定的 Selenium 标签。如果你使用多个用于生成 test scenarios(比
如 Selenium IDE)的 Selenium 工具中的一个,这将变得非常有趣。
新创建的 play 应用程序的默认测试套件已经包含了一个 Selenium 测试,打开
yabe/test/ 文件:
*{ You can use plain Selenium commands using the selenium tag }*
#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
运行这个测试应该不会有任何问题。它只打开了主页,并检测页面内容是否包含
了 ‘Application error’文本。
然而,和任何复杂的测试一样,在导航到应用程序并进行测试之前,你需要设置
一系列众所周知的数据,我们当然需要重用 fixture 概念,并且在开始测试之前
使用 yabe/test/ 文件,#{fixture /}标签导入这些测试数据:
#{fixture delete:'all', load:'' /}
#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
另外一个重要的事情就是我们要在测试启动时检查我们是否有一个最新的用户
session。这个 session 将存储在浏览器的临时 cookie 里,你应该在两个连续的测
试运行操作期间保持同一个 session,因此,让我们用一个特定的命令开始测试:
#{fixture delete:'all', load:'' /}
#{selenium}
clearSession()
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
运行这个测试,并确定没有错误发生,结果应该是绿色的。
接下来我们将书写很特殊的测试,测试打开主页后检查默认的博文是否显示出来:
#{fixture delete:'all', load:'' /}
#{selenium 'Check home page'}
clearSession()
// Open the home page
open('/')
// Check that the front post is present
assertTextPresent('About the model layer')
assertTextPresent('by Bob, 14 Jun 09')
assertTextPresent('2 comments , latest by Guest')
assertTextPresent('It is the domain-specific representation')
// Check older posts
assertTextPresent('The MVC application')
assertTextPresent('Just a test of YABE')
#{/selenium}
在这里,我们使用了标准的 Selenium 语法,它叫 Selenese。
运行它(你可以运行于一个不同的浏览器窗口里)。
我们现在就可以测试评论窗体了,只需要添加一个 #{selenium /} 到模板里即可:
#{selenium 'Test comments'}
// Click on 'The MVC application post'
clickAndWait('link=The MVC application')
assertTextPresent('The MVC application')
assertTextPresent('no comments')
// Post a new comment
type('content', 'Hello')
clickAndWait('css=input[type=submit]')
// Should get an error
assertTextPresent('no comments')
assertTextPresent('Author is required')
type('author', 'Me')
clickAndWait('css=input[type=submit]')
// Check
assertTextPresent('Thanks for posting Me')
assertTextPresent('1 comment')
assertTextPresent('Hello')
#{/selenium}
再次才能,哦,失败了!这里有一个严重的问题出现。
我们事实上不能正确测试 captcha 验证码机制,因此,我们必须搞一些欺骗手段。
在测试模式下,我们将验证任何代码作为一个正确的验证码。我们知道当框架
a. We know that we’re in test mode when the framework id is test. So let’s modify the
postComment action in the yabe/app/controllers/ file to skip this
validation in test mode:
…
if(!("test")) {
(code, (randomID)).message("Invalid code. Please
type it again");
}
…
Now just modify the test case to type any code in the text field, as is:
…
type('author', 'Me')
type('code', 'XXXXX')
clickAndWait('css=input[type=submit]')
…
And now run the test again, it should work.
Measuring code coverage
Of course we haven’t written all required test cases for the application. But it’s
enough for this tutorial. Now in a real-world project, how can we know if we have
written enough test cases? We need something called ‘code coverage’.
The Cobertura module generates code coverage reports using the Cobertura tool.
Install the module using the install command:
play install cobertura-{version}
We need to enable this module only for test mode. So add this line to the
file, and restart the application in test mode.
# Import the cobertura module in test mode
%=${}/modules/cobertura
Now reopen the browser at the http://localhost:9000/@tests URL, select all tests and
run them. All should be green.
http://localhost:9000/@tests
When all tests are passed, stop the application and cobertura will then generate the
code coverage report. You can then open the
yabe/test-result/code-coverage/ in your browser and check the report.
If you start the application again, you can also view it at
http://localhost:9000/@cobertura.
As you see we’re far from testing all of the application’s cases. A good testing suite
should approach 100%, even if it is of course nearly impossible to check all the code.
Typically because we often need to hack in test mode, like we did for the captcha.
http://localhost:9000/@cobertura