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_le
và url_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.