07/09/2018, 15:57

DÙNG LET HAY KHÔNG?

Mở đầu Khi chúng ta (lập trình viên Ruby on Rails) viết test, cho dù sử dụng rspec hay minitest, đều sẽ dùng let rất nhiều. Cú pháp let giúp chúng ta viết code dễ dàng và tiện lợi hơn rất nhiều: def activable? inactive? && !blacklist? end describe '#activable?' do let(:inactive?) ...

Mở đầu

Khi chúng ta (lập trình viên Ruby on Rails) viết test, cho dù sử dụng rspec hay minitest, đều sẽ dùng let rất nhiều. Cú pháp let giúp chúng ta viết code dễ dàng và tiện lợi hơn rất nhiều:

def activable?
  inactive? && !blacklist?
end

describe '#activable?' do
  let(:inactive?) { true }
  let(:blacklist?) { false }
  subject { build_stubbed(:user) }

  before do
    expect(subject).to receive(:inactive?).and_return(inactive?)
    expect(subject).to receive(:blacklist?).and_return(blacklist?)
  end

  context 'when user is active' do
    let(:inactive?) { false }
    it { is_expected.not_to be_activable }
  end

  context 'when user is blacklist' do
    let(:blacklist?) { true }
    it { is_expected.not_to be_activable }
  end

  context 'otherwise' do
    it { is_expected.to be_activable }
  end
end

Như các bạn thấy ở trên, ở mỗi context chúng ta override một giá trị duy nhất tương ứng với trường hợp này. Điều này giúp ta hai điều:

  • Tránh lặp lại code, giúp code ngắn và dễ đọc hơn
  • Thể hiện rõ từng điều kiện của từng context

Tuy nhiên, gần đây, trong một cuộc thảo luận với bạn của mình, mình được biết một số công ty như Thoughtbot và Thoughtworks đã khuyến cáo không sử dụng cách làm này.

Vấn đề của let

Các kỹ sư của Thoughtbot có một bài nói về lý do không sử dụng let, qua đó đề cập đến hai vấn đề:

  • Nhập nhằng trong bước setup dữ liệu. Do let là lazyload, đôi khi bạn sẽ phải gặp phải lỗi là thời điểm object được khởi tạo sau khi test được thực hiện:
describe '.active' do
  let(:user) { create(:user, active: active) }
  subject { User.active }

  describe 'when user is active' do
    let(:active) { true }    
    it { is_expected.to eq [user] } # this test will fail
  end

  # ...
end
  • Dễ gặp lỗi và tight coupling giữa các dữ liệu được setup, đặc biệt là khi test phức tạp.

Hãy cùng xem một ví dụ khác ở đoạn code sau.

Ở ví dụ trên, trong phần sẽ rất khó để người đọc biết được vị trí của lỗi sai là nằm ở đâu do biến attributes đã bị override quá nhiều lần. Họ cũng không thể phân biệt được đâu là phần setup dữ liệu, đâu là phần test.

Giải pháp được đưa ra

Các kỹ sư của Thoughtbot đưa ra một số giải pháp cho tình trạng trên:

  • Chấp nhận code duplicate ở một số trường hợp
  • Tuân thủ theo nguyên tắc 4 bước trong testing: test phải phân tách rõ ràng 4 bước setup, execute, assertion và tear down.
  • Không sử dụng let, subject hoặc các method tương tự.

Ta sẽ refactor lại testcase ở bên trên theo các tiêu chí này, ta có đoạn code ở link sau.

Ta có thể thấy là đoạn test mới này dễ đọc hơn rất nhiều so với ví dụ ở phần 2. Chúng ta biết rõ ràng dữ liệu được setup như thế này, do đó sẽ dễ dàng debug hoặc chỉnh sửa các test này khi cần thiết.

Tuy nhiên, không phải là không có vấn đề khi sử dụng phương pháp này. Khi test của bạn ngày càng dài, bạn sẽ càng phải viết nhiều dupplicate code, do đó test của bạn sẽ ngày càng khó đọc, khó debug. Để tránh tình trạng này, bạn sẽ dần phải refactor code để giảm duplication và chia tách các concern. Dần dần, với quá nhiều redirection, code của bạn cũng sẽ trở lại tình trang khi sử dụng let. Mặc dù vậy, do đây chỉ là ruby code và bạn nắm toàn quyền quản lý code, bạn có thể tạo ra những abstraction dễ đọc và hiểu hơn thông qua OOP hoặc FP.

Kết luận

Mình vẫn chưa cảm thấy việc loại bỏ hoàn toàn let khỏi test là hợp lý. Mỗi phương pháp đều có lợi điểm riêng và phù hợp với từng trường hợp nhất định. Dù vậy, việc consistent khi viết code (cũng như viết test) là điều cần thiết mà bất cứ project nào cũng cần có, và mình vẫn còn lấn cấn ở điểm này. Do vậy, hi vọng qua bài viết này các bạn có thể cho mình thêm ý kiến về vấn đề này.

0