GUI测试是测试驱动开发的经典难题之一。很多团队在他们项目中的很多部分都采用过TDD,但却因为某种原因而无法在GUI模块中充分进行。
在这一系列的撰文中我要告诉你GUI测试其实是个可解决的问题。这些年来,TDD社区已经积累了一些工具、框架、类库或是其他的技术。这些技术让你能够完整的测试GUI代码就像测试其他模块一样。
Ruby on Rails
在Web开发世界里,没有任何社区能够像Ruby on Rails那样漂亮的解决了GUI测试问题。如果你开发一个Rails项目的话,GUI测试则是必经之路。Rails框架提供了测试应用程序方方面面所需要的工具和方法,这包括了HTML的生成到web回传页的结构。
在Rails中,web页面是通过混合了HTML和ruby代码的.rhtml文件(就像是Java和HTML混合在.jsp文件中)来实现的。不同之处在于,.rhtml文件是在运行期被翻译的,而不是像.jsp页面那样要先编译成servlet。对于Rails来说,这种机制使得在web容器之外为web页面生成HTML变得容易。实际上,web服务器都不需要运行。
这种产生HTML的便利和灵活性意味着Rails的测试框架仅仅需要为.rhtml文件内的ruby scriptlet设定值,生成HTML,然后解析HTML成一个测试能够访问的格式。
典型示例
测试程序通过一种像是xpath的语法访问HTML,并且提供了着一系列强大的断言函数。理解这一点的最佳方式就是去看看它的格式。这里有个简单的文件,叫做:autocomplete_teacher.rhtml。
<ul class="autocomplete_list">
<% @autocompleted_teachers.each do |t| %>
<li class="autocomplete_item"><%= "#{create_name_adornment(t)} #{t.last_name}, #{t.first_name}"%></li>
<% end %>
</ul>
你不用非得是Ruby程序员才能看明白上面的代码。它所作的是构建一个HTML列表。在<%和%>符号之间的Ruby 脚本语句遍历每个teacher,并在“adornment”里创建<li>标签,姓还有名。(这个adornment恰巧是与括号中的teacher在数据库中的id一致)这个.rhtml文件的一个简单测试如下:
def test_autocomplete_teacher_finds_one_in_first_name
post :autocomplete_teacher, :request=>{:teacher=>"B"}
assert_template "autocomplete_teacher"
assert_response :success
assert_select "ul.autocomplete_list" do
assert_select "li.autocomplete_item", :count => 1
assert_select "li", "(1) Martin, Bob"
end
end
不难猜出,当前测试所运行的环境中有一些特定数据已经预先从数据库中读取了出来。例如说,这个测试数据库的Teacher表中第一行(id是1)的数据总是“Bob Martin”。
assert_select函数功能强大,它能访问HTML中的大量复杂数据,同时又保持了精细的粒度。尽管在本例中你只看到了它的冰山一角,但你应该能够看出Rails的测试方案是能够让你测试.rhtml文件中的所有脚本,保证它们有正确的行为,而且从控制器中获得的数据也是正确的。
使用RSpec和行为驱动设计的例子
下面的例子更有意义,它使用了一种特殊的被称作行为驱动设计(BDD)的测试语法。而可识别词语法的工具被称作RSpec。
设想我们有一个记录不同学校老师的电话信息的页面。.rhtml页面中的一部分如下:
<h1>Message List</h1>
<table id="list">
<tr class="list_header_row">
<th class="list_header">Time</th>
<th class="list_header">Caller</th>
<th class="list_header">School</th>
<th class="list_header">IEP</th>
</tr>
<%time_chooser = TimeChooser.new%>
<% for message in @messages %>
<%cell_class = cycle("list_content_even", "list_content_odd")%>
<tr id="list_content_row">
<td id="time" class="<%=cell_class%>"><%=h(time_chooser.format_time(message.time)) %></td>
<td id="caller" class="<%=cell_class%>"><%=h person_name(message.caller) %></td>
<td id="school" class="<%=cell_class%>"><%=h message.school.name %></td>
<td id="iep" class="<%=cell_class%>"><%=h (message.iep ? "X" : "") %></td>
</tr>
<% end %>
</table>
显然,每条消息都有时间、呼叫人、学校、以及某种叫做“IEP”的布尔类型字段。我们可以用以下的RSpec来测试此.rhtml文件:
context "Given a request to render message/list with one message the page" do
setup do
m = mock "message"
caller = mock "person",:null_object=>true
school = mock "school"
m.should_receive(:school).and_return(school)
m.should_receive(:time).and_return(Time.parse("1/1/06"))
m.should_receive(:caller).any_number_of_times.and_return(caller)
m.should_receive(:iep).and_return(true)
caller.should_receive(:first_name).and_return("Bob")
caller.should_receive(:last_name).and_return("Martin")
school.should_receive(:name).and_return("Jefferson")
assigns[:messages]=[m]
assigns[:message_pages] = mock "message_pages", :null_object=>true
render 'message/list'
end
specify "should show the time" do
response.should_have_tag :td, :content=>"12:00 AM 1/1", :attributes=>{:id=>"time"}
end
specify "should show caller first and last name" do
response.should_have_tag :td, :content=>"Bob Martin", :attributes=>{:id=>"caller"}
end
specify "should show school name" do
response.should_have_tag :td, :content=>"Jefferson", :attributes=>{:id=>"school"}
end
specify "should show the IEP field" do
response.should_have_tag :td, :content=>"X",:attributes=>{:id=>"iep"}
end
end
这里不打算介绍前面的那些含有mock的初始化片断,我只想说Rspec的mock功能既强大又方便。应该来说在理解这些片断上你不会碰到太多的麻烦,况且理解那些部分也并非本例子的必要内容。有趣的内容是specifiy段落。
应该不大费尽就能看懂specify代码段。只要你看明白了之前的段落,后面的就不难了。以下就是它所作的:
第一段内容确保<td id="time">12:00 AM 1/1</td>在HTML中存在。这并非是字符串比较,而是一种语义对比。空格以及其它复杂的属性被忽略了。需要有td标签、正确的id以及内容,这段代码才能通过。
HTML的测试原则与策略
GUI测试在.jsp世界里之所以那么成问题的原因在于,那些文件中的java scriptlet经常与应用的其他部分相关联,而且又与Web容器和应用服务器有代码上的依赖。比如说,如果你在jsp中连接数据库,或是调用了Entity bean,或是其他与数据库紧密相连的结构,那样的话,你就必须拥有整个运行环境才能测试。Rails却不必如此,因为程序所依存的环境是轻量级的,灵活的而且与可以独立于web容器或是数据库连接。尽管如此,Rails应用程序也并不能像我们想象的那样保持独立性。
对于Rails,Java或是其他的web环境,原则应该是在保证.jsp,.html等脚本文件中都找不到依赖应用程序的其他部分蛛丝马迹。此外,控制逻辑的代码应该是先把数据读取到简单对象中,然后再传给脚本程序(典型的做法是把这些属性放入到HttpServletRequest或是相关对象中)。脚本程序可以变动数据的格式(比如,数据格式、金钱格式等)但是,它不应该去做那些计算、查找或是其他的业务规则以及数据库的操作。并且,它也不应该通过组件或是实体对象来访问这些内容。最好是由控制逻辑的模块做好所有的搜寻,组装与计算之后再把数据以某种完整的包装的形式递交给脚本程序。
如果你依循上述这个简单的设计原则的话,你的web页面就能脱离web运行环境而独立运行,并且你的测试程序也就能在一种更为简单友好的环境下解析并访问html页。
结论
我会在今后的blog中更多的提及有关RSpec的内容。BDD是一种非常好的语言级的测试方法,并且有着超越语言本身的强大效用。
我希望本文说服了你关于Rails社区已经解决了测试HTML页的难题。这种解决方法能够容易的应用在Java或是.NET(会在今后的blog系列中告诉你)。
显然,测试javascript,有关Web2.0的更多的复杂问题还有GTK在本方案中还并未涉足到。不过,关于javascript的解决方案将会在今后一系列的blog中进一步探讨。
最后,这些技术无法对于集成或是整个程序的运行流程进行测试,这些主题也会在后面讨论到。
我希望这篇首文是有所启示的。如果你有什么意见、建议,或甚至是怒斥,请毫不犹豫的发表评论。
(原文链接网址:http://blog.objectmentor.com/articles/2007/01/13/testing-guis-part-i-ror; Robert C. Martin的英文blog网址:http://www.butunclebob.com/ArticleS.UncleBob)
作者简介:Robert C. Martin是Object Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。他不仅是Jolt获奖图书《敏捷软件开发:原则、模式与实践》(中文版)(《敏捷软件开发》(英文影印版))的作者,还是畅销书Designing Object-Oriented C++ Applications Using the Booch Method的作者。Martin是Pattern Languages of Program Design 3和More C++ Gems的主编,并与James Newkirk合著了XP in Practice。他是国际程序员大会上著名的发言人,并在C++ Report杂志担任过4年的编辑。