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