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
当用这种格式的url: POST /teachers/autocomplete_teacher 触发了的时候,上面张贴的post语句就会被控制器调用到。 第一个断言确保了autocomplete_teacher.rhtml模板已经显示了。 下一句确保了控制器返回的是sucess。 第三句是个组合断言,从寻找<ul>标签和autocomplete_list属性的行。(请注意css语法的使用) 在标签之中,应该有个<li>标签,class属性是autocomplete_item 而且还要包括文本(1)Martin, Bob。不难猜出,当前测试所运行的环境中有一些特定数据已经预先从数据库中读取了出来。例如说,这个测试数据库的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 %> <