Learn RSpec (part I)

Mỗi một dòng code viết ra đều phải qua quá trình với rất nhiều kiểm tra chặt chẽ. Nhằm mục đích giảm thiểu công sức bỏ ra để kiểm tra mỗi lần phải viết lại code cũng như đảm bảo chất lượng đầu ra, chúng ta có rất nhiều công cụ để giúp test code tiện lợi và logic hơn, một trong số đó là RSpec. Trong bài viết này tôi sẽ giới thiệu về cách viết cũng như test Ruby code bẳng RSpec, dựa theo mô hình BDD (Behavior Driven Development)

RSpec là một công cụ test dành cho Ruby, được sử dụng phioir biến trong các ứng dụng thương mại. Mặc dù nó rất mạnh và có DSL (domain-specific language) mạnh, nó lại có thể được sử dụng rất đơn giản và bạn có thể dễ dàng làm quen với nó. Bài viết này sẽ giới thiệu cho bạn các test dù bạn chưa có kinh nghiệm với RSpec hoặc thậm chí chưa từng test bao giờ.

Ý tưởng dựa theo mô hình BDD

Để hiểu được tại sao RSpec lại đi theo hướng này, chúng ta phải hiểu xuất phát điểm của mô hình BDD, mô hình TDD

Ý tưởng của test-driven development (TDD) đơn giản rằng thay vì viết test cho những dòng code mà chúng ta viết ra, ta sẽ làm việc với vòng lặp "red-green":

Viết các test case nhỏ nhất có thể phù hợp mà chúng ta cần cho chương trình. Run test và xem tại sao nó fail. Từ đó dẫn tới cách để bạn viết lại đoạn code đó sao cho nó pass. Viết một đoạn code với mục địch làm cho test case pass. Run toàn bộ test, sau đó lặp lại 2 bước bên trên tới khi toàn bộ test pass. Quay lại và refactor toàn bộ code mới, đơn giản nó cũng như clear code trong khi vẫn giữ test pass.

Workflow này nghĩa là "step zero": dành thời gian để suy nghĩ cẩn thận chính xác chúng ta cần phải xây dựng và làm thế nào rất mất thời gian và cũng dễ mất tập trung. Khi chúng ta luôn bắt đầu với việc implement, viết những dòng code không cần thiết và dễ bị rơi vào bế tắc.

Ý tưởng là viêt stest giống như spec behavior của hệ thống. Đó là một cách khác để tiếp cận cùng một mục tiêu, giúp ta nghĩ rõ ràng và viết test dễ hiểu và maintain hơn. Từ đó cũng giúp ta viết code để implement tốt hơn.

Một điều khó khăn với những người mới làm quen là khi bắt đầu test là test quá ít và quá tập trung vào để hiểu những gì code cần test đang viết.

def test_making_order
  book = Book.new(:title => "RSpec Intro", :price => 20)
  customer = Customer.new
  order = Order.new(customer, book)

  order.submit

  assert(customer.orders.last == order)
  assert(customer.ordered_books.last == book)
  assert(order.complete?)
  assert(!order.shipped?)
end

Ví dụ bên trên viết test/unit, unit testing framework một phần của bộ thư viện chuẩn của Ruby.

Với Rspec, chúng ta có thể thấy rõ chi tiết, mô tả "bebavior" một các rõ ràng:

describe Order do
  describe "#submit" do

    before do
      @book = Book.new(:title => "RSpec Intro", :price => 20)
      @customer = Customer.new
      @order = Order.new(@customer, @book)

      @order.submit
    end

    describe "customer" do
      it "puts the ordered book in customer's order history" do
        expect(@customer.orders).to include(@order)
        expect(@customer.ordered_books).to include(@book)
      end
    end

    describe "order" do
      it "is marked as complete" do
        expect(@order).to be_complete
      end

      it "is not yet shipped" do
        expect(@order).not_to be_shipped
      end
    end
  end
end

Cần lưu ý rằng đối với một chu trình BDD đầy đủ, chúng ta cần một công cụ như Cucumber để viết kịch bản bên ngoài bằng ngôn ngữ con người. Điều này cũng hoạt động như một bài kiểm tra tích hợp cấp cao, đảm bảo ứng dụng hoạt động như mong đợi từ quan điểm của người dùng. Sau khi đã nắm bắt được ý tưởng của BDD, chúng ta sẽ đi vào tìm hiểu những điều cơ bản của RSpec.

RSpec Basics

Chúng ta sẽ học RSpec bằng thực thi 1 phần của string calculator:

Tạo một calculator với method int Add(string numbers) Method sẽ lấy 0, 1 hoặc 2 số, và trả về tổng của chúng (với một string rỗng truyền vào sẽ trả về 0). ví dụ: "", "1", "1,2" Cho phép Add method thực thi với số biên đầu vào không xác định

Setting Up RSpec

Chúng ta configure RSpec như một gem trong Gemfile:

# Gemfile
source "https://rubygems.org"

gem "rspec"

Thực hiện trong terminal bundle install --path .bundle để cài đặt RSpec và quá trình cài đặt thành công như sau:

$ bundle install --path .bundle
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Installing diff-lcs 1.2.5
Installing rspec-support 3.1.2
Installing rspec-core 3.1.7
Installing rspec-expectations 3.1.2
Installing rspec-mocks 3.1.3
Installing rspec 3.1.0
Using bundler 1.6.0
Your bundle is complete!
It was installed into ./.bundle

First Spec

Để thuận tiện, test viết vằng RSpec được gọi là "specs" (specifications) và được đặt trong folder spec của project:

mkdir spec

Bây giờ chúng ta viết specs đầu tiên.

# spec/string_calculator_spec.rb
describe StringCalculator do
end

Với RSpec, chúng ta luôn miêu tả behavior của các class, module và method. Describe block luôn được dùng ở đầu để đặt specs trong một context.

Vì method của Ruby không yêu cầu dùng dấu hoặc, vậy nên những dòng code trong file spec giống ngôn ngữ tự nhiên hơn là code.

Để chạy specs:

bundle exec rspec

Và spec bây giờ sẽ fail vì chúng ta chưa khỏi tạo StringCalculator (NameError) error. Đơn giản là vì chúng ta chưa tạo class đó.

Tạo một thư mục mới tên lib:

mkdir lib

Khai báo StringCalculator trong stringcalculator.rb:

# lib/string_calculator.rb
class StringCalculator
end

And require it in your spec:

# spec/string_calculator_spec.rb
require "string_calculator"

describe StringCalculator do
end

Chạy Rspec bây giờ sẽ pass:

$ bundle exec rspec
No examples found.


Finished in 0.00068 seconds (files took 0.30099 seconds to load)
0 examples, 0 failures

Điều đó có nghĩa chúng ta đã sẵn sàng để viết code. Điều đơn giản nhất mà hàm string calculator có thể làm là cho vào một chuỗi rỗng, trong trường hợp đó hàm trả về 0. Method cần miêu tả đầu tiên là "add":

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "given an empty string" do
      it "returns zero" do
        expect(StringCalculator.add("")).to eql(0)
      end
    end
  end
end

Ở đây chúng ta sử dụng một describe block khác để miêu tả add class. Để tiện hơn thì class method chúng ta sẽ thêm chấu chấm ở trước (".add"), với instance methods là dấu thăng ("#add"). Chúng ta sử dụng một context block để miêu tả bối cảnh mà ở đó, add method nên trả về 0. Context ở đây cũng dúng như Describe, nhưng sẽ được đặt ở vị trí khác, để giúp code được rõ ràng logic. Chúng ta sử dụng it block để miêu tả một ví dụ cụ thể, đây được RSpec gọi là "test case". Về cơ bản, mọi ví dụ nên được miêu tả và cùng với các bối cảnh tạo nên một câu có nghĩa. Như trên chúng ta sẽ đọc như sau: "add class method: given an empty string, it returns zero". expect(...).to và phủ định của nó expect(...).notto được sử dụng để định nghĩa đầu ra. Chúng ta có rất nhiều các định nghĩa đầu ra expect của code, trong từng trường hợp. Ví dụ như so sánh bằng sử dụng eql, và còn nhiều các so sánh khác. Link: https://relishapp.com/rspec/rspec-expectations/v/3-1/docs/built-in-matchers Nếu chạy spec, chúng ta nhận được kết quả fail rằng method chưa defined:

$ bundle exec rspec
F

Failures:

1) StringCalculator.add given an empty string returns zero
Failure/Error: expect(StringCalculator.add("")).to eql(0)
NoMethodError:
undefined method `add' for StringCalculator:Class
# ./spec/string_calculator_spec.rb:8:in `block (4 levels) in <top (required)>'

Và giờ chúng ta làm cho test pass:

# lib/string_calculator.rb
class StringCalculator

  def self.add(input)
    0
  end
end

Đi tiếp tới các trường hợp khác của RSpec:

Tiếp theo là trường hợp đưa vào 1 số là string. Chúng ta viết tiếp exampke:

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "given '4'" do
      it "returns 4" do
        expect(StringCalculator.add("4")).to eql(4)
      end
    end

    context "given '10'" do
      it "returns 10" do
        expect(StringCalculator.add("10")).to eql(10)
      end
    end
  end
end

Sau khi chạy spec lại chúng ta sẽ nhận được output:

$ bundle exec rspec
.FF

Failures:

1) StringCalculator.add given '4' returns 4
Failure/Error: expect(StringCalculator.add("4")).to eql(4)

expected: 4
got: 0

(compared using eql?)
# ./spec/string_calculator_spec.rb:14:in `block (4 levels) in <top (required)>'

2) StringCalculator.add given '10' returns 10
Failure/Error: expect(StringCalculator.add("10")).to eql(10)

expected: 10
got: 0

(compared using eql?)
# ./spec/string_calculator_spec.rb:20:in `block (4 levels) in <top (required)>'

Finished in 0.00133 seconds (files took 0.0835 seconds to load)
3 examples, 2 failures

Tiếp tục. mục đích vẫn là làm cho test pass:

# lib/string_calculator.rb
class StringCalculator

  def self.add(input)
    if input.empty?
      0
    else
      input.to_i
    end
  end
end

Để làm cho stringCalculator thêm đầy đủ, chúng ta còn thiếu trường hợp string gồm 2 số được phân cách bởi dấu phấy:

# spec/string_calculator_spec.rb
describe StringCalculator do

  describe ".add" do
    context "two numbers" do
      context "given '2,4'" do
        it "returns 6" do
          expect(StringCalculator.add("2,4")).to eql(6)
        end
      end

      context "given '17,100'" do
        it "returns 117" do
          expect(StringCalculator.add("17,100")).to eql(117)
        end
      end
    end
  end
end

Đúng expect, spec trả về fail. Chúng ta nên chạy lại spec mỗi thay đổi nhỏ trong code. Dưới đây là một các để pass các test case:

class StringCalculator

  def self.add(input)
    if input.empty?
      0
    else
      numbers = input.split(",").map { |num| num.to_i }
      numbers.inject(0) { |sum, number| sum + number }
    end
  end
end

RSpec có nhiều cách để hiển thị output. Một sự thay thế rất phổ biến cho dot format mặc định là format "documentation":

$ bundle exec rspec --format documentation

StringCalculator
  .add
    given an empty string
      returns zero
    single numbers
      given '4'
        returns 4
      given '10'
        returns 10
    two numbers
      given '2,4'
        returns 6
      given '17,100'
        returns 117

Trong phần này tôi đã giới thiệu những block cơ bản của RSpec. Bằng các sử dụng RSpec's built-in matchers bạn có thể sẵn sàng viết test cho code của mình RSpec sẽ được giới thiệu kĩ hơn trong những phần tiếp theo

Tài liệu tham khảo: https://semaphoreci.com/community/tutorials/getting-started-with-rspec http://bundler.io/gemfile.html https://relishapp.com/rspec/rspec-core/v/3-1/docs/example-groups/basic-structure-describe-it https://relishapp.com/rspec/rspec-expectations/docs https://relishapp.com/rspec/rspec-expectations/v/3-1/docs/built-in-matchers