Hướng Dẫn Viết RSpec Theo Một Cấu Trúc Hợp Lý
Bài đăng này đã không được cập nhật trong 3 năm
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ặcits
để 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
All rights reserved