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 ...
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