第一篇:play手把手教你创建一个博客项目-10.完整的应用程序测试
10.完整的应用程序测试
现在,我们已经结束了博客引擎的编码工作,但对项目来说还没有完成,为了让我们的代码能够完全正确的工作,我们还需要对项目进行测试。
当然,我们之前已经为yabe的模型层功能书写的单元测试,并且确信博客引擎的核心功能已经进行了完好的测试,但是对于一个web应用程序来说模型层只是其中的一部分,我们还需要确定web接口能否按预期的目标一样正常工作。也就是说还需要测试yabe博客引擎的控制器层,甚至需要对UI自身进行测试,比如我们的JavaScript代码。
测试控制器部分
Play提供了一种功能,就是使用JUnit来直接测试应用程序的控制器。我们把这些测试叫做‘功能性测试’,这是因为我们打算测试web应用程序的完整功能。
基本上,一个功能性测试将直接调用Play的ActionInvoker,和一个HTTP请求相似。因此我们需要给出一个HTTP方法、一个URI和多个HTTP参数。Play之后会路由这些请求,调用相应的action,并且回发到填写的响应(filled response)。之后,你就可以对之进行分析,以检查响应内容是否你所预期的。接下来让我们书写第一个功能性测试代码,打开 yabe/test/ApplicationTest.java单元测试: import org.junit.*;import play.test.*;import play.mvc.*;import play.mvc.Http.*;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)的安全工作能否正常工作。在ApplicationTest.java里添加下面这个新测试: „ @Test public void testAdminSecurity(){ Response response = GET(“/admin”);assertStatus(302, response);assertHeaderEquals(“Location”, “/login”, response);} „
现在,用play test命令把yabe应用程序运行于测试模式,打开http://localhost:9000/@tests, 选择ApplicationTest.java测试并运行。是绿色的吗? 当然!通过这种方式,我们可以对所有的应用程序功能性进行测试,但把这用于测试一个基于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/Application.test.html文件:
*{ 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/data.yml文件,#{fixture /}标签导入这些测试数据: #{fixture delete:'all', load:'data.yml' /}
#{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:'data.yml' /}
#{selenium} clearSession()
// Open the home page, and check that no error occurred open('/')waitForPageToLoad(1000)assertNotTitle('Application error')#{/selenium} 运行这个测试,并确定没有错误发生,结果应该是绿色的。
接下来我们将书写很特殊的测试,测试打开主页后检查默认的博文是否显示出来:
#{fixture delete:'all', load:'data.yml' /}
#{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/Application.java file to skip this validation in test mode: „
if(!Play.id.equals(“test”)){ validation.equals(code, Cache.get(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 application.conf file, and restart the application in test mode.# Import the cobertura module in test mode %test.module.cobertura=${play.path}/modules/cobertura Now reopen the browser at the http://localhost:9000/@tests URL, select all tests and run them.All should be green.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/index.html 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.
第二篇:play手把手教你创建一个博客项目-08.添加身份认证
08.添加身份认证
在上一节是,我们为应用程序添加了管理区域(administration area)功能,现在我们将在这些管理区域中插入一些身份认证功能。幸运的是,play已经为这个功能准备了一个模块,这个模块叫Secure(安全)。
在程序里允许使用Secure模块
在yabe/conf/application.conf文件里允许使用Secure模块,并重启程序: # Import the secure module module.secure=${play.path}/modules/secure 重启后,play应用在控制台显示模块已经成功启动的相关信息。
Secure模块带有一系列默认的路由,需要在yabe/conf/routes里引入(或定义自己的路由也行): # Import Secure routes * / module:secure 保护admin(此处指需要身份认证的)控制器
安全模块提供了一个controllers.Secure控制器,它声明了所有可能用到的拦截器。当然,我们可以以继承这个控制器的方法获得其拦截器,但是java只允许单继承,这就导致了一些问题。
为了避免单继承带来的限制,我们可以用@With来注释admin控制器,以告诉play去调用相应的拦截器:
package controllers;
import play.*;import play.mvc.*;
@With(Secure.class)public class Posts extends CRUD { } 同样用于Comments, Users和Tags控制器。
Now if you try to access any admin action, you should get a log-in page: 事实上,现在你就可以试着输入任意username/password对看看,它其实并没有对身份进行认证。
定制身份认证处理
应用程序必须提供一个controllers.Secure.Security实例来定制身份认证处理。通过继承这个类来创建我们自己版本的Secure类,我们可以指定如何对用户身份进行认证。
创建yabe/app/controllers/Security.java文件,重写authenticate()方法: package controllers;
import models.*;
public class Security extends Secure.Security {
static boolean authenticate(String username, String password){ return true;} } 既然我们已经拥有了User对象,那么就非常容易实现这个方法: static boolean authenticate(String username, String password){ return User.connect(username, password)!= null;} 现在打开http://localhost:9000/logout进行登录尝试,用户名和密码在initial-data.yml文件里定义,比如bob@gmail.com/secret。
重构管理区域(administration area)
我们之前已经使用CRUD模块来实现管理区域,但是这个管理区域仍然没有集成到博客UI里,接下来我们将在一个新的管理区域上开始工作。这个新的管理区域允许每个作者访问他自己的博客。而超级用户则继续使用完整功能的管理区域。
接下来,让我们为管理部分创建一个新Admin控制器: package controllers;
import play.*;import play.mvc.*;
import java.util.*;
import models.*;
@With(Secure.class)public class Admin extends Controller {
@Before static void setConnectedUser(){ if(Security.isConnected()){ User user = User.find(“byEmail”, Security.connected()).first();renderArgs.put(“user”, user.fullname);} }
public static void index(){ render();} } 重构路由定义yabe/conf/routes: # Administration GET /admin/? Admin.index * /admin module:crud 请注意路由的顺序,第一行就匹配了的http请求相应的action会率先使用,同时忽略在其之下的路由配置。也就是说Admin控制器必须位于第二行之上,第二条路由将匹配所有其他的/admin请求,用于调用CRUD模块页面,否则/admin/将映射到CRUD.index而不是Admin.index。
现在把yabe/app/views/main.html模块里的 ‘Log in to write something’文本修改到Admin.index控制器action:
…
…最后一件事就是为yabe/app/views/Admin/index.html模板文件完成所有的填充工作,让我们从简单的开始: Welcome ${user}!现在回到主页,单击 ‘Log in to write something’链接就回进入样的管理区域页面: 非常好!但是既然我们已经有几个管理区域的页面,那么,我们就应该有一个超级模板以重用代码,让我们创建一个yabe/app/views/admin.html模板:
#{get 'moreStyles' /}
第三篇:play手把手教你创建一个博客项目-03.构建第一个页面
03.构建第一个页面
之前,我们已经编写好了数据模型,是时候为应用程序创建第一个页面了。这个页面只显示当前发表的博文完整内容,同时显示之前发表的博文列表。下面是该页面结构示意图:
在启动时加载默认数据
事实上,在编写第一个页面之前,我们还需要做一些工作。在一个没有任何测试数据的页面上进行工作并不太好,你甚至不能进行任何测试。
为博客程序注入测试数据的一条途径就是在应用程序启动时加载一个固定文件。为了实现这个目的,我们将创建一个引导任务(Bootstrap Job)。一个play job 任务就是一在没有任何http请求的情况下执行一些特定工作,比如在应用程序启动时或指定时间间隔时使用CRON任务。
接下来,让我们创建/yabe/app/Bootstrap.java job文件,使用Fixtures加载一系列默认数据:
import play.*;import play.jobs.*;import play.test.*;
import models.*;
@OnApplicationStart public class Bootstrap extends Job {
public void doJob(){ // 检查数据库是否为空 if(User.count()== 0){ Fixtures.loadModels(“initial-data.yml”);} } } 在这里,我们使用@OnApplicationStart来注释这个Job,用于告诉play我们打算在应用程序启动时同步运行这个任务。
事实上,在DEV和PROD模式下,这个任务的运行情况有所不同。在DEV模式下,play会等待第一个请求达到时才运行任务。因此这个任务会在第一个请求到达时才同步执行,也就是说,如果这个任务失败,你会在浏览器里看到错误消息。在PROD模式里,这个任务将会在应用程序启动时就执行(与play run命令同步),以防止应用程序在启动时发生错误。
你必须在yabe/conf/下创建一个initial-data.yml文件。当然,你也可以重用我们之前在data.yml里定义的内容。
博客主页
这次,我们将真正开始编写主页代码。
还记得程序的第一个页面是如何显示的吗?首先是在routes文件里指定/ URL 将调用controllers.Application.index()action方法,然后这个index()调用render()方法渲染/yabe/app/views/Application/index.html模板。我们将保留这些组件,但是我们会在其中添加代码来加载博文列表并显示。打开/yabe/app/controllers/Application.java控制器并修改index()action来加载博文列表:
package controllers;
import java.util.*;
import play.*;import play.mvc.*;
import models.*;
public class Application extends Controller {
public static void index(){ Post frontPost = Post.find(“order by postedAt desc”).first();List
olderPosts = Post.find(“order by postedAt desc”).from(1).fetch(10);render(frontPost, olderPosts);} } 看到我们是如何向render方法传递对象的吗?通过这种方式,我们就可以在模板里使用相同的名称来访问这些对象了。在上面的代码里,我们在模板里就可以直接使用变量frontPost和olderPosts。
打开/yabe/app/views/Application/index.html并修改,用于显示这些对象: #{extends 'main.html' /} #{set title:'Home' /}
#{if frontPost}