第一篇:play手把手教你創(chuàng)建一個博客項目-10.完整的應(yīng)用程序測試
10.完整的應(yīng)用程序測試
現(xiàn)在,我們已經(jīng)結(jié)束了博客引擎的編碼工作,但對項目來說還沒有完成,為了讓我們的代碼能夠完全正確的工作,我們還需要對項目進(jìn)行測試。
當(dāng)然,我們之前已經(jīng)為yabe的模型層功能書寫的單元測試,并且確信博客引擎的核心功能已經(jīng)進(jìn)行了完好的測試,但是對于一個web應(yīng)用程序來說模型層只是其中的一部分,我們還需要確定web接口能否按預(yù)期的目標(biāo)一樣正常工作。也就是說還需要測試yabe博客引擎的控制器層,甚至需要對UI自身進(jìn)行測試,比如我們的JavaScript代碼。
測試控制器部分
Play提供了一種功能,就是使用JUnit來直接測試應(yīng)用程序的控制器。我們把這些測試叫做‘功能性測試’,這是因為我們打算測試web應(yīng)用程序的完整功能。
基本上,一個功能性測試將直接調(diào)用Play的ActionInvoker,和一個HTTP請求相似。因此我們需要給出一個HTTP方法、一個URI和多個HTTP參數(shù)。Play之后會路由這些請求,調(diào)用相應(yīng)的action,并且回發(fā)到填寫的響應(yīng)(filled response)。之后,你就可以對之進(jìn)行分析,以檢查響應(yīng)內(nèi)容是否你所預(yù)期的。接下來讓我們書寫第一個功能性測試代碼,打開 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);} } 現(xiàn)在看,它還是一個標(biāo)準(zhǔn)的JUnit測試。請注意,在這里我們使用Play的 FunctionalTest超類,主要是為了得到所有可用的工具。這個測試只對應(yīng)用程序的主頁進(jìn)行了測試(/ URL渲染一個HTML響應(yīng),以‘200 OK’作為狀態(tài)代碼)。接下來,我們將檢查管理區(qū)域(administration area)的安全工作能否正常工作。在ApplicationTest.java里添加下面這個新測試: ? @Test public void testAdminSecurity(){ Response response = GET(“/admin”);assertStatus(302, response);assertHeaderEquals(“Location”, “/login”, response);} ?
現(xiàn)在,用play test命令把yabe應(yīng)用程序運行于測試模式,打開http://localhost:9000/@tests, 選擇ApplicationTest.java測試并運行。是綠色的嗎? 當(dāng)然!通過這種方式,我們可以對所有的應(yīng)用程序功能性進(jìn)行測試,但把這用于測試一個基于html的web應(yīng)用程序時,這并不是最好的方式。對于我們的博客引擎項目來說,直接在真實的瀏覽器進(jìn)行測試可能會更好。這就是play的‘Selenium tests’測試要干的事。
這種基于“功能性測試”的JUnit仍舊很有用,特別是用于測試一個返回非html響應(yīng)(比如JSON或XML)的Web services時。
書寫Selenium測試代碼 Selenium 是一個專用于測試web應(yīng)用程序的測試工具。這個工具最酷的就是Selenium允許我們在一個瀏覽器里直接運行測試套件,由于它使用的是真實的瀏覽器,因此,我們可以確定測試通過后,項目就可以在生產(chǎn)環(huán)境下完美的運行。一個Selenium測試套件就是一個特殊的html文件。HTML syntax required by Selenium必須使用的HTML語句比較單調(diào)(使用HTML表格元素進(jìn)行數(shù)據(jù)格式化顯示),好消息是play將使用play模板引擎和一系列支持簡單Selenium表示語法的標(biāo)簽來幫助你生成這些元素)。使用模板最有趣的特點是你根本不需要‘static scenarios’,并且可以使用play模板強大的功能(如循環(huán)、條件塊)來書寫更復(fù)雜的測試。
然而,你仍舊可以繼續(xù)在模板里使用原始的HTML Selenium語法,如果需要的話,還可以忘記特定的Selenium標(biāo)簽。如果你使用多個用于生成test scenarios(比如Selenium IDE)的Selenium工具中的一個,這將變得非常有趣。
新創(chuàng)建的play應(yīng)用程序的默認(rèn)測試套件已經(jīng)包含了一個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} 運行這個測試應(yīng)該不會有任何問題。它只打開了主頁,并檢測頁面內(nèi)容是否包含了 ‘Application error’文本。
然而,和任何復(fù)雜的測試一樣,在導(dǎo)航到應(yīng)用程序并進(jìn)行測試之前,你需要設(shè)置一系列眾所周知的數(shù)據(jù),我們當(dāng)然需要重用fixture概念,并且在開始測試之前使用yabe/test/data.yml文件,#{fixture /}標(biāo)簽導(dǎo)入這些測試數(shù)據(jù): #{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里,你應(yīng)該在兩個連續(xù)的 測試運行操作期間保持同一個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} 運行這個測試,并確定沒有錯誤發(fā)生,結(jié)果應(yīng)該是綠色的。
接下來我們將書寫很特殊的測試,測試打開主頁后檢查默認(rèn)的博文是否顯示出來:
#{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} 在這里,我們使用了標(biāo)準(zhǔn)的Selenium語法,它叫Selenese。運行它(你可以運行于一個不同的瀏覽器窗口里)。我們現(xiàn)在就可以測試評論窗體了,只需要添加一個 #{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} 再次才能,哦,失敗了!這里有一個嚴(yán)重的問題出現(xiàn)。我們事實上不能正確測試captcha驗證碼機制,因此,我們必須搞一些欺騙手段。在測試模式下,我們將驗證任何代碼作為一個正確的驗證碼。我們知道當(dāng)框架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手把手教你創(chuàng)建一個博客項目-08.添加身份認(rèn)證
08.添加身份認(rèn)證
在上一節(jié)是,我們?yōu)閼?yīng)用程序添加了管理區(qū)域(administration area)功能,現(xiàn)在我們將在這些管理區(qū)域中插入一些身份認(rèn)證功能。幸運的是,play已經(jīng)為這個功能準(zhǔn)備了一個模塊,這個模塊叫Secure(安全)。
在程序里允許使用Secure模塊
在yabe/conf/application.conf文件里允許使用Secure模塊,并重啟程序: # Import the secure module module.secure=${play.path}/modules/secure 重啟后,play應(yīng)用在控制臺顯示模塊已經(jīng)成功啟動的相關(guān)信息。
Secure模塊帶有一系列默認(rèn)的路由,需要在yabe/conf/routes里引入(或定義自己的路由也行): # Import Secure routes * / module:secure 保護(hù)admin(此處指需要身份認(rèn)證的)控制器
安全模塊提供了一個controllers.Secure控制器,它聲明了所有可能用到的攔截器。當(dāng)然,我們可以以繼承這個控制器的方法獲得其攔截器,但是java只允許單繼承,這就導(dǎo)致了一些問題。
為了避免單繼承帶來的限制,我們可以用@With來注釋admin控制器,以告訴play去調(diào)用相應(yīng)的攔截器:
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: 事實上,現(xiàn)在你就可以試著輸入任意username/password對看看,它其實并沒有對身份進(jìn)行認(rèn)證。
定制身份認(rèn)證處理
應(yīng)用程序必須提供一個controllers.Secure.Security實例來定制身份認(rèn)證處理。通過繼承這個類來創(chuàng)建我們自己版本的Secure類,我們可以指定如何對用戶身份進(jìn)行認(rèn)證。
創(chuàng)建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;} } 既然我們已經(jīng)擁有了User對象,那么就非常容易實現(xiàn)這個方法: static boolean authenticate(String username, String password){ return User.connect(username, password)!= null;} 現(xiàn)在打開http://localhost:9000/logout進(jìn)行登錄嘗試,用戶名和密碼在initial-data.yml文件里定義,比如bob@gmail.com/secret。
重構(gòu)管理區(qū)域(administration area)
我們之前已經(jīng)使用CRUD模塊來實現(xiàn)管理區(qū)域,但是這個管理區(qū)域仍然沒有集成到博客UI里,接下來我們將在一個新的管理區(qū)域上開始工作。這個新的管理區(qū)域允許每個作者訪問他自己的博客。而超級用戶則繼續(xù)使用完整功能的管理區(qū)域。
接下來,讓我們?yōu)楣芾聿糠謩?chuàng)建一個新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();} } 重構(gòu)路由定義yabe/conf/routes: # Administration GET /admin/? Admin.index * /admin module:crud 請注意路由的順序,第一行就匹配了的http請求相應(yīng)的action會率先使用,同時忽略在其之下的路由配置。也就是說Admin控制器必須位于第二行之上,第二條路由將匹配所有其他的/admin請求,用于調(diào)用CRUD模塊頁面,否則/admin/將映射到CRUD.index而不是Admin.index。
現(xiàn)在把yabe/app/views/main.html模塊里的 ‘Log in to write something’文本修改到Admin.index控制器action:
…
…最后一件事就是為yabe/app/views/Admin/index.html模板文件完成所有的填充工作,讓我們從簡單的開始: Welcome ${user}!現(xiàn)在回到主頁,單擊 ‘Log in to write something’鏈接就回進(jìn)入樣的管理區(qū)域頁面: 非常好!但是既然我們已經(jīng)有幾個管理區(qū)域的頁面,那么,我們就應(yīng)該有一個超級模板以重用代碼,讓我們創(chuàng)建一個yabe/app/views/admin.html模板:
#{get 'moreStyles' /}
第三篇:play手把手教你創(chuàng)建一個博客項目-03.構(gòu)建第一個頁面
03.構(gòu)建第一個頁面
之前,我們已經(jīng)編寫好了數(shù)據(jù)模型,是時候為應(yīng)用程序創(chuàng)建第一個頁面了。這個頁面只顯示當(dāng)前發(fā)表的博文完整內(nèi)容,同時顯示之前發(fā)表的博文列表。下面是該頁面結(jié)構(gòu)示意圖:
在啟動時加載默認(rèn)數(shù)據(jù)
事實上,在編寫第一個頁面之前,我們還需要做一些工作。在一個沒有任何測試數(shù)據(jù)的頁面上進(jìn)行工作并不太好,你甚至不能進(jìn)行任何測試。
為博客程序注入測試數(shù)據(jù)的一條途徑就是在應(yīng)用程序啟動時加載一個固定文件。為了實現(xiàn)這個目的,我們將創(chuàng)建一個引導(dǎo)任務(wù)(Bootstrap Job)。一個play job 任務(wù)就是一在沒有任何http請求的情況下執(zhí)行一些特定工作,比如在應(yīng)用程序啟動時或指定時間間隔時使用CRON任務(wù)。
接下來,讓我們創(chuàng)建/yabe/app/Bootstrap.java job文件,使用Fixtures加載一系列默認(rèn)數(shù)據(jù):
import play.*;import play.jobs.*;import play.test.*;
import models.*;
@OnApplicationStart public class Bootstrap extends Job {
public void doJob(){ // 檢查數(shù)據(jù)庫是否為空 if(User.count()== 0){ Fixtures.loadModels(“initial-data.yml”);} } } 在這里,我們使用@OnApplicationStart來注釋這個Job,用于告訴play我們打算在應(yīng)用程序啟動時同步運行這個任務(wù)。
事實上,在DEV和PROD模式下,這個任務(wù)的運行情況有所不同。在DEV模式下,play會等待第一個請求達(dá)到時才運行任務(wù)。因此這個任務(wù)會在第一個請求到達(dá)時才同步執(zhí)行,也就是說,如果這個任務(wù)失敗,你會在瀏覽器里看到錯誤消息。在PROD模式里,這個任務(wù)將會在應(yīng)用程序啟動時就執(zhí)行(與play run命令同步),以防止應(yīng)用程序在啟動時發(fā)生錯誤。
你必須在yabe/conf/下創(chuàng)建一個initial-data.yml文件。當(dāng)然,你也可以重用我們之前在data.yml里定義的內(nèi)容。
博客主頁
這次,我們將真正開始編寫主頁代碼。
還記得程序的第一個頁面是如何顯示的嗎?首先是在routes文件里指定/ URL 將調(diào)用controllers.Application.index()action方法,然后這個index()調(diào)用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}