12/08/2018, 14:40

Viết test cho tương tác SMS trong Rails

Khi xây dựng một ứng dụng mà có tính năng gửi SMS nhằm mục đích xác thực hay thông báo, chúng ta thường sử dụng một dịch vụ bên ngoài như Twilio để xử lý việc gửi một tin nhắn thực. Khi viết unit test cho các đoạn codes mà tương tác với SMS, bạn có thể dễ dàng stub out việc gửi một tin nhắn thực ...

Khi xây dựng một ứng dụng mà có tính năng gửi SMS nhằm mục đích xác thực hay thông báo, chúng ta thường sử dụng một dịch vụ bên ngoài như Twilio để xử lý việc gửi một tin nhắn thực. Khi viết unit test cho các đoạn codes mà tương tác với SMS, bạn có thể dễ dàng stub out việc gửi một tin nhắn thực để giữ cho test của bạn cô lập. Nhưng với feature specs thì sao?

Viết feature specs

Giả sử có 2 kịch bản như sau:

Khi tôi thực hiện mua hàng, tôi muốn nhận được một liên kết tới hóa đơn thông qua một tin nhắn SMS xác nhận

Khi tôi đăng nhập, tôi muốn được hỏi cả mật khẩu của tôi và bốn chữ số được gửi cho tôi thông qua SMS nhằm tăng cường an ninh hơn cho tài khoản của tôi.

Chúng đều yêu cầu chúng ta tương tác với một tin nhắn SMS. Lý tưởng nhất, một feature spec sẽ giống như sau:

feature "signing in" do
  scenario "with two factors" do
    user = create(:user, password: "password", email: "user@example.com")

    visit root_path
    click_on "Sign In"

    fill_in :email, with: "user@example.com"
    fill_in :password, with: "password"
    click_on "Submit"

    secret_code = SMS::Client.messages.last # this would be so nice
    fill_in :code, with: secret_code
    click_on "Submit"

    expect(page).to have_content("Sign out")
  end
end

Nếu chúng ta đã có một vài cơ chế cho việc truy cập vào các tin nhắn đã được gửi từ các test thì mọi chuyện đã trở nên dễ dàng hơn. Các thư viện SMS client không cung cấp tính năng này bởi vì nó sẽ là một sự lãng phí vô nghĩa của bộ nhớ để lưu trữ tất cả tin nhắn được gửi trong production.

Xây dựng một fake SMS client

Một giải pháp tốt ở đây là tạo một SMS client riêng cho việc test bắt chước API của một client thật thay vì gửi một tin nhắn SMS lưu trữ trong bộ nhớ.

Ví dụ, nhìn vào document của gem Twilio Ruby, chúng ta có thể thấy rằng API của họ như sau:

# set up a client to talk to the Twilio REST API
@client = Twilio::REST::Client.new(account_sid, auth_token)

# send an SMS
@client.messages.create(
  from: '+14159341234',
  to: '+16105557069',
  body: 'Hey there!'
)

Hãy xây dựng một fake client bước chước API này như sau:

class FakeSMS
  Message = Struct.new(:from, :to, :body)

  cattr_accessor :messages
  self.messages = []

  def initialize(_account_sid, _auth_token)
  end

  def messages
    self
  end

  def create(from:, to:, body:)
    self.class.messages << Message.new(from: from, to: to, body: body)
  end
end

Stubbing the constant

RSpec cung cấp một phương thức stub_const cho phép chúng ta stub các constant trong specs. Trong spec_helper, chúng ta có thể làm như sau:

# spec/spec_helper.rb

RSpec.configure. do |config|
  config.before(:each) do
    stub_const("Twilio::REST::Client", FakeSMS)
  end
end

Khi đó, Twilio::REST::Client sẽ tham chiếu đến FakeSMS

Rails loading

Nếu như stubbing constants có thể làm cho bạn cảm thấy khó chịu, bạn có thể tận dụng lợi thế của hệ thống tải các lớp của Rails. Rails sẽ chỉ cố gắng load các constant nếu như nó chưa được trỏ đến một cái gì đó. Bằng cách định nghĩa Twilio::REST::Client trước khi gem Twilio được load, bạn có thể tránh được bất kì lỗi tái định nghĩa constant nào.

# config/initializers/fake_twilio.rb

if Rails.env.test?
  Twilio::REST::Client = FakeSMS
end

Cấu hình

Twilio code có thể được wrap bên trong một adapter như sau:

class SMSClient
  cattr_accessor :client
  self.client = Twilio::REST::Client

  def initialize
    @client = self.class.client.new(
      ENV.fetch("TWILIO_ACCOUNT_SID"),
      ENV.fetch("TWILIO_AUTH_TOKEN"),
    )
  end

  def send_message(from:, to:, body:)
    @client.messages.create(from: from, to: to, body: body)
  end
end

Sau đó, trong spec_helper, chúng ta có thể thay đổi cài đặt như sau:

# spec/spec_helper.rb

SMSClient.client = FakeSMS

Reset hàng đợi chứa các tin nhắn

Bạn có thể sẽ muốn reset lại FakeMS.messages giữa mỗi các test. Để làm điều này, thêm đoạn sau vào trong spec_helper:

RSpec.configure do |config|
  config.before :each, type: :feature do
    FakeSMS.messages = []
  end
end

Kết luận

Sau khi có một vài điều chỉnh, feature spec của chúng ta sẽ có thể chạy thành công:

feature "signing in" do
  scenario "with two factors" do
    user = create(:user, password: "password", email: "user@example.com")

    visit root_path
    click_on "Sign In"

    fill_in :email, with: "user@example.com"
    fill_in :password, with: "password"
    click_on "Submit"

    last_message = FakeSMS.messages.last # this now returns a message object
    fill_in :code, with: last_message.body # the code is the body of the message
    click_on "Submit"

    expect(page).to have_content("Sign out")
  end
end

Sử dụng một fake SMS client là một cách đẹp đẽ để cho phép feature specs test các flow yêu cầu tương tác với tin nhắn SMS. Phương pháp này không chỉ sử dụng được với riêng Twilio mà còn sử dụng được với bất kì nhà cung cấp SMS nào.

0