RSpec: sự khác biệt giữa mocks và stubs
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ó ...
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ủa Validator.
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