def on page_type, &block page = page_type.new $selenium assert page.visible?, "not on #{page_type}" page.instance_eval &block if block_given? pageend
这个方法神秘地返回了page对象,这里是一个比较取巧的技巧。实际上,我们只想利用page != nil这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:
on LoginPage do assert_equal 'Welcome!', welcome_message login_as :name => 'xxx', :password => 'xxx'endassert on WelcomeRegisteredUserPage
除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:
def cart_empty? @driver.get_text('xpath=
') == 'Shopping Cart(0)'end
需要注意的是,虽然我们在page object里引入了Test::Unit::Asseration模块,但是并没有在断言方法里使用任何assert*方法。这是因为,概念上来讲 page object并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使page object变成针对某些场景的test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为boolean的方法。
3. Test Data
测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:
on LoginPage do login_as :name => 'userA', :password => 'password'endassert on WelcomeRegisteredUserPageregistered_user = {:name => 'userA', :password => 'password'}on LoginPage do login_as registered_userendassert on WelcomeRegisteredUserPage
测试的是同一个东西,但是显然第二个测试更好的体现了测试意图:使用一个已注册的用户登录,应该进入欢迎页面。我们看这个测试的时候,往往不会关心用户名啊密码啊具体是什么,我们关心它们表达了怎样的测试案例。我们可以通过DataFixture来实现这一点:
module DataFixture USER_A = {:name => 'userA', :password => 'password'} USER_B = {:name => 'userB', :password => 'password'} def get_user identifier case identifier when :registered then return USER_A when :not_registered then return USER_B end endend
在这里,我们将测试案例和具体数据做了一个对应:userA是注册过的用户,而userB是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改DataFixture模块就可以了,而不必修改相应的测试:
include DataFixtureDatuser = get_user :registeredon LoginPage do login_as userendassert on WelcomeRegisteredUserPage
当然,在更复杂的测试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:
def get_user identifier case identifier when :registered then return User.find '
.' endend
4.Navigator
与界面元素类似,URL也是一类易变且难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data相似,这里就不赘述了,下面是一个例子:
navigate_to detail_page_for @producton ProductDetailPage do
.end
5. Shortcut
前面我们已经有了一个很好的基础,将Selenium测试与各种脆弱且意图不明的元素分离开了,那么最后shortcut不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的helper:
def should_login_successfully user on LoginPage do assert_equal 'Welcome!', welcome_message login_as user end assert on WelcomeRegisteredUserPageend
然后是另外一个magic方法:
def given identifer words = identifier.to_s.split '_' eval "get_#{words.last} :#{words[0..-2].join '_'}"end
之前的测试就可以被改写为:
def test_should_xxxx should_login_successfully given :registered_userend
这是一种结论性的shortcut描述,我们还可以有更behaviour的写法:
def login_on page_type on page_type do assert_equal 'Welcome!', welcome_message login_as @user end enddef login_successfully on WelcomeRegisteredUserPageenddef given identifer words = identifier.to_s.split '_' eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"end
最后,测试就会变成类似验收条件的样子:
def test_should_xxx given :registered_user login_on LoginPage assert login_successfullyend
总之shortcut是一个无关好坏,只关乎想象力的东西,尽情挥洒Ruby DSL吧!
结论
Selenium是一个让人又爱又恨的东西,错误地使用Selenium会给整个敏捷团队的开发节奏带来灾难性的影响。不过值得庆幸的是正确地使用Selenium的原则也是相当的简单:
通过将脆弱易变的页面元素和测试分离开,使得页面的变化不会对测试产生太大的影响。
明确指定测试数据的意图,不在测试用使用任何具体的数据。
尽一切可能,明确地表达出测试的意图,使测试易于理解。