RSpec Subject, Helpers, Hooks và Exception Handling
Bài đăng này đã không được cập nhật trong 7 năm
Sau phần đầu tiên giới thiệu về Rspec, chúng ta đã hiểu được cấu trúc cơ bản của nó và bước tiếp theo là học cách sử dụng những cú pháp cơ bản thường dùng.
Trong phần tiếp theo này, chúng ta sẽ tìm hiểu cách sử dụng subject, let helper method, before hook và exception handling. Từ đó chũng ta sẽ có được nền tảng cơ bản để có thể bắt đầu sử dụng RSpec trong project Ruby và áp dụng những tính năng của nó thành thạo hơn.
Test Subjects
Ví dụ chúng ta cần viết một chương trình đơn giản cho những vận động viên chạy bộ, họ cần ghi lại dữ liệu những lần chạy và tổng hợp dữ liệu hàng tuần. Những thông tin cơ bản của một lần chạy bao gồm khoảng cách chạy, thời gian chạy và khoảng thời gian chạy hết quãng đường đó.
Từ bài toán đó chúng ta hiểu rằng phải xây dựng một class Run gồm 3 thuộc tính và có thể miêu tả đơn giản như sau:
describe Run do
describe "attributes" do
subject do
Run.new(:duration => 32,
:distance => 5.2,
:timestamp => "2014-12-22 20:30")
end
it { is_expected.to respond_to(:duration) }
it { is_expected.to respond_to(:distance) }
it { is_expected.to respond_to(:timestamp) }
end
end
Trong ví dụ trên ta khai báo một subject instance của class Run. Chúng ta phải định nghĩa vì có rất nhiều test cùng làm việc chung với subject. RSpec hiểu đó là một object có thể respond với các method ví dụ như duration. Expect ở đây là việc sử dụng RSpec's built-in respond_to matcher.
Cú pháp trên sẽ thuận tiện khi bạn có thể trách trùng lặp giữa một matcher và một string trong test example. Nếu có sự trùng lặp chúng ta có thể viết ví dụ trên bằng cách sau:
it "responds to '#duration'" do
expect(subject).to respond_to(:duration)
end
Trong thực tế, nếu object có thể khai báo mà không cần parameters, chúng ta có thể rút gọn như sau:
describe Run do
it { is_expected.to respond_to(:duration) }
end
VÌ chúng ta đã làm class pass desscribe block và RSpec đã khởi tạo một subject trong global example group cho subject { Run.new }. Dưới đây là luồng cơ bản của code mà bạn có thể thường xuyên thấy trong các ứng dụng Rails:
describe Post do
it { is_expected.to validate_presence_of(:title) }
end
Subject cũng có thể đương tham chiếu rõ ràng thông qua subject:
describe Run do
subject do
Run.new(:duration => 32,
:distance => 5.2,
:timestamp => "2014-12-22 20:30")
end
describe "#timestamp" do
it "returns a DateTime" do
expect(subject.timestamp).to be_a(DateTime)
end
end
end
Tuy nhiên, chúng ta không nên sử dụng subject như trên. Còn có các cách khác thể hiện rõ ràng hơn mà chúng ta tìm hiểu tiếp sau:
Before Hooks
Để viết một test, chungs ta thường đưa tất cả về một trạng thái khỏi tạo đầu tiên. Ví dụ, chúng ta miêu tả một method mà trả về tổng số những lần chạy đã được ghi lại thông tin: Run.count. Method có thể tùy chọn số parameter nhận vào đến giới hạn phạm vi một tuần. Trước khi gọi method, chúng ta cần ghi lại số lần chạy trước. RSpec before hook là một cách tiện lợi để cấu trúc code, nó được chạy trước mỗi example, ví dụ như sau:
describe RunningWeek do
describe ".count" do
context "with 2 logged runs this week and 1 in next" do
before do
2.times do
Run.log(:duration => rand(10),
:distance => rand(8),
:timestamp => "2015-01-12 20:30")
end
Run.log(:duration => rand(10),
:distance => rand(8),
:timestamp => "2015-01-19 20:30")
end
context "without arguments" do
it "returns 3" do
expect(Run.count).to eql(3)
end
end
context "with :week set to this week" do
it "returns 2" do
expect(Run.count(:week => "2015-01-12")).to eql(2)
end
end
end
end
end
Chú ý rằng chúng ta không những phải tránh trùng lặp mà còn phải đặt tên sao cho dễ đọc, có ý nghĩa, thể hiện được đầy đủ bản chất của test.
Khi viết before, có một cách tương đương là viết before(:each), có nghĩa là "chạy code này trước mỗi example". Bạn có thể viết before(:all) để chạy code một lần duy nhất trước khi chạy context được chỉ định. nếu cần thiết, bạn cũng có thể định nghĩa một after hook. Tìm hiểu thêm tại đây RSpec documentation
Let Helper
RSpec let helper là cách để định nghĩa những object đọc lập cho test example. Nếu bạn cần những thứ giống nhau trong nhiều example và không thể tạo 1 subject, chúng ta nên dùng let.
Code đươc đặt bên trong let block có nghĩa: Nó sẽ chỉ thực thi một lần duy nhất lúc lần đầu test example được gọi và được cache lại để cho những lần gọi sau ở trong cùng example. Nếu bắt buộc method được gọi lại nhiều lần, hãy dùng let:
Ví dụ chúng ta cần đưa ra dữ liệu thống kê về tổng những lần chạy trong tuần thay thế RunningWeek class.
describe RunningWeek do
let(:monday_run) do
Run.new(:duration => 32,
:distance => 5.2,
:timestamp => "2015-01-12 20:30")
end
let(:wednesday_run) do
Run.new(:duration => 32,
:distance => 5.2,
:timestamp => "2015-01-14 19:50")
end
let(:runs) { [monday_run, wednesday_run] }
let(:running_week) { RunningWeek.new(Date.parse("2015-01-12"), runs) }
describe "#runs" do
it "returns all runs in the week" do
expect(running_week.runs).to eql(runs)
end
end
describe "#first_run" do
it "returns the first run in the week" do
expect(running_week.first_run).to eql(monday_run)
end
end
describe "#average_distance" do
it "returns the average distance of all week's runs" do
expect(running_week.average_distance).to be_within(0.1).of(5.4)
end
end
end
Ngược lại với ví dụ về before hook khi mà chúng ta cần chuẩn bị data trước nhưng không cần quan tâm về nó lúc sau. Trong spec này chúng ta cần chuẩn bị data và gọi chúng trong test example.
Instance Variables in Before Hooks vs Let
Ở đây, chúng ta có thể sử dụng biến instance trong before block thay vì let:
describe RunningWeek do
before do
@monday_run = Run.new(...)
end
end
Có ba cách tiếp cận trong cách sử dụng let:
- Sử dụng let để định nghĩa tất cả object độc lập và giữ cho example ngắn gọn nhất.
- Sử dụng nó hạn chế để tránh trùng lặp bằng việc định nghĩa "variables" khi cần gọi tới những thứ giống nhau trong multiple test example.
- Không sử dụng có tất cả, chỉ dựa vào biến instance trong before hook.
Chúng ta nên dùng cách đầu tiên, kết hợp với data có trong before hook khi cần thiết. Điều đó giúp code dễ đọc, ít lỗi phát sinh và có thể có performance tốt hơn.
Exception Handling
CHúng ta cần xử lý khi method hoặc block code raise exception bằng việc sử dụng raise_error matcher hoặc its equivalent, raise_exception:
describe RunningWeek do
describe "initialization" do
context "given a date which is not a Monday" do
it "raises a 'day not Monday' exception" do
expect { RunningWeek.new(Date.parse("2015-01-13"), []) }.to raise_error("Day is not Monday")
end
end
end
end
Về cơ bản chúng ta có thể thu hẹp expectation dựng vào error message. Nếu code raise exception và không theo matcher, spec fail.
Tổng kết
Chúng ta đã tìm hiểu cách sử dụng subject, let helper method, before hook và exception handling. Từ đây chũng ta đã có được nền tảng cơ bản để có thể bắt đầu sử dụng RSpec trong project Ruby và áp dụng những tính năng của nó thành thạo hơn. Trong phần tiếp theo chúng ta sẽ tìm hiểu về Mocking
All rights reserved