Sử dụng Stubs khi viết Rspec trong Rails

1. Giới thiệu

RSpec là một công cụ test vô cùng mạnh mẽ đồng thời cung cấp nhiều tính năng phong phú. Một trong số đó là khả năng stub một phương thức của một đối tượng hoặc một class. Thay vì phải thực thi một hàm một cách bình thường, stub sẽ trả về một giá trị cứng và không bao giờ thực sự chạy phương thức đó.

2. Ví dụ

student = load_student_from_database(id: 77)
allow(student).to receive(:grades).and_return(['A', 'A+', 'C'])

puts(student.grades)

Ở dòng thứ 2 có ý nghĩa rằng khi gọi student.grades thì sẽ trả về một array với giá trị là ['A', 'A+', 'C']. Nhưng trên thực thế phương thức grades không bao giờ được gọi.

Tiếp tục xây dựng ví dụ trên:

Thử tưởng tượng rằng tồn tại một class Student và class GpaCalculator và chúng ta phải viết test cho những class này.

Class GpaCalculator được dùng để tham chiếu điểm. Ví dụ A tương đương với 4.0 và B tương đương với 3.0. Ta có thể viết test như sau:

describe GpaCalculator do
  it “calculates one A to be 4.0do
    student = Student.new
    allow(student).to receive(:grades).and_return([A])

    expect(GpaCalculator.new(student).calculate).to eq(4.0)
  end

  it “calculates one B to be 3.0do
    student = Student.new
    allow(student).to receive(:grades).and_return([B])

    expect(GpaCalculator.new(student).calculate).to eq(3.0)
  end

  it “calculates one A and one B to be 3.5do
    student = Student.new
    allow(student).to receive(:grades).and_return([A,B])

    expect(GpaCalculator.new(student).calculate).to eq(3.5)
  end
end

Bạn có thể làm gọn code ở đây một chút bằng cách:

describe GpaCalculator do
  let(:student) { Student.new }
  subject { GpaCalculator.new(student) }

  it "calculates one A to be 4.0" do
    allow(student).to receive(:grades).and_return(["A"])

    expect(subject.calculate).to eq(4.0)
  end

  it "calculates one B to be 3.0" do
    allow(student).to receive(:grades).and_return(["B"])

    expect(subject.calculate).to eq(3.0)
  end

  it "calculates one A and one B to be 3.5" do
    allow(student).to receive(:grades).and_return(["A", "B"])

    expect(subject.calculate).to eq(3.5)
  end
end

Nhiều người sẽ dừng lại ở đây và tiếp tục viết test cho những class khác. Với một class nhỏ không phức tạp thì cách trên có vẻ không có vấn đề gì. Nhưng nếu trên một class lớn với những rspec phức tạp thì việc refactor code triệt để là một vấn đề lớn giúp cho việc bảo trì trở nên dễ dàng hơn.

Trong trường hợp này, bạn sử stub cùng một phương thức nhiều lần và trả về một array khác nhau trong mỗi test. Có một kỹ thuật để giúp viết gọn và dễ đọc hơn.

describe GpaCalculator do
  let(:grades) { [] }
  let(:student) { Student.new }
  subject { GpaCalculator.new(student) }

  before :each do
    allow(student).to receive(:grades).and_return(grades)
  end

  it "calculates one A to be 4.0" do
    grades << "A"

    expect(subject.calculate).to eq(4.0)
  end

  it "calculates one B to be 3.0" do
    grades << "B"

    expect(subject.calculate).to eq(3.0)
  end

  it "calculates one A and one B to be 3.5" do
    grades << "A"
    grades << "B"

    expect(subject.calculate).to eq(3.5)
  end
end

Chúng ta chuyển đoạn stub tách ra khỏi mỗi bài test và đặt vào before. Nhiều người có thể không thích cách viết block before nên còn một cách nữa là đưa stub và let :

let(:student) {
  student = Student.new
  allow(student).to receive(:grades).and_return(grades)
  student
}

Một cách hoàn toàn khách nữa đó là bạn sử dụng double thay cho allow. Ví dụ:

let(:student) { instance_double('Student', grades: grades) }

Và bạn có phiên bản hoàn chỉnh như bên dưới :

describe GpaCalculator do
  let(:grades) { [] }
  let(:student) { instance_double("Student", grades: grades) }
  subject { GpaCalculator.new(student) }

  it "calculates one A to be 4.0" do
    grades << "A"

    expect(subject.calculate).to eq(4.0)
  end

  it "calculates one B to be 3.0" do
    grades << "B"

    expect(subject.calculate).to eq(3.0)
  end

  it "calculates one A and one B to be 3.5" do
    grades << "A"
    grades << "B"

    expect(subject.calculate).to eq(3.5)
  end
end

3. Tổng kết

Mặc dù các đoạn mã RSpec không phải là các đoạn mã để chạy chương trình nhưng vẫn hết sức quan trọng đối với project của bạn. Nếu RSpec khó đọc, khó sửa đổi thì khi mở rộng hệ thống sẽ bị ảnh hưởng. Vì vậy hãy cố gắng refactor và làm cho chúng dễ đọc, dễ hiểu nhất có thể.


All Rights Reserved