12/08/2018, 11:58

Hướng Dẫn Viết RSpec Theo Một Cấu Trúc Hợp Lý

Lời mở đầu Ở Framgia phát triển ứng dụng bằng TDD được khuyến khích nhưng với những người chưa quen thì thường sẽ có những khó khăn nhất định trong việc viết test như thế nào, cấu trúc ra sao? Chính vì vậy tôi viết bài này để chia sẻ đến các thành viên Framgia chưa quen với việc viết unit ...

rspec.png

Lời mở đầu

Ở Framgia phát triển ứng dụng bằng TDD được khuyến khích nhưng với những người chưa quen thì thường sẽ có những khó khăn nhất định trong việc viết test như thế nào, cấu trúc ra sao? Chính vì vậy tôi viết bài này để chia sẻ đến các thành viên Framgia chưa quen với việc viết unit test có thể có một guideline để làm theo. Dựa trên một bài viết trên madetech cùng với kinh nghiệm bản thân.

Một đoạn test thông thường

Kinh nghiệm viết unit test của tôi được tăng lên qua các dự án làm cùng các kỹ sư người Nhật. Có một kỹ sư người Nhật đã nói với tôi rằng

Nếu bạn viết test cho model thì khi người khác đọc sẽ hiểu các methods của model đó dễ dàng hơn.

Nhưng một đoạn test làm nhiều việc sẽ khiến cho người đọc khó hiểu vì độ phức tạp. Xem thử một ví dụ sau:

describe Car do
  context "when the car is turned off" do
    it "should turn off the engine" do
      car = Car.new(brand: "Lamboghini", model: "Aventador")
      engine = Engine.new(name: "6.5L V12", power_on: true)

      car.engine = engine

      expect(car.power_on?).to be_true

      car.power_off

      expect(car.power_on?).to be_false
      expect(engine.power_on?).to be_false
    end
  end
end

Ví dụ nêu trên là một cách viết thường thấy không kể đến ngôn ngữ hay test framework nào mà bạn sử dụng. Nhưng đã có rất nhiều thứ xảy ra trong đoạn test đó:

  • Miêu tả sẽ test gì
  • Tạo các objects sử dụng cho việc test
  • Thiết lập state cho các objects
  • Xác nhận kết quả test case

Chúng có vẻ bị pha trộn vào nhau. Chúng ta đang thực sự test những thứ gì? Nếu như chúng ta có thể tái sử dụng các objects giữa các tests?

Cách giải quyết

Chúng ta đang có 2 đối tượng cần test ở đây, Car và Engine. Đoạn test trên nên chia thành test riêng cho từng class.

describe Car do
  let(:car) { FactoryGirl.create(:car, power_on: true) }

  subject { car }

  context "when turning off power" do
    before do
      car.power_off
    end

    its(:power_on?) { is_expected.to be_false }
  end

  context "when turning power on" do
    before do
      car.power_on
    end

    its(:power_on?) { is_expected.to be_true }
  end
end

Và test cho Engine:

describe Engine do
  let(:engine) { FactoryGirl.create(:engine, power_on: true) }

  subject { engine }

  context "when turning on and turning the car off" do
    before do
      engine.car.power_off
    end

    its(:power_on?) { is_expected.to be_false }
  end

  context "when turning off, and turning the car on" do
    before do
      engine.power_off
      engine.car.power_on
    end

    its(:power_on?) { is_expected.to be_true }
  end
end

Đừng để ý quá nhiều vào nội dung của những dòng test trên, cái mà bạn cần rút ra đó là cấu trúc của đoạn test và sự mạch lạc của nó.

Chia nhỏ cấu trúc

Cấu trúc test trên dựa trên mô hình Given-When-Then, bắt nguồn từ Behaviour Driven Development. Trong đó:

  • Given: thiết lập các objects để test sử dụng các block let và before
  • When: chỉ định đổi tướng (subject) test
  • Then: sử dụng it hoặc its để xác nhận kết quả test

Thiết lập bối cảnh

Sử dụng let cùng với FactoryGirl để thiết lập đối tượng test cho phép chúng ta có được những giá trị mặc định dễ hiểu đến từ những file factory riêng khiến cho đoạn test "sạch sẽ" hơn. Những giá trị lưu bên trong các let block có thể tái sử dụng được giữa các tests, khi mà giá trị được cache lại sau lần sử dụng đầu tiên, nhưng không thể tái sử dụng giữa các lần xác nhận kết quả test (expectation/assertion)

Chúng ta có thể sử dụng giá trị của let khi thiết lập test và chúng ta sẽ sử dụng giá trị của let trong before block.

Tại sao lại sử dụng before block mà không thiết lập những giá trị đó bên trong subject? Vì nó dễ đọc hơn, giống tiếng Anh hơn. Đối tượng chỉ là một object và việc khởi tạo object đó nên được thực hiện bởi let và before block.

Thay vì:

subject do
  setup_objects
  do_something_more
  test_subject
end

Hãy viết:

before do
  setup_objects
  do_something_more
end

subject { test_subject }

Giá trị mong đợi từ các tests

Ngay khi các test objects được thiết lập và sẵn sàng, đã đến lúc xác nhận kết quả. Dựa vào đối tượng test, bạn sẽ lựa chọn cú pháp it hoặc its

Khi test các objects, sử dụng its sẽ mang lại được sự linh hoạt trong việc test các thuộc tính khác nhau của object đó bằng cách truyền vào tên thuộc tính

subject { user }
its(:name) { is_expected.to eq("Johny Lâu Ra") }

Câu trên cơ bản là sẽ chạy câu sau:

it { expect(user.name).to eq("Johny Lâu Ra") }

Nếu bạn có nhiều câu xác nhận kết quả test thì bạn có thể viết nhiều câu giống trên lặp lại với các thuộc tính khác nhau mà đoạn test vẫn "sạch sẽ"

Với những loại data khác có nhiều built-in matchers có thể sử dụng được, ví dụ như Array:

it { is_expected.to have_exactly(100).items }

Khá là dễ hiểu đúng không?

Cố gắng giữ những câu xác nhận càng đơn giản càng tốt. Nếu mà viết quá nhiều câu xác nhận vào trong một context, có lẽ là đoạn test đó hơi phức tạp và cần chia nhỏ làm các context nhỏ hơn.

Lời kết

Với vô số các helpers được viết trong RSpec, bạn hoàn toàn có thể viết được những đoạn test cs cấu trúc rõ ràng, rành mạch kèm theo đó là sự dễ đọc cho đồng nghiệp. Hãy coi trọng sự dễ đọc và đưa nó lên làm tiêu chí hàng đầu khi viết test vì trong một hệ thống phức tạp những dòng test chính là những tài liệu tốt nhất cho những kỹ sư khác cùng tham gia dự án.

Reference

Focus with well structured RSpec tests -by David Winter on 30th July 2015

0