笔者最近使用Rspec与cucumber两个工具完成一个学习用project测试后,产生几点思考:
1.BDD与TDD是怎么样一个关系?
1.BDD与TDD是怎么样一个关系?
2.到底一个项目中是不是需要测试?
3.谁来写这个测试?
3.谁来写这个测试?
4.对应的工具测试界线是什么?相应的思路又是什么?
问题一
TDD与BDD是怎么样的关系?
搞清这个问题首先我们要知道这两个是什么概念
TDD: 测试驱动开发是一种开发方法,是开发人员参与的活动。 其效果是以可执行的形式文档化你的需求,迫使你分清职责隔离依赖以驱动你的设计,编织安全网以便将Bug扼杀在在摇篮状态,防止其逃逸。可传统测试人员的活动是试图找到已经逃逸的Bug。这两种活动都是必要的,而且毫不冲突,互为补充。
BDD:行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。问题一
TDD与BDD是怎么样的关系?
搞清这个问题首先我们要知道这两个是什么概念
TDD: 测试驱动开发是一种开发方法,是开发人员参与的活动。 其效果是以可执行的形式文档化你的需求,迫使你分清职责隔离依赖以驱动你的设计,编织安全网以便将Bug扼杀在在摇篮状态,防止其逃逸。可传统测试人员的活动是试图找到已经逃逸的Bug。这两种活动都是必要的,而且毫不冲突,互为补充。
我们可以得出这样一个结论:BDD是在TDD基础上发展而来的,其测试目的不同,TDD是为了抓住开发中BUG为目的,BDD是为了贴近项目需求为目的
问题二
我的思考:不是必须,看实际情况。我个人是非常推荐使用,总要考虑几个问题
1.投入成本,包括编写,和维护(保持更新)
2.效果,是否需要持续集成
3.项目持续时间多长,代码人员代码水平如何?
3.项目持续时间多长,代码人员代码水平如何?
好处
1.当要deploy前,绿色一大片的感觉让你觉得更有安全感。
2.自动化测试,减少大量的从复劳动
3.代码变更时,自动化测试可追踪代码变更是否对现有功能有所影响
4.规范代码人员代码风格,帮助代码人员理清业务逻辑。
5.增加代码复用率,帮助代码人员自觉抽象model结构,和API接口。
6.cucumber测试的Feature可以作为需求文档
2.自动化测试,减少大量的从复劳动
3.代码变更时,自动化测试可追踪代码变更是否对现有功能有所影响
4.规范代码人员代码风格,帮助代码人员理清业务逻辑。
5.增加代码复用率,帮助代码人员自觉抽象model结构,和API接口。
6.cucumber测试的Feature可以作为需求文档
坏处
1.增加劳动时间,和学习周期
2.虽然减少了生产代码行数,但是却增加测试代码行数。
结论 :现有的好坏,可有得出一个结论,实施测试是好处大于坏处!
问题三
谁来写这测试代码?
我个人是推荐由开发人员编写,但cucumber每个feature的定义与Scenario,Rspec测试的Describer与IT都是由PM/测试人员是定义(也就是共同编写,一般都应经过 故事分解会议 后制定) , 那我们就来讨论下这个几个方案的
Case1
由PM/测试人员编写全部测试
优点
1.PM/测试人员对需求和边界条件考虑周全。
缺点
1.PM /测试人员需要跟代码开发人员对业务逻辑学习,增加交流成本,特别是Rspec测试时。
2. 测试人员无法继续考虑到下一步操作。
3.代码开发人员可能会等PM/测试人员编写完了才能开始工作。
Case2
代码开发人员编写所有测试代码
优点
1.增加了代码复用率,加快开发速度。
2. 开发人员一般都不喜欢写测试用例,强制要求,也只能写出基本流程的测试(总比没有强)
缺点
1. 一般开发人员都太喜欢写测试,边界条件可能想的很差。
2. PM/测试人员无法控制团队开发方向。
3. 增加与PM/测试人员交流成本
总结以上两case,优缺点结合起来我们可以得出结论:测试代码需要PM/测试人员与开发人员一起写
也就是下面我们说的第三个Case
Case3
测试人员与开发人员一起编写
流程是
PM了解客户需求 -->初步确定产品方案 ---> 与测试人员初步探讨可行性方案 ----> 召开产品故事分解会议确定故事点---->代码开发人员按故事点编写测试代码---->生成代码开发---->运行测试代码------>代码重构----->下一迭代----->项目完成
这样的方法是综合Case1与Case2的方案优缺点。扬长避短。
体外话~~~关于测试代码使用英语还是中文,我的建议是看客户情况,因为cucumber也要拿给客户看的,可能客户也不懂技术问题,但是cucumber却很巧妙将技术故事变成一个个情节故事,如果遇到比较认真的客户,他也可以通过自己常用的语言看懂方案设计。
问题四:
对应的测试界线是什么?相应的思路又是什么?
这部分内容是笔者实践后得到的感受,可能不是很贴切抓住本质
首先是cucumber,根据BDD的定义,他应该是为了贴切项目需求来的,所以在测试时,关注客户行为,以笔者的新用户注册这个故事点为列,功能是 注册成为网站会员,目的是 为了能够浏览网站只对在线会员可见的那些内容。
那么我们在测试代码中应该这样 写 Feature
位置 :../features/user_login.feature
Feature: 注册成为网站会员
为了能够浏览网站只对在线会员可见的那些内容
作为一名访客
我希望注册成为网站会员
那么现在我们分解这个故事, 客户说我要需要拿到用户的 Email以便我随时向其发送我网站的最新情况,我还需要客户 电话号码,当然如果能有用户照片更好了,好吧~我们在拿到需求后,再次进行分析,用户要注册肯定必须有名称,还得有密码,那么怎么获取这些资料呢我们肯定得构建一个表单,供客户填写,既然有这些需求我们开始编cucumber测试代码。
那么现在我们分解这个故事, 客户说我要需要拿到用户的 Email以便我随时向其发送我网站的最新情况,我还需要客户 电话号码,当然如果能有用户照片更好了,好吧~我们在拿到需求后,再次进行分析,用户要注册肯定必须有名称,还得有密码,那么怎么获取这些资料呢我们肯定得构建一个表单,供客户填写,既然有这些需求我们开始编cucumber测试代码。
位置:../features/user_login.feature
Scenario: 用户填写无效数据并注册
Given 我来到注册页面
And 我在输入框<user_name>中输入<invalidUsername>
And 我在输入框<user_email>中输入<songyuchaomeial@163.com>
And 我在输入框<user_password_confirmation>中输入<1234567810>
And 我在输入框<user_password>中输入<123456789>
And 我在输入框<user_phone_number>中输入<12345678913>
And 我上传文件<user_avatar>中选择<cat.jpg>
When 我按下<Create User>按钮
Then 我应该在错误提示信息<li>看到<Password 两次密码不正确>
可是电脑无法识别这些内容那么我们还需要写个解释文件如下
位置:../features/step_definitions/user_steps.rb
When /^我来到(.+)$/ do |page_name|
visit path_to(page_name)
end
When /^我在输入框<(.+)>中输入<(.*)>$/ do |field, value|
fill_in(field, :with => value)
end
When /^我按下<(.+)>按钮$/ do |button|
click_button(button)
end
When /^我上传文件<(.+)>中选择<(.*)>$/ do |field,filename|
attach_file(field, File.join(Rails.root, "public/features", "assets", filename))
end
Then /^我应该在错误提示信息<(.+)>看到<(.*)>$/ do |field,msg|
page.find(field).should have_content msg
end
但是电脑太傻不知道去哪个页面测试这些,所以我们还要写个地址文件
位置:../features/support/paths.rb
def path_to(page_name)
case page_name
when /首页/
"http://127.0.0.1:3000/users/index"
when /注册页面/
"http://127.0.0.1:3000/users/new"
# Add more page name => path mappings here
else
raise "Can't find mapping from \"#{page_name}\" to a path."
end
end
这样需求分析完成了,那么代码人员也就该开发这个功能了,这是代码人员与测试人员进行探讨边界条件,并写出测试描述,满足上述需求模型为了满足这样的需求,我们需要写个model测试
describe User do
describe"当任意属性为空时,user都不应被创建"do
it "当name值为NIL是,user不应该被创建"do
user = FactoryGirl.build(:user_name_nil)
assert user.invalid?
end
it "当password为空时,user不应该被创建"do
user = FactoryGirl.build(:user_password_nil)
assert user.invalid?
end
it "当email为空时,user不应该被创建"do
user = FactoryGirl.build(:user_email_nil)
assert user.invalid?
end
it "当phone_number为NIL时,user不应该被创建"do
user = FactoryGirl.build(:user_phone_number_nil)
assert user.invalid?
end
end
describe "当检察不正常的时候,user不应该被创建" do
it "当password_confirmation与password验证不通过时,user不该被创建"do
user = FactoryGirl.build(:user_password_confirmation_error)
user.should_not be_valid
end
it "当已有相同name时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_right)
assert user.invalid?
end
it "当已有相同的email时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_email_repeat)
assert user.invalid?
end
it "当已有相同电话号码phone_number时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_phone_number_repeat)
assert user.invalid?
end
describe "当email地址不正确的时候,User不应该被创建" do
it "当缺少@符号时,User不能被创建"do
user = FactoryGirl.build(:user_email_error_at)
assert user.invalid?
user.should have(1).errors_on(:email)
end
it "当缺少com结尾的,user不能被创建"do
user = FactoryGirl.build(:user_email_error_com)
assert user.invalid?
user.should have(1).errors_on(:email)
end
it "当格式不对时,user不能被创建"do
user = FactoryGirl.build(:user_email_error_name)
assert user.invalid?
user.should have(1).errors_on(:email)
end
end
it "当password小于6位数时,User不应该被创建"do
user = FactoryGirl.build(:user_password_error)
assert user.invalid?
end
it"当phone_number不等于11位数时,User不应该被创建"do
user = FactoryGirl.build(:user_phone_number_error)
assert user.invalid?
end
end
it "当保存到数据库时,hashed密码应该会生成"do
user = FactoryGirl.create(:user_right)
user.hashed_password.should_not be_nil
end
describe "登录测试"do
it "当密码不正确时,不会返回用户对象"do
old_user = FactoryGirl.create(:user_right)
user = User.authenticate('name','123458')
user.should be_nil
end
it "当用户名不正确时,不会返回用户对象"do
old_user = FactoryGirl.create(:user_right)
user = User.authenticate("name1","123456")
user.should be_nil
end
describe "关于照片的测试" do
before (:each)do
@photo = Factory.create(:photo)
end
it"是否存入指定文件夹位置"do
File.should be_exists(@photo.avatar.path(:medium))
end
end
end
end
Scenario: 用户填写无效数据并注册
Given 我来到注册页面
And 我在输入框<user_name>中输入<invalidUsername>
And 我在输入框<user_email>中输入<songyuchaomeial@163.com>
And 我在输入框<user_password_confirmation>中输入<1234567810>
And 我在输入框<user_password>中输入<123456789>
And 我在输入框<user_phone_number>中输入<12345678913>
And 我上传文件<user_avatar>中选择<cat.jpg>
When 我按下<Create User>按钮
Then 我应该在错误提示信息<li>看到<Password 两次密码不正确>
可是电脑无法识别这些内容那么我们还需要写个解释文件如下
位置:../features/step_definitions/user_steps.rb
When /^我来到(.+)$/ do |page_name|
visit path_to(page_name)
end
When /^我在输入框<(.+)>中输入<(.*)>$/ do |field, value|
fill_in(field, :with => value)
end
When /^我按下<(.+)>按钮$/ do |button|
click_button(button)
end
When /^我上传文件<(.+)>中选择<(.*)>$/ do |field,filename|
attach_file(field, File.join(Rails.root, "public/features", "assets", filename))
end
Then /^我应该在错误提示信息<(.+)>看到<(.*)>$/ do |field,msg|
page.find(field).should have_content msg
end
但是电脑太傻不知道去哪个页面测试这些,所以我们还要写个地址文件
位置:../features/support/paths.rb
def path_to(page_name)
case page_name
when /首页/
"http://127.0.0.1:3000/users/index"
when /注册页面/
"http://127.0.0.1:3000/users/new"
# Add more page name => path mappings here
else
raise "Can't find mapping from \"#{page_name}\" to a path."
end
end
这样需求分析完成了,那么代码人员也就该开发这个功能了,这是代码人员与测试人员进行探讨边界条件,并写出测试描述,满足上述需求模型为了满足这样的需求,我们需要写个model测试
describe User do
describe"当任意属性为空时,user都不应被创建"do
it "当name值为NIL是,user不应该被创建"do
user = FactoryGirl.build(:user_name_nil)
assert user.invalid?
end
it "当password为空时,user不应该被创建"do
user = FactoryGirl.build(:user_password_nil)
assert user.invalid?
end
it "当email为空时,user不应该被创建"do
user = FactoryGirl.build(:user_email_nil)
assert user.invalid?
end
it "当phone_number为NIL时,user不应该被创建"do
user = FactoryGirl.build(:user_phone_number_nil)
assert user.invalid?
end
end
describe "当检察不正常的时候,user不应该被创建" do
it "当password_confirmation与password验证不通过时,user不该被创建"do
user = FactoryGirl.build(:user_password_confirmation_error)
user.should_not be_valid
end
it "当已有相同name时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_right)
assert user.invalid?
end
it "当已有相同的email时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_email_repeat)
assert user.invalid?
end
it "当已有相同电话号码phone_number时,user不应该被创建"do
user_fist = FactoryGirl.create(:user_right)
user = FactoryGirl.build(:user_phone_number_repeat)
assert user.invalid?
end
describe "当email地址不正确的时候,User不应该被创建" do
it "当缺少@符号时,User不能被创建"do
user = FactoryGirl.build(:user_email_error_at)
assert user.invalid?
user.should have(1).errors_on(:email)
end
it "当缺少com结尾的,user不能被创建"do
user = FactoryGirl.build(:user_email_error_com)
assert user.invalid?
user.should have(1).errors_on(:email)
end
it "当格式不对时,user不能被创建"do
user = FactoryGirl.build(:user_email_error_name)
assert user.invalid?
user.should have(1).errors_on(:email)
end
end
it "当password小于6位数时,User不应该被创建"do
user = FactoryGirl.build(:user_password_error)
assert user.invalid?
end
it"当phone_number不等于11位数时,User不应该被创建"do
user = FactoryGirl.build(:user_phone_number_error)
assert user.invalid?
end
end
it "当保存到数据库时,hashed密码应该会生成"do
user = FactoryGirl.create(:user_right)
user.hashed_password.should_not be_nil
end
describe "登录测试"do
it "当密码不正确时,不会返回用户对象"do
old_user = FactoryGirl.create(:user_right)
user = User.authenticate('name','123458')
user.should be_nil
end
it "当用户名不正确时,不会返回用户对象"do
old_user = FactoryGirl.create(:user_right)
user = User.authenticate("name1","123456")
user.should be_nil
end
describe "关于照片的测试" do
before (:each)do
@photo = Factory.create(:photo)
end
it"是否存入指定文件夹位置"do
File.should be_exists(@photo.avatar.path(:medium))
end
end
end
end
现在模型的设计也OK了,那么我们现在要开始设计流程控制的
controller,同样的需要与测试人员沟通后,我们需要写出下面的测试。
#encoding:utf-8
require 'spec_helper'
describe UsersController do
describe "创建新用户测试" do
before(:each) do
@user = FactoryGirl.build(:user_right)
end
it "GET NEW 成功返回应该带有注册的界面" do
User.stub(:new).and_return(@user)
get :new
assigns(:user).should eq(@user)
end
end
describe "GET index" do
before (:each) do
@one = Factory.create(:user_two)
@two = Factory.create(:user_right)
@three = Factory.create(:user_one)
end
it "返回所有的用户并按名称排序" do
get :index
assigns(:users).should eq([@one, @two, @three])
end
end
#
describe "GET show" do
before (:each) do
@user = Factory.create(:user_right)
end
it "assigns the requested user as @user" do
get :show, {:id => @user.id}
assigns(:user).should eq(@user)
end
end
describe "GET edit" do
before (:each) do
@user = Factory.create(:user_right)
end
it "assigns the requested user as @user" do
get :edit, {:id => @user.id}
assigns(:user).should eq(@user)
end
end
describe "POST create" do
describe "with valid params" do
before (:each) do
@user = Factory.create(:user_right)
User.stub(:new).and_return(@user)
end
def do_post_create
post :create, :user => {}
end
it "creates a new User" do
@user.should_receive(:save).and_return(true)
do_post_create
assigns(:user).should be_a(User)
response.should redirect_to(User.last)
end
it "creates a new User" do
@user.should_receive(:save).and_return(false)
do_post_create
assigns(:user).should be_a(User)
response.should render_template(:new)
end
end
end
describe "PUT update" do
describe "with valid params" do
before (:each) do
@user = Factory.create(:user_right)
end
def do_put_update
put :update
end
it "updates the requested user" do
User.stub(:find).and_return(@user)
@user.should_receive(:update_attributes).and_return(true)
do_put_update
response.should redirect_to(@user)
flash[:notice].should eq ('现在的用户为' + "#{@user.name}")
end
it "assigns the requested user as @user" do
User.stub(:find).and_return(@user)
@user.should_receive(:update_attributes).and_return(false)
do_put_update
response.should render_template(:edit)
end
end
#
describe "DELETE destroy" do
before (:each) do
@user = Factory.create(:user_right)
User.stub(:find).and_return(@user)
end
def do_destroy
delete :destroy
end
it "destroys the requested user" do
@user.should_receive(:destroy)
do_destroy
response.should redirect_to(users_url)
end
end
end
end
require 'spec_helper'
describe UsersController do
describe "创建新用户测试" do
before(:each) do
@user = FactoryGirl.build(:user_right)
end
it "GET NEW 成功返回应该带有注册的界面" do
User.stub(:new).and_return(@user)
get :new
assigns(:user).should eq(@user)
end
end
describe "GET index" do
before (:each) do
@one = Factory.create(:user_two)
@two = Factory.create(:user_right)
@three = Factory.create(:user_one)
end
it "返回所有的用户并按名称排序" do
get :index
assigns(:users).should eq([@one, @two, @three])
end
end
#
describe "GET show" do
before (:each) do
@user = Factory.create(:user_right)
end
it "assigns the requested user as @user" do
get :show, {:id => @user.id}
assigns(:user).should eq(@user)
end
end
describe "GET edit" do
before (:each) do
@user = Factory.create(:user_right)
end
it "assigns the requested user as @user" do
get :edit, {:id => @user.id}
assigns(:user).should eq(@user)
end
end
describe "POST create" do
describe "with valid params" do
before (:each) do
@user = Factory.create(:user_right)
User.stub(:new).and_return(@user)
end
def do_post_create
post :create, :user => {}
end
it "creates a new User" do
@user.should_receive(:save).and_return(true)
do_post_create
assigns(:user).should be_a(User)
response.should redirect_to(User.last)
end
it "creates a new User" do
@user.should_receive(:save).and_return(false)
do_post_create
assigns(:user).should be_a(User)
response.should render_template(:new)
end
end
end
describe "PUT update" do
describe "with valid params" do
before (:each) do
@user = Factory.create(:user_right)
end
def do_put_update
put :update
end
it "updates the requested user" do
User.stub(:find).and_return(@user)
@user.should_receive(:update_attributes).and_return(true)
do_put_update
response.should redirect_to(@user)
flash[:notice].should eq ('现在的用户为' + "#{@user.name}")
end
it "assigns the requested user as @user" do
User.stub(:find).and_return(@user)
@user.should_receive(:update_attributes).and_return(false)
do_put_update
response.should render_template(:edit)
end
end
#
describe "DELETE destroy" do
before (:each) do
@user = Factory.create(:user_right)
User.stub(:find).and_return(@user)
end
def do_destroy
delete :destroy
end
it "destroys the requested user" do
@user.should_receive(:destroy)
do_destroy
response.should redirect_to(users_url)
end
end
end
end
经过这样的实例额,我们得出这样的一个结论: cucumber的测试主要关注是页面上内容,并对页面进行操作,他模拟的是客户在点击页面后,应该看到的情况,Rspec是进行控制器流程控制与模型测试。
参考资料
https://github.com/jnicklas/capybaracapybara
使用
https://github.com/jnicklas/capybara/blob/master/lib/capybara/node/actions.rbcapybara
源代码
转载于:https://blog.51cto.com/jacksongblack/1154634