RSpec mocks

RSpec Mocks

1. Giới thiệu

Trong phát triển phần mềm thì việc viết unit test là vô cùng quan trọng. Viết unit test cẩn thận thì quá trình phát triển kiểm thử sẽ dễ dàng hơn, ít lỗi phát sinh hơn. Tuy nhiên rất nhiều lập trình viên thường không coi trọng việc viết unit test, thường chỉ tập trung vào code điều này thật sự không tốt. Dưới đây tôi xin giới thiệu về công cụ hỗ trợ viết unit test rất hữu ích với rspec mocks. Hi vọng bạn sẽ cảm thấy thú vị với một tư tưởng viết test mới của rspec mocks.

1.1 RSpec Mocks là gì?

RSpec mocks là một mocking framework cho rspec với các method stubs, fakes, message expectations, mock object. Việc sử dụng rspec mocks làm cho việc cài đặt unit test của bạn trở nên dễ dàng và hiệu quả hơn. Bạn có thể lựa chon mock objects trong một số trường hợp sau:

  • Tạo một object của một class nào đó chưa được cài đặt
  • Khi làm việc với các đối tượng mà giá trị của method trả về bị phụ thuộc dữ liệu từ bên ngoài, hoặc khó kiểm soát.
  • Tránh việc cài đặt phức tạp hoặc các sự phụ thuộc lớn của các objects khi cài đặt test.
  • Tránh việc lặp lại test khi đã có test được viết từ trước.

1.2 Messages and Methods

Messages và methods là hai khái niệm khá giống nhau thường hay gây nhầm lẫn. Trong lập trình hướng đối tượng thì các object liên lạc với nhau bằng việc gửi message tới các đối tượng khác. Khi một object nhận 1 message nó sẽ gọi đến method thực thi tương ứng với message nhận được.

1.3 Method Stubs và Message Expectations

Method stubs và message expectations cũng là hai khái niệm tương đối giống nhau. Tuy nhiên bạn cần phải phân biệt rõ và sử dụng 2 loại này đúng cách. Với method stubs thì thường chỉ quan tâm đến kết quả trả về của một đối tượng khi nhận một message nào đó (fake return values)

allow(object).to receive(:test_method).and_return("test values")
...

Với message epectations cũng có có tạo ra fake giá trị trả về tương tự như vói method stubs. Điểm khác biệt ở đây là trong quá trình chạy thì object được expect phải nhận được message đã được chỉ rõ trong expect và số lần đúng như trong expect. Còn với method stubs thì chạy bao nhiêu lần cũng được.

expect(object).to receive(:test_method).with("test result")
...

Chi tiết về hai loại này sẽ được nói rõ ở phần Allowing messagesExpecting messages.

2 RSpec Mocks Basic

2.1 Test double

Test double (mock object, double object) là một object đơn giản nhất (simplified object) của class RSpec::Mocks::Double dùng thể thay thế cho một object nào đó trong khi test. Với double object bạn có thể cho object này nhận các message khác nhau một cách tùy ý với allow()receive().

obj = double
obj.test #=> error here
allow(obj).to receive(:test)
obj.test #=> nil

Chú ý: RSpec::Mocks::Double include một module là RSpec::Mocks::TestDouble nếu bạn muốn một class nào đó có những tính năng giống với Double bạn có thể include module này.

2.2 Method Stubs (Allowing messages)

Dùng để định nghĩa các method ảo (fake methods, method stubs) cho đối tượng. Trong Rspec Mocks nó được sử dùng với allow()receive() methods.

Kết quả trả về của method có được cung cấp bởi and_return() hoặc một block lệnh. Mặc định kết quả trả về sẽ là nil.

obj = double
allow(obj).to receive(:test)
obj.test #=> nil
allow(obj).to receive(:test).and_return("value")
obj.test #=> "value"
allow(obj).to receive(:test){1}
obj.test #=> 1

Trong trường hợp cần chỉ rõ arguments của method rspec mocks cung cấp method with() để thực hiện việc này

obj = double
allow(obj).to receive(:test).with(1).and_return("test1")
obj.test #=> error here
obj.test(1) #=> 1

Bạn cũng có thể tạo một dãy kết quả trả về cho các lần gọi method với and_return() và tham số là các kết quả phân cách nhau bởi dấu ,

o = double
allow(o).to receive(:test).and_return(1, 2)
o.test #=> 1
o.test #=> 2
o.test #=> 2

Chú ý: Với object của Double thì bạn có thể dùng allow()receive() một cách tùy ý. Còn đối với các object của class khác thì bạn chỉ có thể dùng được với các method mà class đó định nghĩa và có thể thay đổi kết quả trả về của method đó

o = Object.new
o.to_s #=> "#<Object:0x00000006a5ba58>"
allow(o).to receive(:to_s).and_return("o")
o.to_s #=> "o"
allow(o).to receive(:test) #=> error here

Trong một số trường hợp cần thêm method cho class trong test bạn có thể dùng define_methoddefine_singleton_method

Object.define_method(:instance_method)
o = Object.new
o.instance_method #=> nil
allow(o).to receive(:instance_method).and_return("instance method")
o.instance_method #=> "instance method"
Oject.define_singleton_method(:class_method){|args|}
Object.class_method "args" #=> nil
allow(Object).to receive(:class_method).and_return("class method")
Object.class_method "args" #=> "class method"

2.3 Message expectations

Message expectations khá giống với method stubs, đều có thể tạo các method ảo cho đối tượng và giả định giá trị trả về của method. Điểm khác biệt là với message expectations thì trong quá trình chạy một test kể từ khi expect thì object phải gọi đến method (nhận được message) với số lần giống như theo kịch bản. Nếu object không nhận được message giống với expect hoặc số lần nhận message khác với expect thì test đó sẽ fail. Dưới đây là một ví dụ test đơn giản với message expectations

class A
  def method1
    method2
  end

  def method2
    method3
    method3
  end

  def method3
  end
end

Như trên ta có một class A với 2 method là method1method2 ta muốn test method1. Thông thường ta sẽ test giống như với method2 với các input và output. Nếu dùng rspec mocks thì bạn không cần phải lặp lại test giống như với method2 mà bạn chỉ cần expect là method2 được gọi đúng 1 lần khi method1 được gọi và kết quả trả về đúng với kết quả của method2

describe A do
  context "method1" do
    subject{a.method1}

    let(:a){A.new}
    let(:expected){"expected"}

    before do
      expect(a).to receive(:method2).and_return(expected)
    end

    it{is_expected.to eq expected}
  end
end

Như vậy ta đã test xong cho method1 khá đơn giản và không bị phụ thuộc vào method2 khi method2 thay đổi.

Trong trường hợp ta muốn kiểm tra method được gọi bao nhiêu lần khi chạy test bạn có thể dùng exactly()

describe A do
  ...
  context "method2" do
    subject{a.method2}

    let(:a){A.new}
    let(:expected){"expected"}

    before do
      expect(a).to receive(:method3).exactly(2).and_return(expected)
    end

    it{is_expected.to eq expected}
    end
end

3. Kết luận

Phần trên đã hướng dẫn sử dụng một số thành phần quan trọng của rspec mocks với allowing messages, expecting messages, mock object trong rspec. Tôi hy vọng bài viết này giúp bạn phần nào hiểu được về rspec mocks và có thể sử dụng chúng để viết unit test một cách hiệu quả cho project của mình.

Bạn có thể tham khảo chi tiết hơn tại

  1. Documents
  2. RSpec Mocks github