If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization. -- Gerald Weinberg, Weinberg’s Second Law
- 节省时间 -- 回归测试
- 可重复
- 更精确
- 更好的维护性
Behavior-driven development != Ingration test
-
Do the right thing
-
Avoid miscommunication
For the image, the most common answer is a red dot, but only 33 percent of observers wrote that. Next is a red circle, 18 percent. BDD asks questions about the behavior of an application before and during development so that the stakeholders are less likely to miscommunicate.
TDD is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved to pass the new tests, only.
In sort: right test first then write the code
-
Integration test ```ruby # features/sort_movie_list.feature Feature: display list of movies sorted by different criteria
As an avid moviegoer So that I can quickly browse movies based on my preferences I want to see movies sorted by title or release date
Background: movies have been added to database
Given the following movies exist:
| title | rating | release_date |
| Aladdin | G | 25-Nov-1992 |
| The Terminator | R | 26-Oct-1984 |
| When Harry Met Sally | R | 21-Jul-1989 |
| The Help | PG-13 | 10-Aug-2011 |
| Chocolat | PG-13 | 5-Jan-2001 |
| Amelie | R | 25-Apr-2001 |
| 2001: A Space Odyssey | G | 6-Apr-1968 |
| The Incredibles | PG | 5-Nov-2004 |
| Raiders of the Lost Ark | PG | 12-Jun-1981 |
| Chicken Run | G | 21-Jun-2000 |
And I am on the RottenPotatoes home page
Scenario: sort movies alphabetically
When I follow "Movie Title"
Then I should see "2001: A Space Odyssey" before "Aladdin"
And I should see "Aladdin" before "Amelie"
Scenario: sort movies in increasing order of release date
When I follow "Release Date"
Then I should see "2001: A Space Odyssey" before "Raiders of the Lost Ark"
And I should see "Raiders of the Lost Ark" before "The Terminator"
# features/support/paths.rb
module NavigationHelpers
# Maps a name to a path. Used by the
#
# When /^I go to (.+)$/ do |page_name|
#
# step definition in web_steps.rb
#
def path_to(page_name)
case page_name
when /^the (RottenPotatoes )?home\s?page$/ then '/movies'
when /^the movies page$/ then '/movies'
# Add more mappings here.
# Here is an example that pulls values out of the Regexp:
#
# when /^(.*)'s profile page$/i
# user_profile_path(User.find_by_login($1))
else
begin
page_name =~ /^the (.*) page$/
path_components = $1.split(/\s+/)
self.send(path_components.push('path').join('_').to_sym)
rescue NoMethodError, ArgumentError
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
end
World(NavigationHelpers)
# features/step_definitions/movie_steps.rb
Given /the following movies exist/ do |movies_table|
movies_table.hashes.each do |movie|
# each returned element will be a hash whose key is the table header.
# you should arrange to add that movie to the database here.
Movie.create(movie)
end
end
# features/step_definitions/web_steps.rb
# Make sure that one string (regexp) occurs before or after another one
# on the same page
Then /I should see "(.*)" before "(.*)"/ do |e1, e2|
page.body.index(e1).should < page.body.index(e2)
end
# Single-line step scoper
When /^(.*) within (.*[^:])$/ do |step, parent|
with_scope(parent) { When step }
end
# Multi-line step scoper
When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string|
with_scope(parent) { When "#{step}:", table_or_string }
end
Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name)
end
```
- Unit test
```ruby
require 'spec_helper'
describe Movie do
describe 'searching Tmdb by keyword' do
it 'should call Tmdb with title keywords' do
expect(TmdbMovie).to receive(:find).with(hash_including title: 'Inception')
Movie.find_in_tmdb('Inception')
end
end
end
class Movie < ActiveRecord::Base
def self.find_in_tmdb(string)
TmdbMovie.find(title: string)
end
# ...
end
```
- Character test ```ruby # time_setter.rb class TimeSetter def self.convert(d) y = 1980 while (d > 365) do if (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0)) if (d > 366) d -= 366 y += 1 end else d -= 365 y += 1 end end return y end end
# time_setter_spec.rb
SimpleCov.start
require './time_setter'
describe TimeSetter do
{ 365 => 1980, 366 => 1981, 900 => 1982 }.each do |arg, result|
it "#{arg} days puts us in #{result}" do
expect(TimeSetter.convert(arg)).to eq(result)
end
end
end
```
- Specific Feature: User can search for a movie (vague) Feature: User can search for a movie by title (specific)
- Measurable. Adding Measurable to Specific means that each story should be testable, which implies that there are known expected results for some good inputs. Feature: Rotten Potatoes should have good response time (unmeasurable) Feature: When adding a movie, 99% of Add Movie pages should appear within 3 seconds (measurable)
- Achievable. Ideally, you implement the user story in one Agile iteration. If you are getting less than one story per iteration, then they are too big and you need to subdivide these stories into smaller ones
- Relevant. A user story must have business value to one or more stakeholders.
To drill down to the real business value, one technique is to keep asking “Why.” Using as an example a ticket-selling app for a regional theater, suppose the proposal is to add a Facebook linking feature. Here are the “Five Whys” in action with their recursive questions and answers:
- Why add the Facebook feature? As box office manager, I think more people will go with friends and enjoy the show more.
- Why does it matter if they enjoy the show more? I think we will sell more tickets.
- Why do you want to sell more tickets? Because then the theater makes more money.
- Why does theater want to make more money? We want to make more money so that we don’t go out of business.
- Why does it matter that theater is in business next year? If not, I have no job.
- Timeboxed. Timeboxing means that you stop developing a story once you’ve exceeded the time budget. Either you give up, divide the user story into smaller ones, or reschedule what is left according to a new estimate. If dividing looks like it won’t help, then you go back to the customers to find the highest value part of the story that you can do quickly.
- Fast: it should be easy and quick to run the subset of test cases relevant to your current coding task, to avoid interfering with your train of thought. We will use a Ruby tool called Autotest to help with this.
- Independent: No test should rely on preconditions created by other tests, so that we can prioritize running only a subset of tests that cover recent code changes.
- Repeatable: test behavior should not depend on external factors such as today’s date or on “magic constants” that will break the tests if their values change, as occurred with many 1960s programs when the year 2000 arrived.
- Self-checking: each test should be able to determine on its own whether it passed or failed, rather than relying on humans to check its output
- Timely: tests should be created or updated at the same time as the code being tested.