Saturday, October 18, 2008

Behaivour-driven development using plain Chinese story

This blog post is talking about how to use rspec and cucumber in Ruby to make behavior driven development with plain story in Chinese language.

It seems behaviour-driven development is getting more and more popular in recent days. If you are still not sure what BDD is, take a look at following links:

1] Introducing BDD by Dan North
2] Beyond Test Driven Development: Behaviour Driven Development, Google Talk, by Dave Astels
3] Behaviour Driven Development with RSpec, RubyConf 2007, by David Chelimsky, Dave Astels

The most interesting feature of BDD is using stories of plain text as the test cases. Here the story text itself is not only the description of the application's functionality, but also the executable acceptance criterion of application's behavior. Let's have a look at the story example of a calculator addition feature:
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers

Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
And the result class should be Fixnum
Both developers and customers can benefit from above story since it is in plain text and easily understood. The further question you might ask is that if it is possible to use spoken languages other than English to write our BDD stories? The answer is "definitely yes!". Thanks to the cucumber -- the new RSpec's Story Runner. Multiple spoken languages support is a built-in feature of cucumber.

The first step is to install cucumber, make sure you have install rpsec. You can install cucumber from gem:
gem install cucumber
or building gem yourself:
git clone git://github.com/aslakhellesoy/cucumber.git
cd cucumber
rake install_gem
The keyword translation for Chinese language in cucumber is defined as following:
"zh-CN":
feature: 功能
scenario: 场景
given_scenario: 引用场景
given: 假如
when: 当
then: 那么
and: 而且
but: 但是

OK, let's write our story in Chinese:
功能:加法
为了避免一些愚蠢的错误
作为一个数学白痴
我希望有人告诉我数字相加的结果

场景: 两个数相加
假如我已经在计算器里输入6
而且我已经在计算器里输入7
当我按相加按钮
那么我应该在屏幕上看到的结果是13

场景: 三个数相加
假如我已经在计算器里输入6
而且我已经在计算器里输入7
而且我已经在计算器里输入1
当我按相加按钮
那么我应该在屏幕上看到的结果是14

Save the plain text in Chinese to a file, here we give it name "addition.feature". Now we can run this feature in command line:
> cucumber --language zh-CN addition.feature
You should see following output:
功能:加法  # addition.feature
为了避免一些愚蠢的错误
作为一个数学白痴
我希望有人告诉我数字相加的结果

场景: 两个数相加 # addition.feature:6
假如 我已经在计算器里输入6 # addition.feature:7
而且 我已经在计算器里输入7 # addition.feature:8
当 我按相加按钮 # addition.feature:9
那么 我应该在屏幕上看到的结果是13 # addition.feature:10

场景: 三个数相加 # addition.feature:12
假如 我已经在计算器里输入6 # addition.feature:13
而且 我已经在计算器里输入7 # addition.feature:14
而且 我已经在计算器里输入1 # addition.feature:15
当 我按相加按钮 # addition.feature:16
那么 我应该在屏幕上看到的结果是14 # addition.feature:17

9 steps pending

You can use these snippets to implement pending steps:

假如 /^我已经在计算器里输入6$/ do
end

假如 /^我已经在计算器里输入7$/ do
end

当 /^我按相加按钮$/ do
end

那么 /^我应该在屏幕上看到的结果是13$/ do
end

假如 /^我已经在计算器里输入1$/ do
end

那么 /^我应该在屏幕上看到的结果是14$/ do
end

See pending step notification? Yes, that is because we haven't defined the steps for this feature. Before we define the steps, we might first consider writing our calculator code. Following is a simple implementation. Here we add a silly bug(The initial sum is not 0 but 1) to let cucumber report it.

class Calculator
def push(n)
@args ||= []
@args << n
end

def
add
@args.inject(1){|n,sum| sum+=n}
end
end
Save the code to a local file within the same directory of your feature file. Now let's define the step matcher for addition feature with Calculator class defined above.
require 'spec'
require 'calculator'

Before do
@calc = Calculator.new
end

After do
end

Given "我已经在计算器里输入$n" do |n|
@calc.push n.to_i
end

When /我按(.*)按钮/ do |op|
if op == '相加'
@result = @calc.send "add"
end
end

Then /我应该在屏幕上看到的结果是(.*)/ do |result|
@result.should == result.to_f
end

Now run the feature again, you should see:
功能:加法  # addition.feature
为了避免一些愚蠢的错误
作为一个数学白痴
我希望有人告诉我数字相加的结果

场景: 两个数相加 # addition.feature:6
假如 我已经在计算器里输入6 # calculator_steps.rb:11
而且 我已经在计算器里输入7 # calculator_steps.rb:11
当 我按相加按钮 # calculator_steps.rb:15
那么 我应该在屏幕上看到的结果是13 # calculator_steps.rb:21
expected: 13.0,
got: 14 (using ==) (Spec::Expectations::ExpectationNotMetError)
./calculator_steps.rb:22:in `那么 /461021457224505745453450456117457125447012473413451060473204477323463634463057(.*)/'
addition.feature:10:in `那么 我应该在屏幕上看到的结果是13'

场景: 三个数相加 # addition.feature:12
假如 我已经在计算器里输入6 # calculator_steps.rb:11
而且 我已经在计算器里输入7 # calculator_steps.rb:11
而且 我已经在计算器里输入1 # calculator_steps.rb:11
当 我按相加按钮 # calculator_steps.rb:15
那么 我应该在屏幕上看到的结果是14 # calculator_steps.rb:21
expected: 14.0,
got: 15 (using ==) (Spec::Expectations::ExpectationNotMetError)
./calculator_steps.rb:22:in `那么 /461021457224505745453450456117457125447012473413451060473204477323463634463057(.*)/'
addition.feature:17:in `那么 我应该在屏幕上看到的结果是14'

Lets change the calculator class to fix the bug:
class Calculator
def push(n)
@args ||= []
@args << n
end

def
add
@args.inject(0){|n,sum| sum+=n}
end
end
And run cucumber again, you should see:
功能:加法  # addition.feature
为了避免一些愚蠢的错误
作为一个数学白痴
我希望有人告诉我数字相加的结果

场景: 两个数相加 # addition.feature:6
假如 我已经在计算器里输入6 # calculator_steps.rb:11
而且 我已经在计算器里输入7 # calculator_steps.rb:11
当 我按相加按钮 # calculator_steps.rb:15
那么 我应该在屏幕上看到的结果是13 # calculator_steps.rb:21

场景: 三个数相加 # addition.feature:12
假如 我已经在计算器里输入6 # calculator_steps.rb:11
而且 我已经在计算器里输入7 # calculator_steps.rb:11
而且 我已经在计算器里输入1 # calculator_steps.rb:11
当 我按相加按钮 # calculator_steps.rb:15
那么 我应该在屏幕上看到的结果是14 # calculator_steps.rb:21

9 steps passed
Congratulations! All steps passed!

You can find the code of this example at /examples/chinese_simplified_calculator of cucumber git repository(git://github.com/aslakhellesoy/cucumber.git).