我的知识库

知识等于力量

« Ruby on Rails 2.0的新特性介绍异地分布式敏捷软件开发 »

Rails的矩阵化测试

这篇文章是7月份写的,但是10月份才发表。昨天看到了InfoQ的这篇介绍Rspec新特性的文章,才知道RSpec刚刚把本文所述功能实现了,所以本文那个matrix_spec插件基本废弃了。但是本文所讲主要是矩阵化测试的思想,对于RSpec中的RBehave特性的理解还是很有帮助的。

摘要

对于Rails应用的自动化测试,本文介绍了如何使用矩阵测试来提高测试覆盖率,减少编写测试代码的工作量。同时介绍了如何使用正交化方法进行代码复用。文中使用的测试工具是RSpec,但是原理上Test::Unit 同样适用。

引文

本 文作者是一名“测试驱动强迫症”患者和“调试恐惧症”患者。以TDD为荣,以不写测试为耻。以100%测试覆盖率为荣,以低测试覆盖率为耻。全面覆盖的 Test Case都有这样的特点,一方面要覆盖所有的行为,另一方面要覆盖所有的边界条件,就是说每一个边界条件和每一个行为的组合都要测试到。这就给手工编写测 试带来了巨大的挑战,如何重用和缩减测试代码是令人头痛的问题。

编写测试和编写实现代码的思维方式有很大差别。实现代码就像狙击枪,目标明确--通过当前失败的测试。测试代码就像轰炸机,对所有可能的目标进行地毯式轰炸,一旦有失败的测试用例,就是轰炸成功。轰炸机的弹药越充足,越便宜,轰炸成功的几率就越大。

如何用最廉价的方式得到最全面的测试用例呢?通过动态组合行为和边界条件,我们就可以生成所有可能的测试,从而最大限度地重用测试代码。这就是Matrix Test的思想。

矩 阵测试方法是由ZenTest的作者 Ryan Davis在今年4月提出的,并且在ZenTest 3.5.0中提供了一个Test:Unit的Matrix Test实现。矩阵测试的原理是把Test Case分解成3个正交的部分--行为,边界条件以及校验,通过矩阵来描述如何运行时生成所有可能的Test Case,从而使测试覆盖率最大化。 本文作者开源了一个针对RSpec的矩阵测试实现--matrix_spec,本文的Matrix Test是通过matrix_spec来实现的。

RSpec

RSpec是Ruby语言的新一代测试工具,跟Ruby的核心库Test::Unit相比功能上和非常接近,RSpec的优点是可以容易地编写领域特定语言(Domain Specific Language,简称DSL)。RSpec 的一个重要目标是支持Behaviour-Driven Development (BDD) , BDD是一种融合了 Test Driven Development, Acceptance Test Driven Planning和Domain Driven Design的一种敏捷开发模型。

两种测试代码的比较:

Test::Unit

assert_equal 2, post.comments.size

RSpec

post.should have(2).comments

可以看出RSpec的测试语法非常接近英语的自然语言,易于理解和维护。这种测试代码不仅仅是测试,更是用代码来撰写的Function spec。

例子

在边界条件很多的情况下,编写测试覆盖所有测试用例非常辛苦。比如对PostsController的 show 和 edit 两个 action 编写功能测试。并且需要测试两个边界条件,普通用户登录 VS 管理员登录。测试用例用文字描述如下:

  1. 拒绝普通用户编辑Post
  2. 允许管理员编辑Post
  3. 允许普通用户访问Post
  4. 允许管理员访问Post

针对上面的测试用例,很容易写出下面的测试:

describe PostsController do

integrate_views

it "should deny edit with visitor" do
login_as :visitor # visitor is a readable user
get :edit, :id => posts(:matrix).id
#start some assertion for deny a readable user
flash[:error].should == 'You have no permission to edit'
response.should redirect_to(login_path)
end

it "should allow edit with admin" do
login_as :admin
get :edit, :id => posts(:matrix).id
#start some assertion for edit
response.should render_template(:edit)
end

it "should allow show with visitor" do
login_as :visitor # visitor is a readable user
get :show, :id => posts(:matrix).id
#start some assertion for show
response.should render_template(:show)
end

it "should allow show with admin" do
login_as :admin # admin is a readable user
get :show, :id => posts(:matrix).id
#start some assertion for show
response.should render_template(:show)
end

end

可以从上面的代码中感觉到重复,但是重构又无从着手。传统的通过抽取方法,多态等重构手段难以奏效。这个例子很简单,但是当系统的边界条件和Controller的action增加时,比如说5个边界条件和7个action(一般的RESTful Controller 都有7个action)的时候,系统的测试用例可能达到5 x 7=35个,手工编写这些测试是一件繁重的工作。这种情况下,就需要更高级的抽象方法来消灭重复。

矩阵测试

通过观察,上面每一个测试用例都可以分解为以下部分:

  1. 初始化,比如
    login_as :visitor
  2. 动作, 比如
    get :show, :id => posts(:matrix)
    post :create, :post => {:title => "Matrix Test"}
  3. 断言,比如
    response.should be_success
    response.should rediret_to(login_path)

继 续分析我们会发现,初始化和动作两个阶段是正交的。也就是说,初始化和动作是相互独立的,任何测试用例都可以表示为一个初始化和一个动作以及断言的组合。 那么我们把这三部分的代码抽取出来,动态地加以组合,就可以运行时生成测试用例,最大限度地缩短测试代码。这种二维关系的组合可以用一个矩阵来表示。

对于上面的例子,可以如此编写矩阵测试:

describe PostsController do

matrix([ "admin", "visitor"],
["edit", "allow edit", "deny" ],
["show", "allow show", "allow show"]
)

before(:each) do
with("admin") {login_as :admin}
with("visitor"){login_as :visitor}

action("edit"){ get :edit, :id => posts(:matrix).id }
action("show"){ get :show => posts(:matrix) }

verify("edit"){ response.should render_template(:edit)}
verify("show"){ response.should render_template(:show)}

verify "deny" do
flash[:error].should == 'No permission'
assert_redirected_to login_path
end

after_verify "edit", "show" do
response.should be_success
assigns(:post).should_not be_nil
end

end
end

如何使用Matrix Spec

  1. matrix
    接受一个二维数组作为测试矩阵的描述。测试矩阵的第一行代表初始化条件,第一列代表动作,其余部分每一个字符串代表一个测试用例,字符串内容代表作为校验代码block的key。如果是空值,代表这个case不存在。
  2. with:
    描述如何初始化。
  3. action:
    描述执行什么动作。
  4. verify:
    对动作的结果进行校验。
  5. after_verify:
    类似于Controller中的after_filter,执行某些verify之后需要执行的代码,目的是重用不同verify之间相似的断言代码。

自动生成测试描述

RSpec可以根据代码中对测试的描述生成测试文档,或者称为规格说明。
在生成Test Case的描述时,matrix_spec的格式是:
"should #{verify} with #{setup} when #{action}"
生成的spec doc有良好的可读性。
运行 rake spec:doc ,测试用例的文字描述会被打印出来:

PostsController
- should deny edit with visitor when edit
- should allow edit with admin when edit
- should allow show with visitor when show
- should allow show with admin when show


矩 阵化测试可以改变我们的编程思考方式。我们把测试中的每个部分想象成空间中的一维,如果不同部分状态不互相影响,那么不同部分的组合所产生的新状态就有可 能是一个新的Test Case。通过这种动态组合,我们就可以把测试代码的重用提升至极限。这种通过正交分解来重用代码的方式类似于java社区的AOP编程。下面举一个我项 目中实际的测试矩阵:

matrix([ "logged in", "without login", "with error field", "with invalid id"],
["new", "allow new", "deny", nil, nil],
["create", "allow create", "deny", "create failed", nil],
["edit", "allow edit", "deny", nil, "show invalid"],
["update", "allow update", "deny", "update failed", "show invalid"],
["show", "allow show", "allow show", nil, "show invalid"],
["index", "allow index", "allow index", nil, nil],
["destroy", "allow destroy", "deny", nil, "show invalid"]
)

这个测试矩阵只需要很少的代码,就能在运行时生成了20个测试用例,得到了非常全面的测试覆盖。

安装RSpec和matrix_spec插件:


运行本文代码需要安装RSpec和matrix_spec插件。

  1. 安装RSpec
    ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec
    ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails
  2. 安装matrix_spec插件:

    ./script/plugin install http://svn.nibirutech.com/opensource/plugins/matrix_spec/

总结

刚 刚接触TDD的同学往往只注意实现代码的Refactoring,而忽略了测试代码的质量。随着测试代码维护成本的提高,测试驱动的积极性必然会受到打 击。矩阵测试可以大幅缩短测试代码,提高代码复用和代码质量,同时提高测试覆盖率。 这种测试方式也强迫程序员从多维对测试代码进行分解,从而提高代码质量。如果测试代码不能够或者很难正交分解,本文介绍的方法就不能适用。这种正交分解的 思想对于其他编程领域也有启发。

参考资料


http://blog.zenspider.com/archives/2007/04/functional_test_matrix.html
-- Ryan Davis 首次提出矩阵测试
http://www.infoq.com/news/2007/04/matrix-your-tests
-- InfoQ上介绍Matrix Test的文章
http://rspec.rubyforge.org/
-- Rspec官方网址

Search

导航

热门文章

最新文章

Powered By duduwolf's wiki 1.0

Copyright 1999-2007 duduwolf.com Some Rights Reserved.