2006年1月11日
Currently the concept of Progressive Enhancement is getting hotter and hotter. It emphasizes accessibility, semantic markup, and the importance of separating the complex rich interaction logic into well modularized javascript files. It's really a usefully way of thinking about how to modularize and manage web presentation components. But the Rails framework doesn't have good support for PE, so we have to define our own convention and helpers to make our life easier.
Usually, I'd like to organize js files in the Rails convention, which means we'll have something like this:
app
|
- views
|
- admin
|
_ new.html.erb
- index.html.erb
public
|
- javascripts
|
- admin
|
- new.js
- index.js
And new.js looks similar to:
$(document).ready(function() {
enhanceInteractionOnElements();

});
function helper_methods() {

}
Then, add the follow method to ApplicationHelper module:
def page_javascript_include_tag
file = "#{params[:controller]}/#{params[:action]}.js"
File.exist?("#{RAILS_ROOT}/public/javascripts/#{file}") ? javascript_include_tag(file) : ""
end
this method will look for js file for a particular page. And in you layout file, add one line in the head sectin:
<%= page_javascript_include_tag %>
That's it. Whenever you request an action of a particular controller, it will find and include the PE js files automatically. Now we've very very primitive support of PE in Rails framework now.
前幾天在JavaEye海闊被標(biāo)題黨陰了一把,看了一篇轉(zhuǎn)的文章叫《 被中國人誤傳了數(shù)千年的七句話》,頗有些哭笑不得的感慨:
1. 這些話的確是被誤傳了不假,但是最多也就一百年吧。中國知識分子不讀四書五經(jīng)史子集的壞風(fēng)氣大抵是開始于所謂的新文化運(yùn)動吧。再往前的人,對于這些典籍字字爬梳,提了上句馬上背下句,就算是以章句式解讀為主的宋元,也不應(yīng)該隨隨便便就被忽悠了,更不用說反對宋儒理學(xué)講究正本清源的明清了。
2. 古人斷章取義是一種風(fēng)雅的言談習(xí)慣,所謂“雅言”是要字字出典的,有點(diǎn)像對暗號。比如我們家貓跑了,擱古代我肯定問“誰之過歟?”,十有八九會回答說,“言是典守者之過也”,這句射的是“虎兕出于柙”,正好應(yīng)景。甚至為了詼諧應(yīng)景,故意曲解文義的情況也是很常見的。如果以此為證說誤傳的話,恐怕只能算是牛嚼牡丹了。順便多說一句,其實(shí)這個毛病現(xiàn)代人也有,不過不再是古文了,大多數(shù)是電影電視臺詞:“空氣在顫抖仿佛天空在燃燒。是啊,暴風(fēng)雨就要來了”,“道哥,牌子啊”,“你看我的英語,有沒有長進(jìn)”之類的,雖不復(fù)古韻,但也還算有趣。
P.S. : 今天team里有人把David Wheeler的名言,貼在了Quote Wall上:“Any problem in computer science can be solved with another layer of indirection.”
這到的確算是一句被誤傳的名言吧,原文是“Any problem in computer science can be solved with another layer of indirection. But that usually will create another problem.”
Aurum is a Ruby-based LALR(n) parser generator that you can use to develop your own domain specified languages, scripting languages and programming languages.Although it's just yet another parser generator, Aurum is slightly different from other widely used parser generators:
- One of major targets of Aurum is to simplify external DSL development, espectually Ruby external DSL.
- Aurum uses incremental LALR(n) algorithm instead of the common used LALR(1)/Full LALR(n) algorithm. That means:
- Allowing the user to express grammars in a more intuitive mannar.
- Making it easier to handle complicated grammars. For exmaple,
COBOL(LALR(2 or 3)), simplified nature language(LALR(3+)) and etc.
- Closer to Generalized LR in language recognizing but much more faster.
- Smaller parsing table comparing to Full LALR/LR(n) algorithm.
- Aurum supports grammar reuse, and itslef'll be shipped with some pre-defined common structures. One of the pain points of external DSL is that you have to re-define lots of common structures, such as if statements, block structure and etc. With Aurum, you could simply reuse them.
- Aurum uses a Ruby interal DSL as meta-language, and provides a generic lexer/parser as well. You could test your grammar by the comprehensive testing libraries Ruby has(you could even develop your lexer/parser in the TDD fashion).
- As the name suggested, Aurum, the Latin word for Gold, is partially inspired by the GOLD Parsing System. The grammar you created with Aurum could be completely independent of any implementation language,even Ruby.(not implemented yet :) )
Ok, let's start from the 'Hello World in Compiler Construction' —— Expression Evaluation
1 require 'aurum'
2
3 class ExpressionGrammar < Aurum::Grammar
4 tokens do
5 ignore string(' ').one_or_more # <= a
6 _number range(?0, ?9).one_or_more # <= b
7 end
8
9 precedences do # <= c
10 left '*', '/'
11 left '+', '-'
12 end
13
14 productions do # <= d
15 expression expression, '+', expression {expression.value = expression1.value + expression2.value} # <= e
16 expression expression, '-', expression {expression.value = expression1.value - expression2.value}
17 expression expression, '*', expression {expression.value = expression1.value * expression2.value}
18 expression expression, '/', expression {expression.value = expression1.value / expression2.value}
19 expression '(', expression, ')' do expression.value = expression1.value end # <= f
20 expression _number {expression.value = _number.value.to_i}
21 expression '+', _number {expression.value = _number.value.to_i}
22 expression '-', _number {expression.value = -_number.value.to_i}
23 end
24 end
If you has any experience with other compiler compiler/parser generator, you probably could understand what happens above quite easily. Instead of explaining things like token, character class, and production, I'd like to emphasise some Aurum conventions:
- At point a, we use 'ignore' directive to declare the ignored pattern, such as whitespaces etc.'string' is one of the helper methods(others are enum, range and concat), which is used to define lexical patterns. It will create a pattern matching the given string exactly.
- At point b, we declare a lexical token named '_number'. In Aurum, lexical tokens, or terminals from syntax perspective, always start with '_'. The expression '_token_name pattern' is equivalent to 'match pattern, :recognized => :_toke_name'. The 'match' directive is a common way to associate lexical action with leixcal pattern.
- At point c, we declare operator precedences of the Expression grammar.The eariler the operators definied, the higher precedence they will have.
- At point d, we declare syntax rules of Expression grammar. According to Aurum naming convention, all terminals should start with '_' while all nontermainls start with lower case alphabet character. String literals will be interpreted as reserve words, and added to lexer automatically.
- At point e, we define a semantic action to the Addition rule. In semantic action, you could access to the objects in value stack via the name of corresponding symbols.If there are more than one symbol with the same name, you could differentiate them by the order they appered in the production.
- At point f, we use do..end instead of {..}. Using Ruby internal DSL as meta-langauge is a double-side sword, you have to bear its flaws while enjoying the remaining parts. There is no perfect world, isn't it?
Now, let's find out how we could use this expression grammar. You could use the helper method as below(it will recalcuate lexical table and parsing table for every call, could be quite slow):
1 puts ExpressionGrammar.parse_expression('1+1').value
or use the lexical table and parsing table to create your own lexer & parser:
1 lexer = Aurum::Engine::Lexer.new(ExpressionGrammar.lexical_table, '1+1')
2 parser = Aurum::Engine::Parser.new(ExpressionGrammar.parsing_table(:expression))
3 puts parser.parse(lexer).value
At the end of this post, I'd like to give another grammar example coming from Martin Fowler's HelloParserGenerator series:
1 require 'aurum'
2
3 Item = Struct.new(:name)
4
5 class Catalog < Aurum::Grammar
6 tokens do
7 ignore enum(" \r\n").one_or_more
8 _item range(?a,?z).one_or_more
9 end
10
11 productions do
12 configuration configuration, item {configuration.value = configuration1.value.merge({item.value.name => item.value})}
13 configuration _ {configuration.value = {}}
14 item 'item', _item {item.value = Item.new(_item.value)}
15 end
16 end
17
18 config = Catalog.parse_configuration(<<EndOfDSL).value
19 item camera
20 item laser
21 EndOfDSL
22
23 puts config['camera'].name
P.S.:The post is based on the developing version of Aurum(0.2.0). You could get it from the svn repository.
P.S.P.S.: There is a more complicated example in the examples directory, a simple Smalltalk interpreter. Have fun:)
A very brief introduction to Aurum
Aurum是一個用Ruby實(shí)現(xiàn)的LALR(n) parser generator(是的,又是一個parser generator),不過它和其他一些廣泛應(yīng)用的parser generator相比略有不同的:
1.Aurum的主要目標(biāo)之一,是簡化external DSL的開發(fā)(尤其是ruby external DSL)。
2.Aurum采用增量LALR(n)算法,而不是通常的LALR(1)。這意味著:
a.不必由于LALR(1)能力的限制,而改寫語法,很多在LALR(1)中沖突的語法在LALR(n)中可以比較自然地表達(dá)。
b.由于識別能力的增強(qiáng),可以處理一些比較復(fù)雜的語法,比如COBOL(LALR(2)或LALR(3)),比如一些簡化的自然語言(LALR(3+))。
c.處理能力接近Generalized LR,卻快很多
d.比起Full LALR/LR(n),增量算法生成的語法表更小。
3.出于簡化external DSL實(shí)現(xiàn)的考慮,Aurum支持語法重用。
4.Aurum采用Ruby internal DSL作為語法聲明的元語言,可以利用Ruby豐富的測試框架,有效地對編譯/解釋/分析器進(jìn)行測試。
5.正如名字所暗示的,Aurum(Gold的化學(xué)名稱)的一部分靈感來自 GOLD parsing system,它將支持獨(dú)立于平臺和語言的編譯器開發(fā)。
好,閑話少說,看一個例子,編譯原理中的Hello World —— 表達(dá)式求值:
1 require 'aurum'
2
3 class ExpressionGrammar < Aurum::Grammar
4 tokens do
5 ignore string(' ').one_or_more # <= a
6 _number range(?0, ?9).one_or_more # <= b
7 end
8
9 precedences do # <= c
10 left '*', '/'
11 left '+', '-'
12 end
13
14 productions do # <= d
15 expression expression, '+', expression {expression.value = expression1.value + expression2.value} # <= e
16 expression expression, '-', expression {expression.value = expression1.value - expression2.value}
17 expression expression, '*', expression {expression.value = expression1.value * expression2.value}
18 expression expression, '/', expression {expression.value = expression1.value / expression2.value}
19 expression '(', expression, ')' do expression.value = expression1.value end # <= f
20 expression _number {expression.value = _number.value.to_i}
21 expression '+', _number {expression.value = _number.value.to_i}
22 expression '-', _number {expression.value = -_number.value.to_i}
23 end
24 end
如果諸位對之前有用過compiler compiler或者parser generator的話,應(yīng)該能看個七七八八吧。我大概解釋一下:
a.這里定義了文法空白,也就是被lexer忽略的部分,在通常的語言中,是空格回車換行之類的字符;string是用于定義lexical pattern的helper方法(出了string之外,還有range, enum和concat);ignore是一個預(yù)定義的說明指令,表示若文本匹配給定模式則該文本會被lexer自動忽略,其格式為:
ignore pattern {//lexical action}
b.此處為lexical token聲明,所有l(wèi)exical token必須以_開頭,其格式為:
_token_name pattern {//lexical action}
這里其實(shí)是一個簡略寫法,等價于
match pattern, :recognize => :_token_name
c.此處為運(yùn)算符優(yōu)先級聲明,支持左/右結(jié)合運(yùn)算符(無結(jié)合屬性運(yùn)算符開發(fā)中);每一行中所有運(yùn)算符具有相同優(yōu)先級;比它下一行的運(yùn)算符高一個優(yōu)先級。比如在這個例子中,'*'和'/'具有相同優(yōu)先級,但是比'+'和'-'的優(yōu)先級別高。
d.此處為語法規(guī)則聲明,所使用的symbol主要有三種,nonterminal(小寫字母開頭),terminal(其實(shí)就是lexical token,以_開頭)和literal(字符串常量),其中所有l(wèi)iteral都會被自動聲明為保留字。
e.此處定義了一條文法規(guī)則(加法),以及對應(yīng)的semantic action。在semantic action中可以直接通過symbol的名字來獲取值棧中的對象。如遇到同名symbol,則按照出現(xiàn)順序進(jìn)行編號即可。
f.其實(shí)這個沒啥,只不過由于我們使用的是Ruby DSL,所以有時候不能都用{},需要do end,這就是一個例子。
最后測試一下實(shí)際中如何使用定義好的語法(使用helper method,注意由于分析表沒有緩存,每次都會重算語法表,僅僅適用于debug mode。)
puts ExpressionGrammar.parse_expression('1+1').value
或者通過分析表自己構(gòu)造lexer和parser
lexer = Aurum::Engine::Lexer.new(ExpressionGrammar.lexical_table, '1+1')
parser = Aurum::Engine::Parser.new(ExpressionGrammar.parsing_table(:expression))
puts parser.parse(lexer).value
最后最后,給另外一個例子,就是 Martin Fowler Blog上的 HelloParserGenerator系列中所用的語法:
1 require 'aurum'
2
3 Item = Struct.new(:name)
4
5 class Catalog < Aurum::Grammar
6 tokens do
7 ignore enum(" \r\n").one_or_more
8 _item range(?a,?z).one_or_more
9 end
10
11 productions do
12 configuration configuration, item {configuration.value = configuration1.value.merge({item.value.name => item.value})}
13 configuration _ {configuration.value = {}}
14 item 'item', _item {item.value = Item.new(_item.value)}
15 end
16 end
17
18 config = Catalog.parse_configuration(<<EndOfDSL).value
19 item camera
20 item laser
21 EndOfDSL
22
23 puts config['camera'].name
P.S.:本文是根據(jù)Aurum0.2.0寫成的,你可以從rubyforge的svn上得到它。
P.S.P.S.: 在exmaples目錄里有一個更復(fù)雜一些的例子,是一個簡單的Smalltalk解釋器。
《Programming Erlang》第8章后面有一個練習(xí),Ring Benchmark。就是說創(chuàng)建N個進(jìn)程,把它們組合成環(huán)狀。然后在這個環(huán)上把一條消息在環(huán)上傳遞M圈,然后記錄所有的時間。實(shí)現(xiàn)起來也挺簡單,20行左右吧:
1 -module(ring_benchmark). 2 -export([start/2]). 3 4 start(N, M) -> 5 Pid = create_process(self(), N - 1, M), 6 time(fun() -> Pid ! start, loop(Pid, M) end). 7 8 time(Fun) -> 9 statistics(wall_clock), 10 Fun(), 11 {_,Time} = statistics(wall_clock), 12 io:format("Run : ~w s ~n", [Time/1000]). 13 14 create_process(Pid, 0, _) -> Pid; 15 create_process(Pid, N, M) -> create_process(spawn(fun() -> loop(Pid, M) end), N - 1, M). 16 17 loop(_, 0) -> void; 18 loop(Next, M) -> 19 receive 20 Message -> Next ! Message, 21 loop(Next, M - 1) 22 end. 23 24
有意思是它還有一個第二問,讓你用另外一種熟悉的語言實(shí)現(xiàn)同樣的功能,發(fā)送同樣多的消息,也把時間記錄下來,然后寫一篇blog來publish你的結(jié)果。其實(shí),大家心知肚明,這種lightweight process啊,message passing concurrency啊都是Erlang的強(qiáng)項(xiàng),而且實(shí)測結(jié)果也著實(shí)頗為恐怖,一般也就沒那閑心拿別的東西來陪襯一把了(Armstrong同學(xué)自己實(shí)現(xiàn)了一個Java version,效率大約能差到百倍吧)。不過還真有那寫不信邪的老大, 用stackless python實(shí)現(xiàn)了同樣的ring benchmark,發(fā)現(xiàn)比erlang還快...后來修改代碼去掉io操作,Erlang倒是比stackless python快些,但也只是一些而已。
http://www.dcs.ed.ac.uk/home/stg/fengshui.ps.gz今天早上打開Google Reader就看見這么一篇,內(nèi)容倒也罷了,不過是bad smell的另一個名字而已,硬要扯上分水也只能算是勉勉強(qiáng)強(qiáng)。不過郁悶的是,竟然是個洋人的手筆,國學(xué)不昌實(shí)不能不令我輩心憂啊。 p.s. 預(yù)計未來6個月口頭禪:"你這寫當(dāng)心壞了項(xiàng)目的風(fēng)水"
http://www.infoq.com/cn/articles/domain-web-testing 應(yīng)用Selenium進(jìn)行Web測試往往會存在幾個bad smell: 1.大量使用name, id, xpath等頁面元素。無論是功能修改、UI重構(gòu)還是交互性改進(jìn)都會影響到這些元素,這使得Selenium測試變得非常脆弱。 2.過于細(xì)節(jié)的頁面操作不容易體現(xiàn)出行為的意圖,一段時間之后就很難真正把握測試原有的目的了,這使得Selenium測試變得難于維護(hù)。 3.對具體數(shù)據(jù)取值的存在依賴,當(dāng)個別數(shù)據(jù)不再合法的時候,測試就會失敗,但這樣的失敗并不能標(biāo)識功能的缺失,這使得Selenium測試變得脆弱且難以維護(hù)。 而這幾點(diǎn)直接衍生的結(jié)果就是不斷地添加新的測試,而極少地去重構(gòu)、利用原有測試。其實(shí)這到也是正常,單元測試測試寫多了,也有會有這樣的問題。不過比較要命的是,Selenium的執(zhí)行速度比較慢(相對單元測試),隨著測試逐漸的增多,運(yùn)行時間會逐漸增加到不可忍受的程度。一組意圖不明難以維護(hù)的Selenium測試,可以很輕松地在每次build的時候殺掉40分鐘甚至2個小時的時間,在下就有花2個小時坐在電腦前面等待450個Selenium測試運(yùn)行通過的悲慘經(jīng)歷。因此合理有效地規(guī)劃Selenium測試就顯得格外的迫切和重要了。而目前比較行之有效的辦法,往大了說,可以叫domain based web testing,具體來講,就是Page Object Pattern。 Page Object Pattern里有四個基本概念:Driver, Page, Navigator和Shortcut。Driver是測試真正的實(shí)現(xiàn)機(jī)制,比如Selenium,比如Watir,比如HttpUnit。它們懂得如何去真正執(zhí)行一個web行為,通常包含像click,select,type這樣的表示具體行為的方法;Page是對一個具體頁面的封裝,它們了解頁面的結(jié)構(gòu),知道諸如id, name, class,xpath這類實(shí)現(xiàn)細(xì)節(jié),并描述用戶可以在其上進(jìn)行何種操作;Navigator則代表了URL,表示一些不經(jīng)頁面操作的直接跳轉(zhuǎn);最后Shortcut就是helper方法了,需要看具體的需要了。下面來看一個超級簡單的例子——測試登錄頁面。 1. Page Object 假設(shè)我們使用一個單獨(dú)的Login Page進(jìn)行登錄,那么我們可能會將登錄的操作封裝在一個名為LoginPage的page object里:
1 class LoginPage 2 def initialize driver 3 @driver = driver 4 end 5 6 def login_as user 7 @driver.type 'id= ', user[:name] 8 @driver.type 'xpath= ', user[:password] 9 @driver.click 'name= ' 10 @driver.wait_for_page_to_load 11 end 12 end
login_as是一個具有業(yè)務(wù)含義的頁面行為。在login_as方法中,page object負(fù)責(zé)通過依靠id,xpath,name等信息完成登錄操作。在測試中,我們可以這樣來使用這個page object:
1 page = LoginPage.new $selenium 2 page.login_as :name => 'xxx', :password => 'xxx' 3
不過既然用了ruby,總要用一些ruby sugar吧,我們定義一個on方法來表達(dá)頁面操作的環(huán)境:
1 def on page_type, &block 2 page = page_type.new $selenium 3 page.instance_eval &block if block_given? 4 end
之后我們就可以使用page object的類名常量和block描述在某個特定頁面上操作了:
1 on LoginPage do 2 login_as :name => 'xxx', :password => 'xxx' 3 end 4
除了行為方法之外,我們還需要在page object上定義一些獲取頁面信息的方法,比如獲取登錄頁面的歡迎詞的方法:
def welcome_message @driver.get_text 'xpath= ' end
這樣測試也可表達(dá)得更生動一些:
1 on LoginPage do 2 assert_equal 'Welcome!', welcome_message 3 login_as :name => 'xxx', :password => 'xxx' 4 end
當(dāng)你把所有的頁面都用Page Object封裝了之后,就有效地分離了測試和頁面結(jié)構(gòu)的耦合。在測試中,只需使用諸如login_as, add_product_to_cart這樣的業(yè)務(wù)行為,而不必依靠像id,name這些具體且易變的頁面元素了。當(dāng)這些頁面元素發(fā)生變化時,只需修改相應(yīng)的page object就可以了,而原有測試基本不需要太大或太多的改動。 2. Assertation 只有行為還夠不成測試,我們還要判斷行為結(jié)果,并進(jìn)行一些斷言。簡單回顧一下上面的例子,會發(fā)現(xiàn)還有一些很重要的問題沒有解決:我怎么判斷登錄成功了呢?我如何才能知道真的是處在登錄頁面了呢?如果我調(diào)用下面的代碼會怎樣呢?
1 $selenium.open url_of_any_page_but_not_login 2 on LoginPage { }
因此我們還需要向page object增加一些斷言性方法。至少,每個頁面都應(yīng)該有一個方法用于判斷是否真正地達(dá)到了這個頁面,如果不處在這個頁面中的話,就不能進(jìn)行任何的業(yè)務(wù)行為。下面修改LoginPage使之包含這樣一個方法:
1 LoginPage.class_eval do 2 include Test::Unit::Asseration 3 def visible? 4 @driver.is_text_present( ) && @driver.get_location ==  5 end 6 end
在visible?方法中,我們通過對一些特定的頁面元素(比如URL地址,特定的UI結(jié)構(gòu)或元素)進(jìn)行判斷,從而可以得之是否真正地處在某個頁面上。而我們目前表達(dá)測試的基本結(jié)構(gòu)是由on方法來完成,我們也就順理成章地在on方法中增加一個斷言,來判斷是否真的處在某個頁面上,如果不處在這個頁面則不進(jìn)行任何的業(yè)務(wù)操作:
1 def on page_type, &block 2 page = page_type.new $selenium 3 assert page.visible?, "not on #{page_type}" 4 page.instance_eval &block if block_given? 5 page 6 end 7
這個方法神秘地返回了page對象,這里是一個比較tricky的技巧。實(shí)際上,我們只想利用page != nil這個事實(shí)來斷言頁面的流轉(zhuǎn),比如,下面的代碼描述登錄成功的頁面流轉(zhuǎn)過程:
on LoginPage do assert_equal 'Welcome!', welcome_message login_as :name => 'xxx', :password => 'xxx' end assert on WelcomeRegisteredUserPage
除了這個基本斷言之外,我們還可以定義一些業(yè)務(wù)相關(guān)的斷言,比如在購物車頁面里,我們可以定義一個判斷購物車是否為空的斷言:
1 def cart_empty? 2 @driver.get_text('xpath= ') == 'Shopping Cart(0)' 3 end
需要注意的是,雖然我們在page object里引入了Test::Unit::Asseration模塊,但是并沒有在斷言方法里使用任何assert*方法。這是因?yàn)椋拍钌蟻碇vpage object并不是測試。使之包含一些真正的斷言,一則概念混亂,二則容易使page object變成針對某些場景的test helper,不利于以后測試的維護(hù),因此我們往往傾向于將斷言方法實(shí)現(xiàn)為一個普通的返回值為boolean的方法。 3. Test Data 測試意圖的體現(xiàn)不僅僅是在行為的描述上,同樣還有測試數(shù)據(jù),比如如下兩段代碼:
1 on LoginPage do 2 login_as :name => 'userA', :password => 'password' 3 end 4 assert on WelcomeRegisteredUserPage 5 6 registered_user = {:name => 'userA', :password => 'password'} 7 on LoginPage do 8 login_as registered_user 9 end 10 assert on WelcomeRegisteredUserPage
測試的是同一個東西,但是顯然第二個測試更好的體現(xiàn)了測試意圖:使用一個已注冊的用戶登錄,應(yīng)該進(jìn)入歡迎頁面。我們看這個測試的時候,往往不會關(guān)心用戶名啊密碼啊具體是什么,我們關(guān)心它們表達(dá)了怎樣的測試案例。我們可以通過DataFixture來實(shí)現(xiàn)這一點(diǎn):
1 module DataFixture 2 USER_A = {:name => 'userA', :password => 'password'} 3 USER_B = {:name => 'userB', :password => 'password'} 4 5 def get_user identifier 6 case identifier 7 when :registered then return USER_A 8 when :not_registered then return USER_B 9 end 10 end 11 end
在這里,我們將測試案例和具體數(shù)據(jù)做了一個對應(yīng):userA是注冊過的用戶,而userB是沒注冊的用戶。當(dāng)有一天,我們需要將登錄用戶名改為郵箱的時候,只需要修改DataFixture模塊就可以了,而不必修改相應(yīng)的測試:
1 include DataFixtureDat 2 3 user = get_user :registered 4 on LoginPage do 5 login_as user 6 end 7 assert on WelcomeRegisteredUserPage
當(dāng)然,在更復(fù)雜的測試中,DataFixture同樣可以使用真實(shí)的數(shù)據(jù)庫或是Rails Fixture來完成這樣的對應(yīng),但是總體的目的就是使測試和測試數(shù)據(jù)有效性的耦合分離:
1 def get_user identifier 2 case identifier 3 when :registered then return User.find ' .' 4 end 5 end
4.Navigator 與界面元素類似,URL也是一類易變且難以表達(dá)意圖的元素,因此我們可以使用Navigator使之與測試解耦。具體做法和Test Data相似,這里就不贅述了,下面是一個例子:
1 navigate_to detail_page_for @product 2 on ProductDetailPage do 3 . 4 end
5. Shortcut 前面我們已經(jīng)有了一個很好的基礎(chǔ),將Selenium測試與各種脆弱且意圖不明的元素分離開了,那么最后shortcut不過是在蛋糕上面最漂亮的奶油罷了——定義具有漂亮語法的helper:
1 def should_login_successfully user 2 on LoginPage do 3 assert_equal 'Welcome!', welcome_message 4 login_as user 5 end 6 assert on WelcomeRegisteredUserPage 7 end
然后是另外一個magic方法:
1 def given identifer 2 words = identifier.to_s.split '_' 3 eval "get_#{words.last} :#{words[0..-2].join '_'}" 4 end
之前的測試就可以被改寫為:
def test_should_xxxx should_login_successfully given :registered_user end
這是一種結(jié)論性的shortcut描述,我們還可以有更behaviour的寫法:
1 def login_on page_type 2 on page_type do 3 assert_equal 'Welcome!', welcome_message 4 login_as @user 5 end 6 end 7 8 def login_successfully 9 on WelcomeRegisteredUserPage 10 end 11 12 def given identifer 13 words = identifier.to_s.split '_' 14 eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}" 15 end
最后,測試就會變成類似驗(yàn)收條件的樣子:
1 def test_should_xxx 2 given :registered_user 3 login_on LoginPage 4 assert login_successfully 5 end
總之shortcut是一個無關(guān)好壞,只關(guān)乎想象力的東西,盡情揮灑Ruby DSL吧:D 結(jié)論 Selenium是一個讓人又愛又恨的東西,錯誤地使用Selenium會給整個敏捷團(tuán)隊(duì)的開發(fā)節(jié)奏帶來災(zāi)難性的影響。不過值得慶幸的是正確地使用Selenium的原則也是相當(dāng)?shù)暮唵危?br> 1.通過將脆弱易變的頁面元素和測試分離開,使得頁面的變化不會對測試產(chǎn)生太大的影響。 2.明確指定測試數(shù)據(jù)的意圖,不在測試用使用任何具體的數(shù)據(jù)。 3.盡一切可能,明確地表達(dá)出測試的意圖,使測試易于理解。 當(dāng)然,除了遵循這幾個基本原則之外,使用page object或其他domain based web testing技術(shù)是個不錯的選擇。它們將會幫助你更容易地控制Selenium測試的規(guī)模,更好地平衡覆蓋率和執(zhí)行效率,從而更加有效地交付高質(zhì)量的Web項(xiàng)目。 鳴謝 此文中涉及的都是我最近三周以來對Selenium測試進(jìn)行重構(gòu)時所采用的真實(shí)技術(shù)。感謝Nick Drew幫助我清晰地劃分了Driver, Page, Nagivator和Shortcut的層次關(guān)系,它們構(gòu)成我整個實(shí)踐的基石;感謝Chris Leishman,在和他pairing programming的過程中,他幫助我錘煉了Ruby DSL;還有Mark Ryall和Abhi,是他們第一次在項(xiàng)目中引入了Test Data Fixture,使得所有人的工作都變得簡單起來。
最近很多時間都在用Ruby,逐漸地發(fā)現(xiàn)了一件很不爽的事情,就是Ruby的end關(guān)鍵字。block多套幾層,很容易就最后一頁都是end了...難怪有人說,ruby不過是另一種acceptable Lisp,“最后一頁都是括號”的經(jīng)典標(biāo)志以另外一種形式復(fù)現(xiàn)了...對于Lisp的括號,我還是可以接受的,但是滿眼的end,直接讓我回憶起10年前沖刺N(yùn)OI的種種,CPU直接切換到實(shí)模式,什么可讀啊小粒度方法全都沒有了,審美觀赤裸地變?yōu)槎绦【?..最后殺紅了眼,一行算出文法定義的所有nullable symbols...
1 while @productions.inject(false) {|c, p| c |= !nullable?(p.nonterminal) && p.symbols.all? {|s| nullable? s} && @nullables << p.nonterminal}
注意1不是行號...這句用的statement modifier, 1是我能想到的最小ruby語句了... p.s. 我現(xiàn)在已經(jīng)恢復(fù)到OO保護(hù)模式了...剛才追求短小過了頭的同時,發(fā)現(xiàn)了ruby bulid-in object的一個陷阱... a = Array.new 5, [] [[],[],[],[],[]] a[0] << 1 [[1],[1],[1],[1],[1]] 想不到華麗的Array直接假設(shè)傳進(jìn)去的都是值對象了,好歹您也調(diào)個dup啊...
Which Programming Language are You?p.s. 這個可能不準(zhǔn)...因?yàn)槔钅瑢W(xué)竟然是Lisp...怎么可能...
剛才和李默同學(xué)回憶了一下,發(fā)現(xiàn)我自從入行以來做了很多x項(xiàng)目...下面一一列舉一下。
1. IEC61970 Metadata: Electricity Power Trading System
當(dāng)時剛上班,team里有一個Doamin知識很厲害的清華的博士,畢業(yè)的論文就是電力市場,而清華又是國家引入IEC61970的五家之一。所以他很超前的把這兩個東西結(jié)合在一起,做成了一個系統(tǒng)。說實(shí)話,剛了解IEC61970的時候,我是相當(dāng)?shù)恼鸷车模汹s上那時候MDA風(fēng)氣剛起,IEC61970又是同時MOF(Meta Object Facility)和RDF based,華麗得不行。一下子我就變成了一個MDA guy,一個metadata guy...以至于,在BJUG最初的2年里,MDA/MOF/Metadata成為了主旋律...
2. IEC61970 & CWM(Common Warehouse Metamodel) & Office Plugin : Data Warehouse Integration System
這是迄今為止,我最不愿意回憶的一個項(xiàng)目...因?yàn)镺ffice Plugin...動輒藍(lán)屏的遭遇讓我心有余悸...這是一個backend是J2EE,frontend是.Net的office插件系統(tǒng),主要是報表...兩邊都使用CWM作為數(shù)據(jù)統(tǒng)一的形式...基本上做到一半我的意志就崩潰了...
3. DB Migration/Refactoring : Jyxpearl
這個項(xiàng)目...是李默同學(xué)的私房最愛,從大學(xué)一直做了很久,改版無數(shù)次...當(dāng)時沒有這么流行的好詞,什么DB Migration啊,DB Refactoring啊,那時候我們統(tǒng)稱導(dǎo)數(shù)據(jù)...我導(dǎo)了好多會...基本上線一回導(dǎo)一回...時至今日...李默同學(xué)總是不無得意的說:你看,你DB Migration的能力就是我培養(yǎng)的...
4. JMI(Java Metadata Interface) & Eclipse RCP : Multi/Rich Client ERP Product
這個team其實(shí)挺華麗的,老欒的產(chǎn)品經(jīng)理,李默是開發(fā)經(jīng)理,超級資深行業(yè)專家(人家實(shí)際做過生產(chǎn)科長,MRPII,ERP都是人家玩剩下的)老齊做需求,俺是Architect,還有動物園里的豬Senior Dev,我認(rèn)識人中美工能力第一交互設(shè)計能力第一的米米姐做UI和交互。由于當(dāng)時看了netbeans和sun的官方JMI實(shí)現(xiàn)得太玩具。我們決定從自己的JMI實(shí)現(xiàn)開始,系統(tǒng)結(jié)構(gòu)要求多客戶端,web,rcp都要...所以是超輕http協(xié)議的b/s,c/s。結(jié)構(gòu)還是不錯的,過程李默和我當(dāng)然是敏捷了。似乎一起都超級完美的時候,就是要壞菜的時候...企業(yè)事業(yè)部解散了...
5. Java Communication & Eclipse RCP : IC Card Reader
上面那個項(xiàng)目解散之后,我跟李默賦閑在家,有不忍心打擾政府,自謀生路找的項(xiàng)目...這個項(xiàng)目要用IC卡讀卡器,為了鍛煉我們的Eclipse RCP能力,我們決定用eclipse rcp來做。于是問題就出來了...IC卡怎么辦?google一把發(fā)現(xiàn)天無絕人之路...Java有一個Communication包,可以連接serial port...不過當(dāng)時tricky的是...我的本子沒有串口,我們買了一個串口到usb的轉(zhuǎn)換器...發(fā)現(xiàn)根本不能用...于是只好跑到李默家用他華麗的臺式機(jī)(這廝當(dāng)年誓言旦旦的說,laptop太慢,一定要用臺式機(jī),東借西借搞了個2G RAM SATA[注意,這是伏筆]的機(jī)器)。我當(dāng)時就覺得,Java的這個東西基本就是充數(shù)的,貌似完全沒有人用過,文檔啥的都特少...只能自己摸索。在經(jīng)歷了無數(shù)次失敗之后,終于成功了。在showcase那天的上午,我最后實(shí)驗(yàn)了讀卡什么的,都沒問題。興高采烈的把jar拷到優(yōu)盤上,剛插到usb口上...只見一道閃電...機(jī)器黑了...據(jù)李默后來分析是主板燒了...我說沒事,拿上硬盤,土一點(diǎn)也不影響showcase。李默說...這個...SATA耶...還不流行呢...我綠...此后很長時間,我都懷疑是我跟李默同學(xué)范沖,超級項(xiàng)目殺手...
6. RDF, Semantic Web, SparQL : Ontology-Relationship DB Mapping
這是在一家公司做產(chǎn)品,當(dāng)時我元數(shù)據(jù)/MDA領(lǐng)域頗有積累...跟這家公司做得類似,就過來負(fù)責(zé)研發(fā)本體到關(guān)系數(shù)據(jù)庫的映射...兼帶在D2RQ的基礎(chǔ)上實(shí)現(xiàn)一個SparQL查詢語言。怎么樣...聽上去很華麗吧...到現(xiàn)在我都認(rèn)為,這個項(xiàng)目是我最有潛力的牛皮,不定那天web x.0了,我也老了,我就可以拉著小朋友的手去吹牛b了"05年我就做semantic web,O/R mapping知道不?Ontology啊,你們啊,sometime too simple"...不過估計這一天還早得很呢
7. Agile Domain Specified Language : Goodhope
這個也是李默同學(xué)有份的項(xiàng)目...話里的敏捷DSL實(shí)踐...不過說實(shí)話,也有點(diǎn)X...
我常常聽到這樣的觀點(diǎn):敏捷軟件開發(fā)并不是真正的革命性的方法,它所采用的技術(shù)大多都是古已有之的。比如迭代,你看很哪本軟件工程的教科書上沒有提到迭代開發(fā)呢?在比如說User Story,看上去也不只不過是Use Case的翻版而已吧!甚至我看RUP也和敏捷方法沒有太大的區(qū)別吧! 要我說,這些人要么是不真的了解敏捷開發(fā),沒有認(rèn)識到敏捷開發(fā)的革命性,只是用外在的形式來把它和其他方法進(jìn)行了比較。有又或者是實(shí)施敏捷方法的時候不徹底,所以四處碰壁以至于搞起了修正主義。最可怕的就是某些大公司,看敏捷火了,總有包裝一下,到底還是要賣產(chǎn)品。敏捷軟件開發(fā)就是一個革命性的方法,只不過它要顛覆的不僅僅是低質(zhì)量的軟件開發(fā)方式,更重要的是,它要顛覆軟件生產(chǎn)企業(yè)和軟件的使用企業(yè)之間的生產(chǎn)關(guān)系!!這一點(diǎn)在敏捷宣言里寫得再明白不過了
Customer collaboration over Contract negotiation
敏捷軟件開發(fā),就是要以一種更合理的共贏的合作關(guān)系,代替以前畸形的采購式的合約關(guān)系。為什么合約關(guān)系就是畸形的?我們來看看合約雙方的處境。
首先軟件團(tuán)隊(duì)方面承擔(dān)了過多的風(fēng)險:業(yè)務(wù)變化,改代碼!!商業(yè)抉擇轉(zhuǎn)換,改代碼!!憑啥你甲方的緣故非要我承擔(dān)額外的成本?你說我冤不冤?冤!但是人家甲方也冤!!人家花了大把的銀子,拿到一堆不能用的軟件(你要是硬件人家還能轉(zhuǎn)手賣點(diǎn)錢),就像你要砍樹別人給你把鏟子,你要種樹人家給了你把鋸。擱你,你也不愿意。且不說博弈,就算雙方都有心把事情做好,按合同來,甲方不干;不按合同來,乙方不干,最后變成“有心殺賊無力回天”,大家一起扯扯皮等二期算了。lose-lose,沒有贏家。
那么合作的關(guān)系是什么呢?合作的關(guān)系就好比你去subway買三明治,面包你自己選,要什么肉你來挑,蔬菜,cheese,醬汁你也自己看著辦。技術(shù)我來,口味你選。技術(shù)失敗我負(fù)責(zé),口味不合適你負(fù)責(zé)。你做你的強(qiáng)項(xiàng)我來我的強(qiáng)項(xiàng),最終大家高高興興嘻嘻哈哈不吵不鬧,作出一頓可口午餐。這是時候,生產(chǎn)關(guān)系變了,我不是你的冷冰冰的供應(yīng)商,你也不是我邪惡的客戶,我們是拴在一根繩子上的螞蚱。成功是我們的,失敗也是我們的。榮辱與共,攜手并肩。聽著有點(diǎn)耳熟?沒錯,SaaS。敏捷宣言早就說了,CoC啊。從供應(yīng)商變成服務(wù)商,從服務(wù)商變成戰(zhàn)略合作伙伴,這是在給軟件企業(yè)指出路,新的生產(chǎn)關(guān)系已經(jīng)盡在其中了。
如果看不清敏捷的這個根本革命點(diǎn),以為還是開發(fā)方法的小打小鬧,那么敏捷根本實(shí)施不成。這話一般我不敢說的,程序員自發(fā)實(shí)施敏捷,只在一種情況下可能成功:大企業(yè)的IT部門。再趕上個強(qiáng)力的IT領(lǐng)導(dǎo),自家人嘛,有什么不好談的。一來二去,就成功了(看看C3,說白了不就是IT部門和業(yè)務(wù)部門?)但是,如果是做項(xiàng)目的公司,你營銷手段不改變,敏捷就不可能成功。你的客戶跟你不是合作關(guān)系,你通過敏捷增加質(zhì)量(符合性質(zhì)量)的工作就不會被人可,那么就不能成為投資,只能是成本。當(dāng)成本增加到不可承擔(dān)的時候,敏捷就不了了之了。為什么好多人說老板沒有響應(yīng)?舊的生產(chǎn)關(guān)系下敏捷根本就是負(fù)擔(dān)。
說道這里,說一下以敏捷聞名的ThoughtWorks。其實(shí)很多人都以為ThougtWorks只有方法論咨詢,沒錯我們是有方法論咨詢,但是也有業(yè)務(wù)模式咨詢,客戶業(yè)務(wù)模式不改變,他怎么能徹底敏捷?這點(diǎn)大家不可不查啊。
Yesterday I found a interesting? ruby library? ——? blinkenlights, which enables you to control the LEDs on your keyboard. I thouhgt it could be a cheap replacement of lava light, so I wrote a ruby script called 'Poor Man's Lava'
#!/usr/local/bin/ruby require 'rss/1.0' require 'rss/2.0' require 'open-uri' require 'rubygems' require 'blinkenlights'
SUCCESS = 'success'
def read_rss source='http://cruisecontrolrb.thoughtworks.com/projects/CruiseControl.rss' ? content = '' ? open(source) do |s| content = s.read end ? rss = RSS::Parser.parse content, false ? rss.items[0].title.include?(SUCCESS) ? all_ok : alarm end
def all_ok times = 50 ? BlinkenLights.open { |lights| times.times {lights.random} } end
def alarm times = 50, invertal = 0.0 ? BlinkenLights.open { |lights| times.times {lights.flash invertal} } end
while true ? read_rss ? sleep 5? end
make sure to have sufficient permissions to access the device, or you could simple run it as super user.
通常人們會將User Story和Use Case放在一起比較,雖然二者在形式上具有一定相似性,但是究其本質(zhì)來說,還是天淵之別的。這一點(diǎn),專業(yè)BA李默同學(xué)總結(jié)的格外準(zhǔn)確:“用戶故事是可見的商業(yè)價值,而不是功能描述”。想要更好的理解這句話,需要了解什么是好的用戶故事。好的用戶故事,可用INVEST原則來概括:
I - Independent N - Negotiable V - Valuable E - Estimable S - Small T - Testable
我個人覺得,這個總結(jié)雖好,但不免分散注意。要我說,想把握好User Story,只用把握兩個就夠了Negotiable和Valuable。那么首先要確定什么是Negotiable的。User Story有一個流傳廣泛的書寫形式:
As <role>, I'd like to <action>, so that <benifit/value/goal>.
為了更好的獲取story還有很多最佳實(shí)踐,比如personas, 比如business process modeling,其實(shí)這些全是糖衣炮彈,As, I'd like to都是噱頭,就是為了把用戶忽悠暈了,然后圖窮匕現(xiàn)直取商業(yè)價值和目標(biāo)。一旦商業(yè)價值確定下來,role, action都是可以negotiable。比如李默之前在文章里舉的用戶登錄的例子,輸不輸用戶明密碼?可以商量嘛!是不是只有注冊用戶可以享受個性服務(wù)?可以商量嘛!關(guān)鍵是用戶想要什么,至于怎么實(shí)現(xiàn)這些到頭來都是可以商量的,都是Negotiable。只有客戶的商業(yè)價值是不能商量的,也沒的商量。價值沒有了,目標(biāo)不存在了,這個User Story也就沒用了,商量啥?重寫一張就好了。
因此user story又有另外一個名稱,叫requirement placeholder。就是客戶價值的"立此存照"。至于具體需求,那么就到iteration plan meeting上是商量吧,看看當(dāng)時什么樣的形式(功能)才是最符合用戶需要。到此,其實(shí)大家可以看出來了,user story重點(diǎn)就不再How上,而是在Why上的。有了why,且可Negotiable,把握了精神,你就是按用例來寫需求又有何妨涅?
有了valuable和negotiable的想法墊底,在看看基于user story的初步計劃制定——也就是有名的prioritization——就容易理解多了。用戶根據(jù)每張卡的價值,自行比較作出決定,大體場景就跟向神仙許愿一樣。
神仙:我可以滿足你一個愿望。 我:我要榮華富貴!!! 神仙:哦,榮華富貴,那么要不要愛情涅? 我:恩,這個...那我要忠貞的愛情好了!! 神仙:哦,忠貞的愛情,那么要不要健康平安呢? 我:呃.... repeat 無數(shù)次,最終我要了一件過冬的皮猴...
自從到了ThouhgtWorks,我訂閱的rss越來越少,很多程度上來說,有了自家的Planet TW,很多相看的blog可以一次性看全了,沒啥太大的必要去看其他的了。這不,在訂閱了Planet TW后的兩年中,我只增訂了一家:InfoQ。呵呵,想不到如今中文站也來了 http://www.infoq.com/cn/。不錯不錯。
pair programing是所有XP實(shí)踐中爭議最大的一個,但竊以為確實(shí)XP實(shí)施的關(guān)鍵關(guān)鍵實(shí)踐之一,甚至于,我認(rèn)為很多XP實(shí)施的失敗都是由于沒有采用pair programming而造成的。 要了解pair為什么重要,就要了解pair的目的在何。當(dāng)然了,大多數(shù)人都知道pair的重點(diǎn)在于知識傳遞,知識共享,持續(xù)走查,降低代碼缺陷等等等等。這些都是pair的優(yōu)點(diǎn),不過最重要的一點(diǎn)卻常常被忽略——pair programing的最直接而又最根本的目的之一在于simple design。  上圖是著名的Ron Jefferies Model,可以看到XP最佳實(shí)踐被劃分成了一個一個的圓圈,而pair, TDD, refactor和simple design位于中心。這并不是說這四個實(shí)踐就是xp的核心。jefferies model每一圈代表了xp實(shí)踐過程中的不同關(guān)注點(diǎn),最中心的是dev視角,其次是team視角,最外層是交付/管理視角。每圈上的最佳時間多少都有些緊耦合,放開其他的不講,我們專門說說dev圈,pair programing, tdd, refactor和simple design。 這四個實(shí)踐里只有simple design最虛也最重要。有一個問題已經(jīng)被問過無數(shù)次了,“到底多simple的design才叫simple”。我對此也有一個近乎刻板的回答:team里所有人員都能夠理解的design就是simple的。一旦立了標(biāo)準(zhǔn),這四個實(shí)踐的主從關(guān)系就一下子清晰起來——simple design是這四個實(shí)踐的核心,其他三個實(shí)踐都是它服務(wù)的。 首先做出一個設(shè)計,最簡單的判斷標(biāo)準(zhǔn)就是是否可測,一個不可測的設(shè)計基本上可以認(rèn)為無法實(shí)現(xiàn),于是TDD即是simple design的質(zhì)量保證又是simple design的直覺驗(yàn)證。 refactor是為了得到好的代碼,那么什么是好的代碼?simple design!!!這里有人不同意了,有人說只是要易于修改和擴(kuò)展,可是擴(kuò)展和修改也要別人看得懂才行啊...simple design是起碼的要求嘛。實(shí)際上,XP中的refactor就是朝著simple design的方向重構(gòu)過去的,也就是朝著所有人都能理解的代碼refactor過去的。插一句題外話,為啥說好的架構(gòu)的不是設(shè)計出來的呢?因?yàn)楹玫募軜?gòu)至少應(yīng)該是simple design的,而simple的概念有和人員相關(guān)...所以當(dāng)你極盡能事show off你的pattern知識之后,得到復(fù)雜設(shè)計根本就不可能是好的架構(gòu)。時刻緊記,架構(gòu)是妥協(xié)啊... 最后,pair programming是simple design的實(shí)際檢驗(yàn)!!!因?yàn)榧幢闶亲顝?fù)雜的設(shè)計,只要是你自己想出來的,你都覺得它簡單無比,里面充滿了直白且顯而易見的理由。可惜不幸的是,我們要的簡單,是對team里所有人的簡單。如果你的pair不能理解你的設(shè)計,那么說明你的設(shè)計復(fù)雜了;如果你們兩個人懂,但是swith pair的時候,換過來的人不懂,說明你的設(shè)計復(fù)雜了。pair programming(以及他那容易讓人忽略的子實(shí)踐switching pair)就是檢驗(yàn)simple design的過程。pair programing + refactor就是時刻保證simple design防止過渡設(shè)計反攻倒算的過程。pair programming + refactor + tdd就是團(tuán)結(jié)在以Deming同學(xué)built quality in的質(zhì)量大旗下,堅定地與過渡設(shè)計做斗爭的過程。據(jù)我觀察,至沒有使用pair programming的團(tuán)隊(duì)中,少一半simple design成了口號,而這一半中,至少又有一半最終放棄了xp放棄了敏捷(俺以前帶過的團(tuán)隊(duì)就有這樣的...默哀一下)。深刻的教訓(xùn)啊,我們來高呼一下:"pair programming是檢驗(yàn)simple design的唯一標(biāo)準(zhǔn)!"。 最后說一下pair programming經(jīng)濟(jì)學(xué),過多的假設(shè)我就不講了。單說一點(diǎn),有哪一位上班的8小時從來不上msn/yahoo/qq等im?有哪一位上班從來不上論壇/不回貼/不發(fā)郵件?以我pair的經(jīng)驗(yàn)來看,pair programming的過程中,兩個人幾乎不會用im,幾乎不會逛論壇。你不好意思呀,畢竟不是你一個人的機(jī)器,畢竟是兩個人的時間,畢竟你也不愿意給同事一種懶散的印象吧?收回的這么浪費(fèi)的時間,至少頂?shù)眠^另外一個人的工作時間了吧?
想要理解敏捷軟件開發(fā)為什么好,需要從軟件質(zhì)量講起。那么軟件的質(zhì)量是什么?這個問題有很多中答案,我們不妨想看看傳統(tǒng)質(zhì)量理論對于質(zhì)量是如何理解的。教科書上說,在20世紀(jì)質(zhì)量管理的發(fā)展歷程經(jīng)歷了質(zhì)量檢驗(yàn)、統(tǒng)計質(zhì)量控制和全面質(zhì)量管理三個階段。其中,質(zhì)量理念也在不斷的演變。據(jù)說有這么幾個階段:
符合性質(zhì)量 20世紀(jì)40年代,符合性質(zhì)量概念以符合現(xiàn)行標(biāo)準(zhǔn)的程度作為衡量依據(jù),“符合標(biāo)準(zhǔn)”就是合格的產(chǎn)品質(zhì)量,符合的程度反映了產(chǎn)品質(zhì)量的水平。比如說我做一個杯子,沒什么特別的要求,也不是我神經(jīng)質(zhì)的藝術(shù)作品,就是普普通通的一個杯子,那么需要高矮長短,大小胖瘦,等等一干質(zhì)量屬性,我做好了可以拿著質(zhì)量標(biāo)準(zhǔn)來對比,一眼就可以看出那里出了什么問題。通過是否符合這些標(biāo)準(zhǔn)判斷產(chǎn)品具有相應(yīng)的質(zhì)量。 那么軟件的質(zhì)量理是不是符合性質(zhì)量呢?我個人覺得不屬于。雖然我們一樣可以拿出各種各樣的標(biāo)準(zhǔn),比如故障率,比如bug數(shù)等等。但是這些標(biāo)注都滿足確不一定是好的軟件,比如我寫一個helloworld,雖然他可以沒有bug。但是卻發(fā)揮不了任何的作用。這樣的軟件就屬于“高質(zhì)量”的廢品。正如趙辛梅評價方鴻漸,“你不討厭,但是毫無用處。”,顯然毫無用處的軟件不會是真正高質(zhì)量的軟件。
適用性質(zhì)量 20世紀(jì)60年代,適用性質(zhì)量概念以適合顧客需要的程度作為衡量的依據(jù),從使用的角度定義產(chǎn)品質(zhì)量,認(rèn)為質(zhì)量就是產(chǎn)品的“適用性”。是“產(chǎn)品在使用時能夠成功滿足用戶需要的程度”。質(zhì)量涉及設(shè)計開發(fā)、制造、銷售、服務(wù)等過程,形成了廣義的質(zhì)量概念。適用性質(zhì)量的例子也很多,比如我買了一件Givenchy西服(我還真買了一件),但是一時又沒有特別正是的場合(目前還真沒有什么正式的場合),于是我一天四頓牛排(其實(shí)只有一頓),于是就吃胖了,這件華麗的Givenchy就穿不上了。那么這件衣服從符合性質(zhì)量來說,是優(yōu)質(zhì)品,但是從適用性質(zhì)量來說,卻不是一個高質(zhì)量的產(chǎn)品——因?yàn)槲掖┎簧稀_€有一句話,叫甲之熊掌乙之砒霜。也是適用性質(zhì)量的標(biāo)準(zhǔn)體現(xiàn)。 那么軟件的質(zhì)量是不是適用性質(zhì)量呢?我個人覺得,軟件的質(zhì)量至少是適用性質(zhì)量。軟件,尤其是定制軟件/企業(yè)軟件,就是量體裁衣。軟件的基本質(zhì)量就是要在用戶使用的過程中發(fā)揮價值,支撐客戶的業(yè)務(wù)發(fā)展。 書上說,從“符合性”到“適用性”,反映了人們在對質(zhì)量的認(rèn)識過程中,已經(jīng)開始把顧客需求放在首要位置。但是它沒說怎么才能做到把客戶需求放到首要位置。我看光靠文檔是堆不出來的,光考說說也是不行的。這個后面講,戴明同學(xué)比我講得好。
滿意性質(zhì)量 20世紀(jì)80年代,質(zhì)量管理進(jìn)入到TQM階段,將質(zhì)量定義為“一組固有特性滿足要求的程度”。它不僅包括符合標(biāo)準(zhǔn)的要求,而且以顧客及其他相關(guān)方滿意為衡量依據(jù),體現(xiàn)“以顧客為關(guān)注焦點(diǎn)”的原則。這個的最典型的例子是麥當(dāng)勞,他所有的店鋪從風(fēng)格到食物都保持在同一水平,使你無論在那里,都可以得到一定的購物體驗(yàn)。也就構(gòu)成了對麥當(dāng)勞的滿意性質(zhì)量的驗(yàn)證。這個軟件上也是有例子的,內(nèi)舉不必親,ThoughtWorks大多數(shù)項(xiàng)目都可以達(dá)到“滿意性質(zhì)量”,呵呵誰讓俺們是consultant涅。 我隱約覺得滿意性質(zhì)量應(yīng)該是一個過程的質(zhì)量,而不僅僅是軟件的質(zhì)量,但是目前沒有好的想法,暫且按下不表。
卓越質(zhì)量 ......下略100字。個人覺得大多數(shù)軟件還沒有達(dá)到適用性質(zhì)量,大多是過程也都沒有達(dá)到滿意性質(zhì)量,卓越質(zhì)量就先不說了吧。
總之,我們大體的認(rèn)為軟件質(zhì)量主要是適用性質(zhì)量起碼是不會錯的。那么怎么才能達(dá)到這個質(zhì)量標(biāo)準(zhǔn)涅?俺是做軟件的,質(zhì)量管理還是看看Deming同學(xué)怎么說吧,不過他老人家的14點(diǎn)總是發(fā)生變化。我也只好斷章取義,說說一個敏捷開發(fā)人員眼中的14原則:
1. 持之以恒地改進(jìn)產(chǎn)品和服務(wù) Create constancy of purpose for improvement of product and service
這個很明顯嘛,small release,快速發(fā)布,每次發(fā)布都是對產(chǎn)品的持續(xù)改進(jìn)。
2.采用新的觀念 Adopt the new philosophy
敏捷啊...
3.停止依靠大規(guī)模檢查去獲得質(zhì)量 Cease dependence on mass inspection
這個還有另一個說法,build quality in。TDD,QA/BA全程參與,都是build quality in的好方法。
4.結(jié)束只以價格為基礎(chǔ)的采購習(xí)慣 End the practice of awarding business on the basis of price tag alone
這個...貌似是說請咨詢吧...
5.持之以恒地改進(jìn)生產(chǎn)和服務(wù)系統(tǒng) Improve constantly and forever the system of production and service
這個是敏捷過程的持續(xù)改進(jìn),對應(yīng)的實(shí)踐大家可能比較陌生——Restrospective!!!
6.實(shí)行崗位職能培訓(xùn) Institute training on the job
Pair Programming,Learning Lunch敏捷從來都不缺乏學(xué)習(xí)的機(jī)會,就看你有沒有學(xué)習(xí)的動力了。
7. 建立領(lǐng)導(dǎo)力企業(yè)管理 Institute leadership
敏捷團(tuán)隊(duì)的終極目標(biāo),自組織團(tuán)隊(duì),的管理是也。
8. 排除恐懼 Drive out fear
XP第一原則,勇氣,不要恐懼。
9. 打破部門之間的障礙 Break down barriers between staff areas
只有開發(fā)團(tuán)隊(duì)的敏捷不是真正的敏捷,敏捷說到底,是將軟件的供求關(guān)系從合約型轉(zhuǎn)為合作型,本來就要是大破障礙。而且障礙不打破,就很難將敏捷實(shí)施到底。這也是很多同學(xué)嘗試敏捷失敗的原因,僅僅以為敏捷是技術(shù)層面上的事情,其實(shí)不是。從這個角度來所,敏捷方法的確是深刻而震撼心靈的變革,有些人...呃...敏捷在十月...
10. 取消對員工的標(biāo)語訓(xùn)詞和告誡 Eliminate slogans, exhortations, and targets for the work force
恩,什么激情100天...封閉開發(fā)...見鬼去吧...不過restrospective的結(jié)果是要寫在白板上的,準(zhǔn)備時刻改進(jìn)。自我表揚(yáng)和自我批評,算不上訓(xùn)詞吧。
11.取消定額管理和目標(biāo)管理 Eliminate numerical quotas for the work force. Eliminate management by objectives
很多人都問過我,pair programming了之后,技校怎么辦?嘿嘿,Deming同學(xué)已經(jīng)說了,這樣的考核不要也罷。
12 消除打擊員工工作情感的考評 Remove barriers that rob the hourly worker of his right to pride of workmanship. Remove barriers that rod people in management and in engineering of their right to pride of workmanship
敏捷團(tuán)隊(duì)的自我評價很簡單,360度,由于你幾乎跟所有人都pair過,如果所有人都不說你好...這已經(jīng)是rp問題了,就不是打擊這么簡單了...
13 鼓勵學(xué)習(xí)和自我提高? Encourage education and self-improvement for everyone
同前,Pair Programming,Learning Lunch敏捷從來都不缺乏學(xué)習(xí)的機(jī)會,就看你有沒有學(xué)習(xí)的動力了。
14 采取行動實(shí)現(xiàn)轉(zhuǎn)變 Take action to accomplish the transformation
每次restrospective之后必須定出方案,以實(shí)踐改進(jìn)。而諸位如果想實(shí)施敏捷又覺得難于說服領(lǐng)帶,不妨拿Deming同學(xué)說說事,這位大老的殺傷力還是曼大的,尤其你老大是MBA的話
很長時間以來我對rails框架本身沒什么興趣,因?yàn)槲覐膩矶疾皇莣eb fans,15歲那年我打印了一本HTML Reference準(zhǔn)備學(xué)習(xí)一下,感覺完全nonsense。很長一段時間(大約有3年吧)里基本上看到Markup Language我就會大腦短路。但是我對rails背后的東西很感興趣,是什么讓rails如此有效率,一開始我以為是Ruby DSL,但隨著做的rails項(xiàng)目越來越多,我發(fā)現(xiàn)rails的效率的根源來自它對delphi的繼承和發(fā)揚(yáng)。 1.? data centric object model Active Record delphi之所以成功,在于它看準(zhǔn)了大部分商用軟件都是數(shù)據(jù)庫核心的,并為之設(shè)計一套相應(yīng)的框架, delphi的核心就是圍繞數(shù)據(jù)庫進(jìn)行開發(fā)(李維同學(xué)的borland傳奇里寫道,當(dāng)時之所以起名字叫Delphi,就是因?yàn)橐猼aking to Oracle)。而rails也很好的在新時代(Web時代)把握了相似的核心點(diǎn)——content是web的核心,以及數(shù)據(jù)庫數(shù)據(jù)到content的映射是動態(tài)web的核心。因此它沒有采用所謂的更加嚴(yán)肅的ORM技術(shù)。而是依然使用了delphi時代的active record(但是進(jìn)行了object封裝)。如下代碼示范了ruby和delphi在active record實(shí)現(xiàn)上的相似 Ruby class?Person?<?ActiveRecord::Base end
x8x?=?Person.new?:name?=>?'x8x' x8x.age?=?15 Delphi people?:?TADOTable;
 begin??? ???people.Table?=?'people'; ???people.InsertRecord('x8x');
???people.First; ???people.FieldByName('age')?:=?15; end 可以看出,Delphi的數(shù)據(jù)庫味道更濃一些,而ruby則更多使用了對象的視角(我記得在98年前后(我變成oo狂熱分子之后),在寫delphi的時候我基本上不會直接使用Table對象了,而是做一些簡單的封裝,用business object代替直接的數(shù)據(jù)庫對象。但是最后由于和delphi的ui組件結(jié)合的不好而放棄)。但是active record的pattern是一致的。 2. DB(resource)-aware UI component —— Action View Delphi另一個為人稱道的地方是DB-Aware的UI組件,像TDBLabel, TDBMemo還有至今仍位人稱道的TDBGrid,極大的簡化了UI的開發(fā)。不過說到底,仍然是Delphi數(shù)據(jù)庫核心策略的延續(xù)。同樣,rails的view helper也是db核心的。text_field之類的可以自動感知active record的內(nèi)容和錯誤。 <label>Name:</label>?<%=?text_field?'person',?'name'?%> 和 nameLabel?:?TDBLabel;
nameLabel.DataSource?=?peopleTable; nameLabel.Field?=?'name'; nameLabel.Label?=?'Name';
拋開Desktop和web的差異,也可以算是大致相當(dāng)吧。 3. Simple Component Model —— Plan Object as Component Delphi是基于組件開發(fā),并且是非常成功的一個組件模型。基本上有經(jīng)驗(yàn)的delphi程序員,都會在開發(fā)過程中抽象幾個VCL component出來簡化自己的開發(fā)。一方面是DRY精神,另一方面Delphi簡單的組件模型使得這樣做的代價非常的小。Delphi的組件基本上就是一個對象,重構(gòu)的過程中修修改改就成組件了。rails其實(shí)有類似的機(jī)制,而且更簡單直接,更符合web時代的胃口,一個對象外加一個helper就可以成為一個UI組件。與此同時rails還有另外一個天然的同盟——Ruby DSL。把以前Delphi需要用UI設(shè)計器的地方都用Ruby DSL代替了。這一點(diǎn)是我最近在用rails做曹老師華麗的RedSaga的時候推行DSL geek主義時發(fā)現(xiàn)的。比如現(xiàn)在我有這樣一個tiny DSL用以定義portlet: in controller @portlet?=?Portlet.new?do ????????????name?'administration' ????????????title?'Administration' ????????????tabs?do ????????????????Projects?:controller?=>'projects',?:action?=>?'list' ????????????end ????????end in view <%=?render_portlet?@portlet?%> 這種描述/configuration block風(fēng)格的dsl與delphi組件的初始化非常相似,或者可以說,只有語法上的差異而無思路上的差異(當(dāng)然delphi可以借助IDE而不是語言來指定這些,但是這個做法是沒有生產(chǎn)力的)。 with?Portlet?do ???Label?=? ???Name?=? . ???Tabs[0].Controller?=? ???Tabs[1].Action?=? end rails和delphi這種輕量的組件模型,使得構(gòu)建組件/復(fù)用組件級的代價極小。因此可以極大的提高開發(fā)效率(我至今仍記得,01年前后接了一個Delphi私活,客戶要求Office XP菜單風(fēng)格,我找了一個XPMenu的控件,直接仍上去,自己的代碼一行沒改,菜單就Office XP了...)。 總之,Delphi的效率要素Rails大部分都學(xué)走了,最后簡單總結(jié)一下rails在delphi基礎(chǔ)上的發(fā)揚(yáng): 1. 用ruby而不是object pascal。語法上更靈活簡單 2. Object Model on top of ActiveRecord,比起Delphi可以跟好的使用OO開發(fā)。 3. 組件模型更簡單 4. CoC,這個就不說了 5. expressive Ruby DSL 最后最后,說一下Delphi for PHP,做得很華麗。但是UI部分我不是很喜歡。
I got a Sony PSP recently. After playing with it for a while, I
found out that, comparing to other handheld game console, PSP has a
fairly open platform. Some great guys had already customized a gcc
compiler for PSP, and also, a simple toolchain is provided by the
active community to support porting/development/debuging/hacking. So I
decided to port some of my favourite programming langauges to PSP. Of
course, the first one on my list is Ruby.
Cross compiling,
reading psp documents, reading ruby source code, hacking ruby source
code, cross compiling... after 3 days hard working, finally, I managed
to make the following ruby script(which is listed in the book
<Programming Ruby>) running on my PSP:
def say_goodnight(name)
"Good night, #{name}"
end
puts say_goodnight("PSP")
Bingo~~~! Ruby goes entertainment!!!!
btw : my ruby-psp patch could be found here:
https://rubyforge.org/tracker/index.php?func=detail&aid=8134&group_id=426&atid=1700
make sure you have psp-gcc and toolchain installed on you PC, and 3.03-OE/1.5 fireware on your PSP.
今天被老莊拉到JavaEye扯皮,扯來扯去還是lambda演算...本來應(yīng)承了老莊寫lambda演算簡介,不過看到磐石T1同學(xué)提到了Church number來勾引whl同學(xué)...于是我想還是寫一些更有意思的東西吧。 每個Church number都是一個接受兩個參數(shù)的函數(shù),這兩個參數(shù)又都是函數(shù),第一個參數(shù)稱為后繼函數(shù),第二個參數(shù)則叫做零點(diǎn)函數(shù)。依據(jù)這兩個函數(shù),我們可以定義Church number zero, one, two: (define zero? (lambda (successor?zero)?zero)) (define one (lambda (successor?zero)?(successor?zero))) (define two?? (lambda (successor?zero)?(successor?(successor?zero))))
可以看出,所謂one就是對零點(diǎn)函數(shù)應(yīng)用一次后繼函數(shù),而two則是對零點(diǎn)函數(shù)應(yīng)用后繼函數(shù)的結(jié)果再次應(yīng)用后繼函數(shù),依次類推可以得到Church Number n。下面我們可以通過后繼函數(shù)increase和零點(diǎn)函數(shù)f(x) = 0來看看這些Church Number的計算結(jié)果: (define?(increase?x)?(+?x?1))
(zero increase 0) > 0 (one increase 0) >1 (two increase 0) >2
an approximate Java version:
public interface Function<T> { ??? T apply(Object... parameters); }
public interface ChurchNumber { ??? Integer apply(Function<Integer> successor, Function<Integer> zero); }
ChurchNumber zero = new ChurchNumber() { ?? public Integer apply(Function<Integer> successor,? Function<Integer> zero) { ????? return zero.apply(); ?? } };
ChurchNumber one = new ChurchNumber() { ?? public Integer apply(Function<Integer> successor, Function<Integer> zero) { ????? return successor.apply(zero); ?? } };
ChurchNumber two = new ChurchNumber() { ?? public Integer apply(Function<Integer> successor, Function<Integer> zero) { ? ? ? return successor.apply(successor.apply(zero)); ?? } };
Function increase = new Function<Integer>() { ?public Integer apply(Object... parameters) { ?? if (parameters[0] instanceof Function) { ????? return ((Function<Integer>) parameters[0]).apply() + 1; ?? } ?? return (Integer) parameters[0] + 1; ?} };
Function numberZero = new Function<Integer>() { ?? public Integer apply(Object... parameters) { return 0;} };
System.out.println(zero.apply(increase, numberZero)); >0 System.out.println(one.apply(increase, numberZero)); >1 System.out.println(two.apply(increase, numberZero)); >2
定義了Church number后,我們繼續(xù)定義Church number上的運(yùn)算,首先是增加1: (define?(inc?x)?(lambda (successor?zero) (successor (x successor zero))))
(define three (inc two)) (three increase 0) >3
an approximate Java version:
static ChurchNumber inc(final ChurchNumber churchNumber) { ?? return new ChurchNumber() { ????? public Integer apply(Function<Integer> successor, Function<Integer> zero) { ??? ???? return successor.apply(churchNumber.apply(successor, zero)); ??? ?? } ?? }; }
ChurchNumber three = inc(two); System.out.println(three.apply(increase, numberZero)); >3
然后是加法: (define?(add?x y)?(lambda (successor?zero)? (x successor (y successor zero))))
(define five (add three two)) (five increase 0) >5
an approximate Java version:
static ChurchNumber add(final ChurchNumber x, final ChurchNumber y) { ??? ??? return new ChurchNumber() { ??? ??? ??? public Integer apply(final Function<Integer> successor, ??? ??? ??? ??? ??? final Function<Integer> zero) { ??? ??? ??? ??? return x.apply(successor, new Function<Integer>() { ??? ??? ??? ??? ??? public Integer apply(Object... parameters) { ??? ??? ??? ??? ??? ??? return y.apply(successor, zero); ??? ??? ??? ??? ??? } ??? ??? ??? ??? }); ??? ??? ??? } ??? ??? }; }
ChurchNumber five = add(two, three); System.out.println(five.apply(increase, numberZero)); >5
最后是乘法: (define?(multiply?x?y) (lambda (successor?zero)? (x? (lambda (z) (y successor z)) zero)))
(define four (multiply two two)) (four increase 0) >4
an approximate Java version:
static ChurchNumber multiply(final ChurchNumber x, final ChurchNumber y) { ??? ??? return new ChurchNumber() { ??? ??? ??? public Integer apply(final Function<Integer> successor, ??? ??? ??? ??? ??? Function<Integer> zero) { ??? ??? ??? ??? return x.apply(new Function<Integer>() { ??? ??? ??? ??? ??? public Integer apply(final Object... parameters) { ??? ??? ??? ??? ??? ??? return y.apply(successor, new Function<Integer>() { ??? ??? ??? ??? ??? ??? ??? public Integer apply(Object... ignoredParameters) { ??? ??? ??? ??? ??? ??? ??? ??? if (parameters[0] instanceof Function) { ??? ??? ??? ??? ??? ??? ??? ??? ??? return ((Function<Integer>) parameters[0]).apply(); ??? ??? ??? ??? ??? ??? ??? ??? } ??? ??? ??? ??? ??? ??? ??? ??? return (Integer) parameters[0]; ??? ??? ??? ??? ??? ??? ??? } ??? ??? ??? ??? ??? ??? }); ??? ??? ??? ??? ??? } ??? ??? ??? ??? }, zero); ??? ??? ??? } ??? ??? }; }
ChurchNumber four = multiply(two, two); System.out.println(four.apply(increase, numberZero));
沒有減法和除法,Church當(dāng)年發(fā)明這套東西的時候就沒有。原因是非常明顯的...因此Church number只有后繼函數(shù),而沒有前驅(qū)函數(shù)。也就是說Church number只能往前數(shù)...不能望后數(shù)...自然不可能作出減法和除法了。當(dāng)然擴(kuò)展一下也是非常容易的: (define?negative-one?(lambda?(successor?precursor?zero)?(precursor?zero))) (define?one?(lambda?(successor?precursor?zero)?(successor?zero)))
(define?(add?x?y)?(lambda?(successor?precursor?zero)?(x?successor?precursor?(?y?successor?precursor?zero)?)))
(define?(inc?x)?(+?x?1)) (define?(dec?x)?(-?x?1))
(define?zero?(add?one?negative-one))
(zero?inc?dec?0) >0
whl同學(xué)問這樣能不能實(shí)現(xiàn)浮點(diǎn),答案是可以實(shí)現(xiàn)有限精度的浮點(diǎn)數(shù)....因?yàn)榘凑者@個思路發(fā)展下去,我們定義浮點(diǎn)的successor和precursor函數(shù)只能在有限的位數(shù)之內(nèi)...當(dāng)然有了one,zero再結(jié)合pair,模擬0/1存儲實(shí)現(xiàn)浮點(diǎn)也不是不可能的事情...
1. Never use Selenium FIT mode
Selenium分為兩種運(yùn)行模式,Driven Mode(現(xiàn)在叫Selenium Remote Control)和FIT Mode(現(xiàn)在叫Selenium Core)。
FIT Mode顧名思義,就是類似FIT Testing Framework那種使用方式,主要用于QA等非技術(shù)人員編寫Web應(yīng)用的功能測試。FIT Mode的Selenium測試使用HTML來組織測試用例。例如我要測試一個web應(yīng)用的登陸功能。我可能寫出這樣的HTML 表格。
?1
<
table
>
?2
<
tr
>
?3
?
<
td
>
open
</
td
>
?4
????????
<
td
>
http://localhost:8080/login
</
td
>
?5
????????
<
td
></
td
>
?6
</
tr
>
?7
<
tr
>
?8
?
<
td
>
type
</
td
>
?9
????????
<
td
>
id=username
</
td
>
10
????????
<
td
>
someuser
</
td
>
11
</
tr
>
12
<
tr
>
13
?
<
td
>
type
</
td
>
14
????????
<
td
>
id=password
</
td
>
15
????????
<
td
>
password
</
td
>
16
</
tr
>
17
<
tr
>
18
?
<
td
>
click
</
td
>
19
????????
<
td
>
id=login_button
</
td
>
20
????????
<
td
></
td
>
21
</
tr
>
22
<
tr
>
23
?
<
td
>
assertTextPresent
</
td
>
24
????????
<
td
>
Welcome?to?xxxx
</
td
>
25
????????
<
td
></
td
>
26
</
tr
>
27
</
table
>
不同于FIT,Selenium內(nèi)置了一系列的命令,如上例中的open, type, click以及assertTextPresent,因此QA可以完全拋開DEV獨(dú)立地編寫測試(FIT需要DEV提供Behavior Fixture)。因此FIT Mode是相當(dāng)容易使用的,哪怕不會使用HTML的QA,也可以使用FrontPage畫出三列表格,依次填入數(shù)據(jù)。
然而對于大多數(shù)team而言——尤其是敏捷team,F(xiàn)IT Mode平易的外表下是令人恐懼的泥沼。大多數(shù)團(tuán)隊(duì)往往選擇使用Selenium作為功能測試和集成測試工具而不僅僅是QA測試工具,在不同的迭代間遇到功能流程或UI變化時,必須要重構(gòu)Selenium測試,或者說,F(xiàn)unctional Test Migration。令人遺憾的是,HTML based的Selenium FIT Testing的重構(gòu)竟然令人難以置信的困難。我們可以使用include等Selenium FIT擴(kuò)展,使得它可以重用詳細(xì)的功能(Log in, Log out諸如此類)。即便如此,在一個真實(shí)的項(xiàng)目中,Selenium Test的數(shù)量往往在200-500之間(我目前所處的項(xiàng)目在改用Driven Mode前已達(dá)350+),對于這么大基數(shù)的Selenium測試,手工重構(gòu)幾乎是不可想象的,而目前尚沒有HTML代碼重構(gòu)工具。即便存在泛泛意義上的HTML重構(gòu)工具,對于Selenium測試重構(gòu)的有效性尚待商榷。而使用Driven Mode上述代碼可以寫為:
1
public
?
void
?testShouldShowAWeclomeMessageAfterUserLoggedIn()?
{
2
????selenium.open(
"
http://localhost:8080/login
"
);
3
????selenium.type(
"
id=username
"
,
"
someuser
"
);
4
????selenium.type(
"
id=password
"
,?
"
password
"
);
5
????selenium.click(
"
id=login_button
"
);
6
????assertTrue(selenium.isTextPresent(
"
Welcome?to?xxxx
"
));
7
}
很自然,一個訓(xùn)練有素的程序員會重構(gòu)出如下代碼:
?1
public
?
void
?login(String?username,?String?password)?
{
?2
????selenium.open(
"
http://localhost:8080/login
"
);
?3
????selenium.type(
"
id=username
"
,username);
?4
????selenium.type(
"
id=password
"
,?password);
?5
????selenium.click(
"
id=login_button
"
);?
?6
}
?7
?8
public
?
void
?testShouldShowAWeclomeMessageAfterUserLoggedIn()?
{
?9
????login(
"
someuser
"
,?
"
password
"
);
10
????assertTrue(selenium.isTextPresent(
"
Welcome?to?xxxx
"
));
11
}
之后無論是pull up到公共基類還是extact到Utils class都是很容易的事情。由于Java在代碼重構(gòu)上便利,Java Selenium Remote Control成為使用Selenium的最佳方式。在這一點(diǎn)上,縱使Ruby語法上比Java簡單靈活得多,它仍不是編寫Selenium測試的最佳載體(當(dāng)然一個經(jīng)過精心設(shè)計的ruby selenium dsl wrapper還是具有非凡的價值的,這個我們后面會涉及到)。
2. Using the name user, system, page instead of selenium
觀察上面提到的代碼,其中使用selenium來操縱web應(yīng)用的行為,這在Remote Control里是常見的做法,但是仍然不夠好,我們可以做一些小的變化以得到更好的測試:
?1
protected
?
void
?setup()?
{
?2
????selenium?
=
? ?
//
?intialize?selenium?instance
?3
????user?
=
?selenium;
?4
????currentPage?
=
?selenium;
?5
}
?6
?7
public
?
void
?login(String?username,?String?password)?
{
?8
????user.open(
"
http://localhost:8080/login
"
);
?9
????user.type(
"
id=username
"
,username);
10
????user.type(
"
id=password
"
,?password);
11
????user.click(
"
id=login_button
"
);?
12
}
13
14
public
?
void
?testShouldShowAWeclomeMessageAfterUserLoggedIn()?
{
15
????login(
"
some?guy
"
,?
"
password
"
);
16
????assertTrue(currentPage.isTextPresent(
"
Welcome?to?xxxx
"
));
17
}
基本上這只不過是"另一種寫法"而已,但是它更好的表達(dá)了"用戶的行為",如login代碼所示。以及"系統(tǒng)的正確相應(yīng)",即currentPage.isTextPresent()。這種是典型的對編譯器無意義對人有意義的代碼,也就是普遍意義上好的代碼。
3. Creating a DSL base on your test codes
懂得HTML的QA可以在沒有DEV的幫助下使用Selenium FIT mode,然而卻不能在沒有DEV的幫助下使用Driven Mode。于是最自然也是最fashion的做法,就是在已有的test codes之上提供Testing DSL或者Scripting Language,讓FIT mode變得更加FIT。這方面內(nèi)容是一個更大的主題,以后再詳細(xì)展開吧。
4. Hacking Selenium Object to support FIT command
Selenium FIT mode和RC mode下的命令有些許差異,比如FIT中的assertTextPresent,在RC中變成了isTextPresent。同樣還有FIT中最實(shí)用的命令clickAndWait,在RC中變成了click和waitForPageToLoad。在RC中使用FIT mode中的命令也非難事,找到com.thoughtworks.selenium.Selenium,添加方法:
public
?
void
?doCommand(String?commmand,?String ?parameters);
然后在com.thoughtworks.selenium.DefaultSelenium中添加實(shí)現(xiàn):
1
public
?
void
?doCommand(String?commmand,?String ?parameters)?
{
2
???String[]?paras?
=
?
new
?String[]
{
""
,
""
,
""
}
3
???
for
?(
int
?i?
=
?
0
;?i?
<
?parameters.length?
&&
?i?
<
?
3
;?i
++
)
4
??????paras[i]?
=
?parameters[i];
5
???commandProcessor.doCommand(command,?paras);
6
}
然后試驗(yàn)一下:
selenium.doCommand(
"
clickAndWait
"
);
在我們使用純RC mode之前曾經(jīng)用過一段中間方案,將rc code轉(zhuǎn)化為fit code來跑(因?yàn)閞c不支持https),由于不是真正的rc mode,像isTextPresent之類的方法都沒有辦法使用,只能使用FIT mode command。因此如果因?yàn)橐恍┨厥獾脑?https, chrome起不來,hta bug多等等),你沒有辦法使用RC mode,但是有希望得到RC可重構(gòu)的好處,那么這個tricky的技巧倒是不錯的選擇。
5. Using chrome and IE hta lanucher to support https 6. Run test using different browser lanucher to test browser compatibility
這兩個都是和browser lanucher相關(guān)的,Selenium和JWebUnit最大的不同在于它使用真實(shí)的瀏覽器來跑測試,從而可以更加真實(shí)地考察系統(tǒng)在不同瀏覽器中的表現(xiàn)。因此使用不同的瀏覽器lanucher來運(yùn)行測試,可以更好測試應(yīng)用的瀏覽器兼容性,這對于web 2.0應(yīng)用而言是很有幫助的。此外,使用rc提供的試驗(yàn)性lanucher,chrome和hta可以解決跨domain測試和https的問題。不過目前hta還是有很多bug的,推薦使用chrome。當(dāng)然,最希望的還是澳洲的同事可以早日在selenium里提供https支持。
摘要: 參加ThoughtWorks University的一個來月沒啥事情,閑了寫寫compiler玩。發(fā)現(xiàn)Lexer部分比較基礎(chǔ)也比較常用,有很多相似的東西,每次都要寫一遍也太麻煩了,下面是我按著JSL寫的一個common java-like lexer,對于大多數(shù)接近java語法的語言估計是夠用了。BTW:這個Lexer定義是TDD出來,以通過測試為要務(wù),可能可讀性不太強(qiáng)。1.WhiteSpaceC... 閱讀全文
Watir doesn't work well with chinese characters. Try the following codes. ie.text_field(:name, 'some text field).set('某某') It will highlight the text field but put nothing in it. I read the Watir source codes, and found an interesting code segment: 1 for i in 0 .. value.length-1 2 sleep @ieController.typingspeed # typing speed 3 c = value[i,1] 4 #@ieController.log " adding c.chr " + c #.chr.to_s 5 @o.value = @o.value.to_s + c #c.chr 6 fire_key_events 7 end The above codes show how Watir simulates typing.If it doesn't work well with chinese characters, There must be something wrong with Ruby string. The first order of business is to figure out how Ruby string works for Chinese string. 1 chineseString = '某某' 2 puts chineseString.length 3 for i in 0..chineseString.length-1 4 puts chineseString[i, 1] 5 end result will be: 4
Does Ruby, which is now capturing all java programmers' love, use 8bit char instead of unicode? Holy fuck! I made a simple patch for the issue after I woke up from a short coma. 1 require 'Watir' 2 3 module Watir 4 module Cn 5 class IE <Watir::IE 6 def text_field(how , what=nil) 7 return TextField.new(self, how, what) 8 end 9 end 10 11 class TextField < Watir::TextField 12 def doKeyPress( value ) 13 begin 14 maxLength = @o.maxLength 15 if value.length > maxLength 16 value = suppliedValue[0 .. maxLength ] 17 @ieController.log " Supplied string is #{suppliedValue.length} chars, which exceeds the max length (#{maxLength}) of the field. Using value: #{value}" 18 end 19 rescue 20 # probably a text area - so it doesnt have a max Length 21 maxLength = -1 22 end 23 24 Cn.characters_in(value) {|c| 25 sleep @ieController.typingspeed 26 @o.value = @o.value.to_s + c 27 fire_key_events} 28 end 29 end 30 31 def Cn.characters_in(value) 32 index = 0 33 while index < value.length 34 len = value[index] > 128 ? 2 : 1 35 yield value[index, len] 36 index += len 37 end 38 end 39 end 40 end I submitted this patch to Watir tracing systsem,and zipped codes could be found here: http://rubyforge.org/tracker/index.php?func=detail&aid=3232&group_id=104&atid=489
昨天發(fā)現(xiàn)了Ruby Watir里的一個小問題,沒有辦法在Text Field里輸入中文。雖然已經(jīng)hack了,但是還是不太爽,G.H.Hardy說: Beauty is the first
test: there is no permanent place in this world for ugly mathematics. 感動了我好久。現(xiàn)在換個說法: Beauty is the first
test: there is no permanent place in this world for ugly hack code. 這個問題也不太難出理,ruby作為C的front interface在字符串處理上有很深的C的痕跡,嗯,10年前我還是個C程序員嘛,按照從前的做法區(qū)分ASCII碼。 1 def characters_in(value) 2 index = 0 3 while index < value.length 4 len = value[index] > 128 ? 2 : 1 5 yield value[index, len] 6 index += len 7 end 8 end 把TextField里的doKeyPress改一下: 1 characters_in(value) {|c| 2 sleep @ieController.typingspeed 3 @o.value = @o.value.to_s + c 4 fire_key_events} 搞定!但是還是很丑,直接把別人的code改了,contributing to eclipse里說要contribute不要隨便change別人的代碼。好吧,好在ruby擴(kuò)展起來也不難: 1 require 'Watir' 2 3 module Watir 4 module Cn 5 class IE <Watir::IE 6 def text_field(how , what=nil) 7 return TextField.new(self, how, what) 8 end 9 end 10 11 class TextField < Watir::TextField 12 def doKeyPress( value ) 13 begin 14 maxLength = @o.maxLength 15 if value.length > maxLength 16 value = suppliedValue[0 .. maxLength ] 17 @ieController.log " Supplied string is #{suppliedValue.length} chars, which exceeds the max length (#{maxLength}) of the field. Using value: #{value}" 18 end 19 rescue 20 # probably a text area - so it doesnt have a max Length 21 maxLength = -1 22 end 23 24 Cn.characters_in(value) {|c| 25 sleep @ieController.typingspeed 26 @o.value = @o.value.to_s + c 27 fire_key_events} 28 end 29 end 30 31 def Cn.characters_in(value) 32 index = 0 33 while index < value.length 34 len = value[index] > 128 ? 2 : 1 35 yield value[index, len] 36 index += len 37 end 38 end 39 end 40 end 測試一下: require 'watir-cn'
ie = Watir::Cn::IE.start('http://www.google.com') ie.text_field(:name, 'q').set('Ruby Watir 功能測試' 成功。最后一步是貢獻(xiàn)社區(qū),直接登到rubyforge,找到Watir然后submit了兩個patch:一個直接修改watir庫的一個是獨(dú)立的watir-cn的。推薦大家使用第二個的patch。地址在: http://rubyforge.org/tracker/index.php?func=detail&aid=3232&group_id=104&atid=489
|
|
隨筆:36
文章:0
評論:93
引用:0
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|
25 | 26 | 27 | 28 | 29 | 30 | 31 | 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 | 1 | 2 | 3 | 4 |
|
常用鏈接
留言簿(21)
隨筆分類
隨筆檔案
搜索
最新評論

閱讀排行榜
評論排行榜
|
|