12/08/2018, 15:45

RSpec Subject, Helpers, Hooks và Exception Handling

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

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

0