# 测试 写测试的基础入门

ACzero · 2017年07月24日 · 最后由 kafei 回复于 2017年12月21日 · 12652 次阅读

## 测试的基本步骤

1. 准备前置条件 (Setup)
2. 执行 (Exercise)
3. 验证 (Verification)
4. 清理数据 (Teardown)

``````require 'rails_helper'

RSpec.describe Teacher, type: :model do
describe "create a new teacher" do
it "will strip and remove space in name" do
# Setup
school = School.create!(name: 'test')

# Exercise
teacher = Teacher.create!(name: ' a bc d', school: school)

# Verification
expect(teacher.name).to eq('abcd')

# Teardown
school.destroy!
teacher.destroy!
end
end
end
``````

## 描述你的测试

``````RSpec.describe Calculator do
describe '.is_odd?' do
context 'when argument is odd' do
it 'will not raise error' do
expect { Calculator.is_odd?(1) }.not_to raise_error
end

it 'return true' do
expect(Calculator.is_odd?(1)).to be true
end
end

context 'when argument is even' do
it 'will not raise error' do
expect { Calculator.is_odd?(2) }.not_to raise_error
end

it 'return false' do
expect(Calculator.is_odd?(2)).to be false
end
end
end
end
``````

• 测试 Calculator 的`is_odd?`方法，当参数为奇数时，不应该抛出异常。
• 测试 Calculator 的`is_odd?`方法，当参数为奇数时，返回`true`
• 测试 Calculator 的`is_odd?`方法，当参数为偶数时，不应该抛出异常。
• 测试 Calculator 的`is_odd?`方法，当参数为偶数时，返回`false`

``````Failures:

1) Calculator.is_odd? when argument is even return false
Failure/Error: expect(Calculator.is_odd?(2)).to be false

expected false
got true
# ./example1/calculator_spec.rb:26:in `block (4 levels) in <top (required)>'

Finished in 0.0201 seconds (files took 0.56765 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./example1/calculator_spec.rb:25 # Calculator.is_odd? when argument is even return false
``````

## 断言 (assertion)

### 相等性检验

``````describe 'expect equality' do
it do
foo = 1
expect(foo).to eq(1)
end

it do
foo = [1, 2, 3]
expect(foo).not_to equal([1, 2, 3])
end
end
``````

``````# expect(foo).to eq(1)
foo == 1

# expect(foo).not_to equal([1, 2, 3])
!foo.equal?([1, 2, 3])
``````

### 检验异常抛出

``````describe 'exception' do
it do
expect { nil.split(',') }.to raise_error(NoMethodError)
end
end
``````

### 检验状态变化

rspec 提供了方便的写法来检验对象状态变化：

``````describe 'state change' do
it do
arr = [1]
expect { arr += [2, 3] }.to change { arr.size }.by(2)
end
end
``````

## 测试替身 (Test Double)

`Double`一词来源于拍电影中常用的`stunt double(替身演员)`，顾名思义，替身的作用是用于替换掉功能中的某个部分，通常会应用在下列情况中：

• 要测试的功能需要访问一些外部服务（如 web API）
• 要测试的功能由几个模块共同工作，但可能有一个甚至多个模块还没完成。
• 想要验证功能中的模块是不是按预期被调用

### Stub

stub 的作用是为特定的方法调用设置返回值。

``````class Calendar
def today_day_off?
Date.today.saturday? || Date.today.sunday?
end
end
``````

`Calendar`中有一个`#today_day_off?`方法判断今日是否休息日，但是因为 Calendar 类使用`Date.today()`方法去获取当前日期，这导致运行结果会跟测试运行时的日期有关，这显然不是我们想要的。因此我们在测试中使用了`stub`

``````RSpec.describe Calendar do
describe '.today_day_off?' do
context 'when today is sunday' do
# before中的内容会在该块中每个测试运行前执行
before do
# stub Date.today
allow(Date).to receive(:today).and_return(Date.parse('2017-07-23'))
end

it 'return true' do
expect(Calendar.new.today_day_off?).to be true
end
end

context 'when today is monday' do
before do
# stub Date.today
allow(Date).to receive(:today).and_return(Date.parse('2017-07-24'))
end

it 'return false' do
expect(Calendar.new.today_day_off?).to be false
end
end
end
end
``````

### Spy

spy 的作用是记录对象的行为，可用于验证在对象上的方法调用。

``````module MyHelper
def average_of(array)
sum = array.reduce(&:+)
sum.fdiv(array.size)
end
end
``````

``````include MyHelper

RSpec.describe MyHelper do
describe '#average_of' do
it 'use reduce to sum' do
arr_spy = spy([1, 2, 3])
average_of(arr_spy)

expect(arr_spy).to have_received(:reduce)
end
end
end
``````

### mock

mock 的功能是设置响应（stub）以及验证预期行为（spy）。一般使用的时候会生成一个 mock 对象，然后再设置该对象的方法响应。

mock 主要用于依赖的模块没有完成时，能正常运行测试。参考下面例子：

``````class Order
def initialize(warehouse, amount)
if warehouse.has_enough?(amount)
warehouse.remove(amount)
@valid = true
else
@valid = false
end
end

def valid?
!!@valid
end
end
``````

``````RSpec.describe Order do
describe 'create new order' do
context 'when inventory is enough' do
it 'order is valid' do
warehouse = double('warehouse')
expect(warehouse).to receive(:has_enough?).with(50).and_return(true)
expect(warehouse).to receive(:remove).with(50)

order = Order.new(warehouse, 50)

expect(order.valid?).to be true
end
end

context 'when inventory is not enough' do
it 'order is invalid' do
warehouse = double('warehouse')
expect(warehouse).to receive(:has_enough?).with(51).and_return(false)

order = Order.new(warehouse, 51)

expect(order.valid?).to be false
end
end
end
end
``````

## 参考链接

jasl 将本帖设为了精华贴。 07月24日 20:57

shellywang 回复