RSpec: sự khác biệt giữa mocks và stubs
Bài đăng này đã không được cập nhật trong 7 năm
Trong bài viết này mình sẽ giới thiệu với bạn cách phân biệt mocks và stubs trong Rspec. Trước tiên, ta cần hiểu mock là gì, stub là gì. Trong cuốn Effective Testing with RSpec 3 có định nghĩa thế này: Stub
Returns canned responses, avoiding any meaningful computation or I/O
Code của nó sẽ trông thế này:
allow(some_object).to receive(some_method).and_return(some_value)
Mock
Expects specific messages; will raise an error if it doesn’t receive them by the end of the example
Ví dụ về mock
expect(some_object).to receive(some_method).and_return(some_value)
mocks
là kỳ vọng một phương thức được gọi đến sẽ trả về một hay nhiều giá trị nào đó, trong khi stubs
chỉ quan tâm đến trạng thái trả về của một đối tượng khi nhận một message nào đó.
Hãy sử dụng một ví dụ để dễ dàng hiểu hơn về khái niệm này. Chúng ta sẽ viết Rspec cho đoạn code này:
class DataProcessor
Error = Class.new(StandardError)
def process(data, validator)
raise Error unless validator.valid?(data)
# simple logic to show the idea
"#{data} processed"
end
end
Chúng ta có lớp DataProcessor
với phương thức process
. Nó được truyền vào tham số data
và validator
. Nếu validator
trả về true
khi gọi đến phương thức valid?
thì sẽ thêm chuỗi "processed" vào cuối data
. Đơn giản.
Giờ ta muốn viết spec kiểm tra xem có chuỗi "processed" cuối data
sau khi gọi đến phương thức hay không. Nhưng phương thức process
vẫn yêu cầu tham số validator
phải qua được phương thức valid?
. Hiện tại chúng ta không quan tâm tới validator
, vì vậy chúng ta có thể sử dụng stub
.
Trước tiên tạo ra spec rỗng cho DataProcessor
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
end
Giờ ta có thể viết một trường hợp với sự kì vọng:
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
it 'adds processed to valid data' do
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
Nhưng chúng ta chưa có validator
nên không thể trả về kết quả như kì vọng nên ta cần tạo một đối tượng validator
gọi tới phương thức valid?
và trả về true
.
Đầu tiên tạo ra bằng double (double dùng thể thay thế cho một đối tượng nào đó trong khi test, có thể sử dụng allow()
và receiver()
tùy ý):
validator = double(:validator)
Khi đã có double validator, chúng ta cho phép nó gọi valid?
.
allow(validator).to receive(:valid?).and_return(true)
Giờ validator
đã nhận valid?
và trả về true
. Đó gọi là stubs, có thể viết gọn lại thành 1 dòng:
validator = double(:validator, valid?: true)
Giờ spec sẽ thế này:
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
it 'adds processed to valid data' do
validator = double(:validator, valid?: true)
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
Spec đã chuyển màu xanh. Validator chúng ta truyền vào phương thức process
trả về true
khi gọi valid?
, vì vậy data
của chúng ta đã được xử lý.
Giờ ta sẽ thêm 1 trường hợp kiểm tra phương thức process
ném ra Error
với dữ liệu không hợp lệ. Chúng ta vẫn không cần quan tâm đến validator
mà chỉ cần nó trả về false
cho phương thức valid?
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
context 'with valid data' do
it 'adds processed to data' do
validator = double(:validator, valid?: true)
expect(processor.process('foo', validator)).to eq('foo processed')
end
end
context 'with invalid data' do
it 'raises Error' do
validator = double(:validator, valid?: false)
expect { processor.process('foo', validator) }.to raise_error(DataProcessor::Error)
end
end
end
Đối với trường hợp này ta cần stub phương thức valid?
trả về false
validator = double(:validator, valid?: false)
Giờ chúng ta đã biết nó sẽ trả về exception với dữ liệu không hợp lệ và thêm chuỗi "processed" với dữ liệu hợp lệ. Bước cuối cùng là cần chắc chắn validator
đã gọi đến phương thức valid?
. Chúng ta sẽ viết 1 kì vọng validator
gọi đến phương thức valid?
Nếu bạn muốn đảm bảo đối tượng sẽ nhận bất kì message nào trong quá trình thực thi, bạn nên dùng mocks. Giờ hãy viết thêm 1 trường hợp để đảm bảo rằng phương thức process
đã gọi validator.valid?(data)
trong quá trình thực thi.
it 'calls validator.valid?' do
validator = double(:validator)
expect(validator).to receive(:valid?).with('foo').and_return(true)
processor.process('foo', validator)
end
Đối với trường hợp này, ta tạo ra một đối tượng đơn giản bằng double và thêm kì vọng cho nó. Chúng ta kì vọng nó sẽ nhận valid?
và foo
, trả về true
. Đó chính là sự khác biệt chính giữa mocks và stubs. Đối với stubs chúng ta allow
đối tượng nhận một thông điệp, còn với mocks ta expect
nó nhận.
Nếu chúng ta bỏ dòng này trong code:
raise Error unless validator.valid?(data)
Trường hợp cuối cùng sẽ fail, với lỗi sau:
(Double :validator).valid?("foo")
expected: 1 time with arguments: ("foo")
received: 0 times
Nếu như dùng stub (allow
) sẽ không bao giờ bị fail, nhưng nếu dùng mock, nó kì vọng validator
sẽ nhận valid?
ít nhất 1 lần.
Stub và mock không chỉ dùng cho double, nó còn có thể dùng có đối tượng thật.
class DataProcessor
Error = Class.new(StandardError)
def process(data)
raise Error unless Validator.new.valid?(data)
"#{data} processed"
end
end
class Validator
def valid?(data)
true
end
end
Chúng ta có thể thấy ở ví dụ trên là process giờ chỉ nhận tham số data
, và dùng Validator
để kiểm tra tính hợp lệ.
Vậy chúng ta có thể thay đổi specs để bao quát được hết các trường hợp hay không? Trong ví dụ này chúng ta có thể sử dụng các phương thức có sẵn của Rspec:
allow_any_instance_of
expect_any_instance_of
Chúng ta có thể sử dụng các phương thức trên cho bất kì instance nào củaValidator
.
require 'spec_helper'
describe DataProcessor do
let(:processor) { described_class.new }
context 'with valid data' do
it 'adds processed to data' do
# it works because true is default value for Validator
expect(processor.process('foo')).to eq('foo processed')
end
end
context 'with invalid data' do
it 'raises Error' do
allow_any_instance_of(Validator).to receive(:valid?).and_return(false)
expect { processor.process('foo') }.to raise_error(DataProcessor::Error)
end
end
it 'calls validator.valid?' do
expect_any_instance_of(Validator).to receive(:valid?).with('foo').and_return(true)
processor.process('foo')
end
end
Tất cả đã lại chuyển sang màu xanh. Hy vọng bài viết này có thể giúp bạn hiểu được sự khác biệt giữa stub và mock. Cảm ơn đã đọc đến hết bài viết!
Nguồn tham khảo: http://rubyblog.pro/2017/10/rspec-difference-between-mocks-and-stubs
All rights reserved