#155 Beginning with Cucumber
- Download:
- source codeProject Files in Zip (105 KB)
- mp4Full Size H.264 Video (24.8 MB)
- m4vSmaller H.264 Video (16.4 MB)
- webmFull Size VP8 Video (45.3 MB)
- ogvFull Size Theora Video (35.3 MB)
Cucumber 是一个用来对Ruby On Rails应用进行行为驱动开发(BDD)的高级测试框架。Cucumber的独有特点是他使用英语(或者其他支持的语言,包括汉语)来定义程序的行为,在这一集我们将从头开始创建Rails 应用程序,并且使用Cucumber来定义其行为。
创建应用程序
我们将首先创建一个新的Rails应用程序,并为这个例子使用最经典的例子来创建一个博客程序。首先我们使用最通常的方式来创建一个Rails项目。
rails blog
接下来,我们需要使用我们将要用到的这些gems来创建测试环境。 在 /config/environments/test.rb
的底部加入这些代码:
config.gem "rspec", :lib => false, :version => ">=1.2.2" config.gem "rspec-rails", :lib => false, :version => ">=1.2.2" config.gem "webrat", :lib => false, :version => ">=0.4.3" config.gem "cucumber", :lib => false, :version => ">=0.2.2"
除了Cucumber,我们还需要 Rspec 和 Webrat.他们都不是运行Cucumber必须的,但是他们可以很好的协同工作。当你熟悉使用Cucumber后你可以使用单元测试来代替Rspec或用一个不同的测试框架来代替Webrat.
现在我们在程序中已经指定了需要的gems ,我们通过运行rake命令来安装他们
sudo rake gems:install RAILS_ENV=test
如果你还没安装过这些gems,他们将会被自动安装。安装完毕后我们就可以为应用程序建立Cucumber了。
script/generate cucumber
这个命令会在应用程序根目录下创建一个features文件夹。这就是我们定义应用程序行为的地方。
创建我们的第一个故事片 (感觉自己像导演吧!):
译注:关于feature究竟应该翻译成"功能"还是"故事片"或者别的什么,我觉得翻译成故事片更合适。因为接下来就是场景什么的,我们把自己当成是一个导演,演员就是我们应用程序的用户,告诉演员要干什么干什么。从这点上来讲,cucumber确实够酷。
在我们创建的这个博客程序中,我们想要管理文章,所以我们在features文件夹下新建一个叫 manage_atricles.feature
的文件。(如果你使用TextMate作为开发工具,那么你可以去下载cucumber的tm bundle,地址 http://github.com/bmabey/cucumber-tmbundle)
Cucumber的定义由两个部份组成。第一项是可选的,用来定义故事片情节本身,他们由3个部份组成:“In order”, “As a” 和 “I Want”. 我们像这样来定义我们的博客程序:
Feature: Manage Articles In order to make a blog As an author I want to create and manage articles
"In order"行定义了我们的总体目标,就是创建一个博客。"As a"行定义了一个条件,可以理解为故事片中的演员。最后一行是我们想要实现的具体功能,也就是这个故事片的主要情节。
现在我们来定义一些这个故事片种的一些行为(情节),这是通过一个或者多个场景来实现的。场景通常按照一种相似的方式来设计,现在我们用"Given","When" 和 "Then"三个部份来组成场景。我们要写一个列出博客文章的场景。
Scenario: Articles List Given I have articles titled Pizza, Breadsticks When I go to the list of articles Then I should see "Pizza" And I should see "Breadsticks"
完成以后,我们来运行Cucumber,看它的返回的输出。我们用 -n
选项来让Cucumber返回一个更简介的输出:
$ cucumber features -n Feature: Manage Articles In order to make a blog As an author I want to create and manage articles Scenario: Articles List Given I have articles titled Pizza, Breadsticks When I go to the list of articles Then I should see "Pizza" And I should see "Breadsticks" 1 scenario 3 skipped steps 1 undefined step You can implement step definitions for missing steps with these snippets: Given /^I have articles titled Pizza, Breadsticks$/ do pending end
Cucumber显示了我们的剧本纲要和场景演出的结果。其中3个步骤被跳过,1个步骤未定义。Cucumber不知道怎么运行没定义的步骤,因为我们还没有写Ruby代码来执行。Cucumber提供了一段我们可以直接粘贴到我们的程序的代码模板。
定义"Given"步骤
场景步骤被定义在 features
文件中的step_definitions文件夹中。我们在这个文件中新建一个叫article_steps.rb
的文件来定义我们的场景。
Given /^I have articles titled Pizza, Breadsticks$/ do pending end
我们的步骤被定义成一个正则表达式。它会定义这个场景种的文本语言并且返回成我们可以用到Ruby代码里去的一些东西。让这个步骤具有可以处理任何 传入的数据的能力是个不错的主意,所以我们要把正则表达式中的"Pizza,Breadsticks"替换成一个子表达式,然后匹配子表达式返回的每个标 题。
Given /^I have articles titled (.+)$/ do |titles| titles.split(', ').each do |title| Article.create!(:title => title) end end
我们将传入一些由逗号和空格分隔的可以被(.+)部份匹配的标题,并且传递给titles这个变量。然后,我们通过","把字符串分隔成单独的title。每个能匹配上的标题我们都会用来创建一篇新的文章。这就可以满足Given的条件了。
其他的步骤呢?
当我们运行Cucumber的时候,它告诉我们有一个步骤没有被定义,其他三个步骤被跳过。这意味着虽然他们没有被执行,但是这些步骤的定义是存在 的。如果是这样的话,那么他们该在哪里定义呢?答案是Webrat在和article步骤相同目录下的一个叫webrat_steps.rb
的文件里定义 了一系列的步骤。这些步骤定义了一些常用的任务,比如进入到一个页面。
When /^I go to (.+)$/ do |page_name| visit path_to(page_name) end
这个步骤定义符合我们第二步(当我查看文章列表)并且还有符合其他被跳过的步骤的定义。
一步一步的把他变绿
现在我们所有的步骤都定义好了,我们可以再次运行Cucumber,并且查看Cucumber的输出来决定我们下一步该做什么。
$ cucumber features -n Feature: Manage Articles In order to make a blog As an author I want to create and manage articles Scenario: Articles List Given I have articles titled Pizza, Breadsticks uninitialized constant Article (NameError) ./features/step_definitions/article_steps.rb:3:in `__instance_exec0' ./features/step_definitions/article_steps.rb:2:in `each' ./features/step_definitions/article_steps.rb:2:in `/^I have articles titled (.+)$/' features/manage_articles.feature:7:in `Given I have articles titled Pizza, Breadsticks' When I go to the list of articles Then I should see "Pizza" And I should see "Breadsticks" 1 scenario 1 failed step 3 skipped steps
这次我们看不那个未定义步骤了,取而代之的是一个失败的步骤。这个步骤之所以失败是因为我们还没有创建Article模型。我们将通过RSpec生成器来创建它。
script/generate rspec_model Article title:string content:text
译者注:windows用户请用 ruby script/generate xxxxxx
然后我们运行数据迁移,并且把开发数据库结构复制到测试数据库。
rake db:migrate rake db:test:clone
现在我们再次运行Cucumber。
$ cucumber features -n Feature: Manage Articles In order to make a blog As an author I want to create and manage articles Scenario: Articles List Given I have articles titled Pizza, Breadsticks When I go to the list of articles Can't find mapping from "the list of articles" to a path. Now, go and add a mapping in features/support/paths.rb (RuntimeError) /Users/eifion/rails/apps_for_asciicasts/blog/features/support/paths.rb:12:in `path_to' ./features/step_definitions/webrat_steps.rb:11:in `/^I go to (.+)$/' features/manage_articles.feature:8:in `When I go to the list of articles' Then I should see "Pizza" And I should see "Breadsticks" 1 scenario 1 failed step 2 skipped steps 1 passed step
我们的第一步通过了测试!但是第二步现在失败了。这是因为Cucumber不知道怎么把 "the list of articles"
对应到我们应用程序的路径。
Cucumber安装的时候在/features/support文件夹里会生成一个叫paths.rb的文件。在这个文件里我们可以自定义 Cucumber文件里的英语定义和我们的Rails应用程序路径之间的映射关系。要增加一条映射,我们只需要在path_to 方法里添加一个新的when条件到case语句。(文件里有一个注释来告诉你添加到哪里)
# Add more page name => path mappings here when /the list of articles/ articles_path
再次运行Cucumber的时候,会显示下一个需要修复的错误。这次他找不到符合articles_path的路由。我们需要把这个映射加入到routes.rb
map.resources :articles
完成以后,Cucumber告诉我们找不到ArticlesController
。和模型一样,我们使用RSpec生成器来创建这个控制器。
script/generate rspec_controller articles index
Cucumber仍然不满意。他想在我们的视图里看到"Pizza",但是我们还没在index视图写任何东西所以Cucumber找不到他。我们要修改Articles控制器的index action 和 视图 代码来显示文章列表。
def index @articles = Article.all end
<h1>Articles</h1> <% @articles.each do |article| %> <h2><%= h(article.title) %></h2> <p><%= h(article.content) %></p> <% end %>
这回Cucumber高兴啦!当我们再次运行他的时候他显示4个步骤全部通过。
看起来我们不得不为了一个Cucumber场景的通过付出大量努力,事实上我们采用了故意很小的步骤来展示Cucumber通常都告诉你下一步该干什么。当你熟练使用cucumber后,你可以在更大些的步骤完成后运行cucumber。
一个新的场景
现在我们已经展示了cucumber大概是怎样工作的了。我们将继续另外一个更复杂的例子,并且使用更大的步骤。
Scenario: Create Valid Article Given I have no articles And I am on the list of articles When I follow "New Article" And I fill in "Title" with "Spuds" And I fill in "Content" with "Delicious potato wedges!" And I press "Create" Then I should see "New article created." And I should see "Spuds" And I should see "Delicious potato wedges!" And I should have 1 article
这个场景定义了新建一个文章的行为,但是和刚才那个不同,他包含了我们应用程序的很多个页面。这个场景假设我们刚开始没有文章,然后点击"New Article"这个文字链接,提填写表单然后提高,然后然后希望看到这篇新文章。并且在这个时候数据库中有一篇文章。
和平常一样,我们的第一部是运行Cucumber. 运行后他告诉我们有两个没有定义的步骤和6个跳过的步骤。两个未定义的步骤是第一个("Given I have no articles")和第二个("And I should have 1 article")。 其他6个被包含在Webrat步骤中。
首先我们要创建那两个没定义的步骤.
Given /^I have no articles$/ do Article.delete_all end Then /^I should have ([0-9]+) article$/ do |count| Article.count.should == count.to_i end
第一步直截了当写出来。为了满足那个条件,我们只需要删除所有的文章。第二步中,我们把数字1改成了一个可以匹配正数的正则表达式,并且把那个数字 当成一个变量传给代码块。然后代码块检查文章的数量是否等于那个传进来的变量。注意Cucumber会把接收到的任何正则表达式当成字符串处理,所以我们 需要显式的把count这个变量转换成integer类型。
再次运行Cucumber,显示前面两个步骤现在通过测试,但是第三步失败。
Given I have no articles And I am on the list of articles When I follow "New Article" Could not find link with text or title or id "New Article" (Webrat::NotFoundError) (eval):2:in `click_link' ./features/step_definitions/webrat_steps.rb:19:in `/^I follow "([^\"]*)"$/' features/manage_articles.feature:15:in `When I follow "New Article"'
这步失败是因为cucumber找不到一个叫"New Article"的文字链接。我们需要在index页和新文章页之间创建一个链接并且创建这个页面。
我们在index页上创建一个指向新文章页的链接。
<p><%= link_to "New Article", new_article_path %></p>
然后在/views/articles/下面的new.html.erb中为新文章创建一个表单。
<p><%= link_to "New Article", new_article_path %></p>
然后在/views/articles/下面的new.html.erb中为新文章创建一个表单。
<% form_for @article do |f| %> <ol class="formList"> <li> <%= f.label :title %> <%= f.text_field :title %> </li> <li> <%= f.label :content %> <%= f.text_field :content %> </li> <li> <%= f.submit "Create" %> </li> </ol> <% end %>
最后我们创建控制器
def new @article = Article.new end def create @article = Article.create!(params[:article]) flash[:notice] = "New article created." redirect_to articles_path end
我们在为控制器编写new和create action的时候迈了一大步。我们来再次用cucumber运行这个场景,然后看还有啥要做的。这回前面6个步骤都通过了,然后就遇到这个失败的步骤。
Then I should see "New article created." expected the following element's content to include "New article created.": ArticlesSpudsDelicious potato wedges!New Article (Spec::Expectations::ExpectationNotMetError) ./features/step_definitions/webrat_steps.rb:94:in `/^I should see "([^\"]*)"$/' features/manage_articles.feature:19:in `Then I should see "New article created."'
Although we’ve created the flash message in the create
action we’re not displaying it on the index page. Adding it in should make this step pass. It should go in a layout file, but we’ll just add it to the top of the index page to make the step pass.
<%= flash[:notice] %>
虽然我们在create action种创建了flash message,但是我们并没有在index页面里显示出来。加上它应该就可以让这步测试通过。它应当放在模板文件里,但是由于我们的目标只是让这步测试通过,所以我们放在index页面里。
<%= flash[:notice] %>
全部变绿啦
我们加上flash message后,再次运行cucumber,所有的步骤都绿了。
$ cucumber features -n Feature: Manage Articles In order to make a blog As an author I want to create and manage articles Scenario: Articles List Given I have articles titled Pizza, Breadsticks When I go to the list of articles Then I should see "Pizza" And I should see "Breadsticks" Scenario: Create Valid Article Given I have no articles And I am on the list of articles When I follow "New Article" And I fill in "Title" with "Spuds" And I fill in "Content" with "Delicious potato wedges!" And I press "Create" Then I should see "New article created." And I should see "Spuds" And I should see "Delicious potato wedges!" And I should have 1 article 2 scenarios 14 passed steps
现在我们的应用程序的行为和我们设计的一样了。随着这个场景接下来的步骤的通过,我们开始一个新的场景,觉一篇没有通过的文章。
在这里我们已经介绍了cucumber的基础部份。它看起来需要很多的工作来用英语定义测试,并且一次一次的完成每个步骤。但是cucumber测试有很多有点。你用它测试你的真个堆栈,所以你写的测试包括了从用户界面到数据库的一切。他不是单元测试的替代物,他提供了一个写测试你的应用程序的整体 行为好方法。