Rails – Unit Testing

4.8/5 - (135 votes)

Trong bài này chúng ta sẽ thực hiện kiểm tra code bằng phương pháp Unit Testing.

Nếu bạn chưa biết thì có thể nói đơn giản Unit Testing là phương pháp kiểm tra xem các đoạn code chúng ta viết ra có chạy chính xác hay không. Rails có riêng một nền tảng hỗ trợ Unit Testing rất mạnh, chúng ta sẽ tìm hiểu cách sử dụng nền tảng này.

Khi chúng ta tạo một project thì ngoài thư mục app, Rails còn tạo ra một thư mục có tên là test, trong thư mục này cũng có thư mục models, controllers và một vài thư mục cũng như file khác tương tự như trong thư mục app. Ý nghĩa của thư mục này là tạo ra một môi trường giả lập giống hệt như môi trường “chính” dùng để kiểm tra tất cả mọi thứ, môi trường này có models riêng, controller riêng và cả CSDL riêng nữa.

Khi chúng ta tạo một model bằng lệnh generate scaffold, thì trong thư mục gốc của project có một thư mục tên là test/models, trong đó sẽ có một file có tên theo dạng <tên model>_test.rb, trong ứng dụng depot mà chúng ta đã làm thì file này sẽ là product_test.rb, file này có nội dung như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    # test "the truth" do
    # assert true
    # end
end

Ở đây Rails định nghĩa lớp ProductTest kế thừa từ lớp ActiveSupport::TestCase, lớp này thực ra cũng chỉ là kế thừa từ lớp Test::Unit::TestCase có trong Ruby thôi. Trong lớp này có một khối lệnh (đã được mặc định là comment) có cú pháp test <tên test> do...end, bạn cứ hình dung đây cũng giống như là một định nghĩa hàm/phương thức thôi cho đơn giản, chúng ta sẽ gọi chúng là “test”, khi kiểm tra thì Rails sẽ gọi những “test” này để chạy.

Tiếp theo trong khối lệnh đó có dòng lệnh assert true, assert là một phương thức, phương thức assert được dùng để:

  • Kiểm tra thuộc tính này có giá trị như thế kia không?
  • Kiểm tra đối tượng có phải là nil hay không.
  • Có phải đoạn code kia sẽ giải phóng exception hay không.

Nói tóm lại giá trị trả về của phương thức assert là true hoặc false, và cứ mỗi lần có một phương thức assert nào đó trả về false thì Rails báo lỗi.

Trước khi đi vào phần kiểm tra thì chúng ta phải chạy lệnh rake db:migrate RAILS_ENV=test:

C:\Project\Rails\depot>rake db:migrate RAILS_ENV=test
== 20161106144824 CreateProducts: migrating ===========================================
-- create_table(:products)
   -> 0.0024s
== 20161106144824 CreateProducts: migrated (0.0066s)===================================

Lệnh này cũng giống như lệnh rake db:migrate thôi, tức là sẽ tạo các bảng trong CSDL, chỉ khác là tạo cho môi trường test, không đụng chạm gì tới file CSDL của app cả, mặc định Rails sử dụng CSDL SQLite3 nên file được tạo ra là file test.sqlite3 nằm trong thư mục db.

Ví dụ Unit Testing

Chúng ta sửa file product_test.rb như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        product = Product.new
        assert product.invalid?
        assert product.errors[:title].any?
        assert product.errors[:description].any?
        assert product.errors[:price].any?
        assert product.errors[:image_url].any?
    end
end

Trong đoạn code trên, chúng ta định nghĩa một test có tên là “Thuoc tinh khong duoc de trong”, trong đó chúng ta tạo một đối tượng Product và không có thuộc tính nào có giá trị nào cả. Tiếp theo chúng ta gọi phương thức assert kiểm tra giá trị của phương thức invalid?, phương thức invalid? là một phương thức của lớp ActiveRecord::Base, phương thức này sẽ kiểm tra xem các thuộc tính của lớp tương ứng có hợp lệ hay không dựa trên các phương thức validates (mà chúng ta đã làm trong bài trước), tất nhiên là không hợp lệ vì không có thuộc tính nào có giá trị nào cả, và Rails sẽ làm một việc là gán một chuỗi thông báo lỗi ứng với từng thuộc tính vào thuộc tính errors (đây là một mảng và cũng là một thuộc tính của lớp ActiveRecord::Base).

Tiếp theo chúng ta lại kiểm tra xem các phần tử của mỗi thuộc tính tương ứng trong thuộc tính errors có tồn tại hay không thông qua phương thức any?, phương thức này kiểm tra xem một thuộc tính nào đó có tồn tại hay không, ở đây là có tồn tại và phương thức assert sẽ trả về true.

Bây giờ chúng ta có thể chạy test bằng lệnh rake test:units và chúng ta sẽ có đoạn output tương tự như thế này:

C:\Project\Rails\depot>rake test:units
Run options: --seed 26133

# Running:

Finished in 0.123351s, 8.1069 runs/s, 40.5347 assertions/s.

1 runs, 5 assertions, 0 failures, 0 errors, 0 skips

Đoạn output trên thông báo là việc test đã thành công, các đoạn code kiểm tra không bị lỗi (0 failures), tức là chúng ta đã viết các phương thức validates đúng.

Bây giờ chúng ta tạo một test khác như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        product = Product.new(:title => 'Hmm...',
                              :description => 'Err...',
                              :image_url => 'Something.jpg')
 
        product.price = -1
        assert product.invalid? 
        assert_equal "must be greater than or equal to 1.0",
                     product.errors[:price].join('') 
 
        product.price = 0
        assert product.invalid?
        assert_equal "must be greater than or equal to 1.0",
                     product.errors[:price].join('')
 
        product.price = 5
        assert product.valid?
 
    end
end

Chúng ta tạo một test có tên "Price phai lon hon 1.0" để kiểm tra xem phương thức validates với thuộc tính price có hoạt động chính xác hay không.

Ở đây chúng ta tạo một đối tượng Product với các thuộc tính còn lại có đầy đủ giá trị hợp lệ, còn thuộc tính price sẽ lần lượt có 3 giá trị là -1, 0, và 5 để kiểm tra.

Đối với giá trị -1 và 0, nếu hàm validates chúng ta viết chạy đúng thì sau khi gọi hàm invalid?, trong thuộc tính errors sẽ có một phần tử có khóa là :price và có giá trị là "must be greater than or equal to 1.0" (phải lớn hơn hoặc bằng 1.0). Chúng ta gọi phương thức assert_equal, phương thức này nhận vào 2 chuỗi và so sánh xem 2 chuỗi đó có bằng nhau hay không, ở đây chúng ta kiểm tra xem chuỗi trong mảng errors có giống với chuỗi "must be..." ở trên hay không. Ở đây thực chất giá trị trong mảng errors được lưu dưới dạng một mảng khác nữa nên chúng ta gọi phương thức join() để chuyển giá trị đó thành một chuỗi thì mới so sánh được.

Đối với giá trị 5 thì hiển nhiên là giá trị này hợp lệ (nếu chúng ta viết validates đúng) nên chúng ta kiểm tra bằng phương thức valid?.

Bạn có thể chạy rake test:units để kiểm tra, nếu có lỗi thì tức là các đoạn validates viết sai.

Tiếp theo chúng ta viết đoạn test kiểm tra tên file trong thuốc tính image_url có hợp lệ hay không như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        ...
    end
 
    def new_product(image_url)
        Product.new(:title => 'Hmm...',
                    :description => 'Err...',
                    :image_url => image_url,
                    :price => 5)
    end
 
    test "image url" do
        url_hop_le = %w{ img.gif img.jpg img.png IMG.JPG IMG.Jpg http://a.b.c/x/y/z/img.gif }
        url_khong_hop_le = %w{ img.doc img.gif/more img.gif.more }
 
        url_hop_le.each do |name|
            assert new_product(name).valid?, "#{name} phai hop le"
        end
 
        url_khong_hop_le.each do |name|
            assert new_product(name).invalid?, "#{name} khong hop le"
        end
    end
end

Đầu tiên chúng ta viết phương thức new_product() để tạo một đối tượng Product mới có thuộc tính image_url được truyền vào từ tham số. Tiếp theo là đoạn test có tên “image url”, trong này chúng ta tạo 2 mảng url_hop_leurl_khong_hop_le chứa các tên file hợp lệ và không hợp lệ. Sau đó chúng ta duyệt qua từng mảng, cứ mỗi lần duyệt thì chúng ta tạo một đối tượng Product mới từ hàm new_product và gọi phương thức assert để kiểm tra. Ở đây phương thức assert được truyền thêm một tham số thứ 2 nữa, đây chẳng qua chỉ là một chuỗi và chuỗi này sẽ được nối vào thuộc tính lỗi trong mảng errors để có gì thì tìm cho dễ thôi.

Fixtures

Trong số các điều kiện kiểm tra mà chúng ta đã viết thì còn một điều kiện nữa là kiểm tra xem thuộc tính title có phải là duy nhất hay không. Để kiểm tra thì có một cách là mỗi lần test chúng ta tạo một đối tượng cụ thể, sau đó lưu vào CSDL, rồi lại một đối tượng khác có thuộc tính giống như đối tượng vừa tạo và kiểm tra, tuy nhiên cách này có hơi rắc rối, do đó Rails cung cấp cho chúng ta cơ chế Fixtures.

Để hiểu một cách đơn giản thì fixtures là các dữ liệu mẫu đã được lưu trong CSDL test, mỗi khi chạy test thì chúng ta có thể sử dụng các dữ liệu này.

Trong Rails thì các dữ liệu fixture được lưu trong thư mục mặc định là test/fixtures dưới định dạng CSV hoặc YAML. Khi tạo model thì Rails cũng tạo một file fixture mẫu trong thư mục này với định dạng YAML, ở đây chúng ta tạo model Product và file fixture tương ứng là products.yml, và chúng ta cũng sẽ dùng định dạng này luôn cho tiện. File products.yml mặc định có nội dung như sau:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
    title: MyString
    description: MyText
    image_url: MyString
    price: 9.99

two:
    title: MyString
    description: MyText
    image_url: MyString
    price: 9.99

Lưu ý là các file .yml này luôn phải trùng với tên một bảng nào đó, không phải ngẫu nhiên mà file này lại có tên là products.yml. Ngoài ra cứ mỗi lần chúng ta chạy lệnh rake test:units thì các bảng trong file test.sqlite3 luôn được tạo mới và được chèn sẵn các dữ liệu fixtures này.

Cú pháp YAML có lẽ cũng không khó hiểu lắm nên mình sẽ không giải thích đoạn code trên 🙂 Chỉ có một lưu ý là các từ như one, two, đây là các tên các dòng để chúng ta có thể tham chiếu khi viết test, chứ các tên này không được lưu vào CSDL.

Bây giờ chúng ta sẽ thêm một bản ghi khác vào fixture như sau:

# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
 title: MyString
 description: MyText
 image_url: MyString
 price: 9.99

two:
 title: MyString
 description: MyText
 image_url: MyString
 price: 9.99

ruby:
 title: Ruby on Rails 4
 description: Zzzzzzz....
 price: 9.99
 image_url: img.jpg

Lưu ý là các dòng được thụt vào bằng một dấu cách, không phải là dấu tab.

Tiếp theo chúng ta viết test như sau:

require 'test_helper'

class ProductTest < ActiveSupport::TestCase
    test "Thuoc tinh khong duoc de trong" do 
        ...
    end 
 
    test "Price phai lon hon 1.0" do
        ...
    end
 
    def new_product(image_url)
        ...
    end
 
    test "image url" do
        ...
    end
 
    fixtures :products
 
    test "title khong duoc trung" do 
        product = Product.new(:title => products(:ruby).title,
            :description => "Hmm...",
            :image_url => "img.jpg",
            :price => 5)
        assert !product.save
        assert_equal "has already been taken",
                     product.errors[:title].join('')
     end
end

Dòng fixtures :products sẽ chỉ định Rails lấy những bản ghi được định nghĩa trong file products.yml từ bảng products trong file test.sqlite3. Sau đó Rails sẽ tạo một phương thức có tên trùng với tên bảng, phương thức này sẽ nhận vào tên fixtures để tìm các bản ghi có tên tương ứng và tạo thành một đối tượng thuộc model đó, nhờ đó chúng ta có thể đọc dữ liệu từ bản ghi này.

Trong ví dụ trên, chúng ta tạo test có tên “title khong duoc trung”, trong đó chúng ta tạo một đối tượng Product có thuộc tính title được lấy từ đối tượng tạo ra từ phương thức products(:ruby). Khi chúng ta gọi phương thức save để lưu đối tượng mới vào CSDL thì phương thức này sẽ trả về false, tức là không hợp lệ và assert sẽ tạo thông báo lỗi vào thuộc tính errors.

Bạn có thể chạy lệnh rake test:units để kiểm tra.

5 1 vote
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Inline Feedbacks
View all comments
NhatTV
6 năm trước
require 'test_helper'

class ProductTest < ActiveSupport::TestCase
  test "Thuoc tinh khong duoc de trong" do
    product = Product.new
    assert product.invalid?
    assert product.errors[:title].any?
    assert product.errors[:description].any?
    assert product.errors[:price].any?
    assert product.errors[:image_url].any?
  end
end
Le Tuan
Le Tuan
2 năm trước

rake test:units không chay được làm sao fix hả mọi người