Bốn sai lầm thường gặp khi viết Rspec

Đôi khi thà không viết test còn hơn viết test yếu (một cách sơ sài, cẩu thả). Bởi vì khi bạn không viết test, bạn biết bạn phải test mọi thứ, nhưng với cách viết test yếu, bạn đang tự đánh lừa mình và mọi người xung quanh rằng nó ổn. Làm thế nào để phát hiện ra cách viết test nào là yếu và chúng ta không bị phụ thuộc vào nó?

Dưới đây là 4 lỗi cơ bản mà chúng ta thường mắc phải trong quá trình viết test.

Chúng ta sẽ viết 1 class đơn giản, file test yếu và phân tích nó.

module Users
  class NameService
    def initialize(user)
      @user = user
    end
  
    def name
      if user.name.present?
        user.name
      else
        ::ExternalAPI.new(user).get_name
      end
    end
  
    private
    attr_reader :user
  end
end

Class của chúng ta sẽ trả về tên của user nếu nó tồn tại hoặc gọi tới một hàm API bên ngoài để trả về tên user trong bản thông tin cá nhân online. Giờ viết cho nó một file test:

require 'spec_helper'

describe Users::NameService do
  describe '#name' do
    it 'returns name' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)
      
      expect(service.name).to eq(user.name)
    end
    
    it 'returns name' do
      user = User.create!(name: nil)
      service = described_class.new(user)
      
      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

Giờ bạn nghĩ sao? Có gì sai ở đây?

1, Tài liệu khó hiểu

Chạy Rspec cùng với --format documentation sẽ không cho bạn biết được class hay method nào bị lỗi. Trong trường hợp này, nó sẽ trả về return name nhưng bạn không biết được một điều gì khác. Đọc test là 1 trong những cách tốt nhất để bạn hiểu về code cho dù bạn là junior hay senior, cho nên điều vô cùng quan trọng là phải viết phần mô tả cho ví dụ một cách đầy đủ và rõ nghĩa nhất. Hãy sửa nó thành thế này:

describe Users::NameService do
  describe '#name' do
    it 'returns user name if the user has a name' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)
      
      expect(service.name).to eq(user.name)
    end
    
    it 'returns user name from API if the user does not have a name' do
      user = User.create!(name: nil)
      service = described_class.new(user)
      
      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

Bạn có thấy sự khác biệt? Giờ bạn chỉ cần chạy Rspec cùng với documentation flag và bạn không phải nhìn vào code để biết rằng có chuyện gì đang xảy ra. Nếu bạn chưa hiểu documentation flag là gì thì ý mình đang nói đến phần này khi chạy Rspec:

Users::NameService
    #name
        returns user name if the user has a name
        returns user name from API if the user does not have a name

2, Không cô lập

Ở case thứ 2, chúng ta phải gọi tới phần code bên trong class ExternalAPI#get_name, trong khi trong unit tests chúng ta chỉ muốn cô lập một phần code (thường là 1 method) và chỉ test nó. Chúng ta không muốn bất cứ điều gì khác ảnh hưởng đến nó. Hãy giả định rằng class của bạn chạy tốt, nhưng lại có lỗi trong ExternalAPI#get_name, test của chúng ta vẫn fail và chúng ta phải fix 2 cái là Users::NameService#nameExternalAPI#get_name. Để tránh trường hợp này, chúng ta có thể dùng stub.

describe Users::NameService do
  describe '#name' do
    it 'returns user name if the user has a name' do
      user = User.create!(name: 'Mike Black')
      service = described_class.new(user)
      
      expect(service.name).to eq(user.name)
    end
    
    it 'returns user name from API if the user does not have a name' do
      user = User.create!(name: nil)
      external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
      allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
      
      service = described_class.new(user)
      
      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

Giờ thì phần test của chúng ta đã bị cô lập và còn chạy nhanh hơn trước. Kể cả khi có lỗi xảy ra bên trong API thì cũng không làm test bị fail được.

3, Sử dụng database vô ích

Chúng ta không nhất thiết phải gọi vào database để viết test. Hãy sử dụng stub một lần nữa để đẩy nhanh tốc độ test.

describe Users::NameService do
  describe '#name' do
    it 'returns user name if the user has a name' do
      user = instance_double(User, name: 'Mike Black')
      service = described_class.new(user)
      
      expect(service.name).to eq(user.name)
    end
    
    it 'returns user name from API if the user does not have a name' do
      user = instance_double(User, name: nil)
      external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
      allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
      
      service = described_class.new(user)
      
      expect(service.name).to eq('Mike Jr Black')
    end
  end
end

Giờ chúng ta không cần phải gọi vào database hay bất kì service phía ngoài nào. Test đã bị cô lập và cực kì nhanh.

4, Test phương thức private

Chúng ta không đề cập đến case này trong ví dụ, nhưng nó là một lỗi thường gặp. Chúng ta không cần test các phương thức private bởi vì chúng ta đã test các phương thức public và kết quả sinh ra bởi các phương thức private. Nếu bạn cảm thấy nên test phương thức private của mình, thì bạn có thể chia nhỏ code thành các class nhỏ hơn và test chúng riêng biệt.

Ngoài ra bạn có thể tham khảo trên trang này, để xem cách viết test sao cho dễ nhìn vào hiệu quả.

Nguồn tham khảo: http://pdabrowski.com/blog/ruby-on-rails/testing/rspec-4-common-tests-design-mistakes/

All Rights Reserved