两个主要的框架垄断了 Ruby 的测试界:Rspec 和 MiniTest. Rspec 是一个非常有表现力的测试框架,它有很多好的特性和辅助方法来让测试变得可读。当我们用 Rspec 写测试的时候,有几个小的方法,或许可以让测试更好写、更易读、更利于维护。
现在假设有一个系统,有书 (Books
) 和 作者 (Authors
),让我们使用一些方法来简化测试。
class Book
attr_reader :title, :genre
def initialize(title, genre)
@title = title
@genre = genre
end
end
class Author
attr_reader :books
def initialize(name, books)
@name = name
@books = Array(books)
end
def has_written_a_book?
!books.empty?
end
end
let()
有两个好处:
subject{}
可用来声明测试的对象,后续的测试用例就无需明确指定了。
通过声明 let
和 subject
变量是一个保持测试可读性和不重复自己 (DRY) 的好方法。
举个例子,如果我们想确认一个作者有名字 (assert an Author
has a name
),如果不用 let
和 subject
变量,测试大概长这样:
describe Author do
before do
@book_genre = 'Historical Fiction'
@book_title = 'A Tale of Two Cities'
@book = Book.new(@book_genre, @book_title)
@author_name = 'Charles Dickens'
@author = Author.new(@author_name, [@book])
end
describe '#name'do
it 'has a name set' do
expect(@author.name).to eq(@author_name)
end
end
end
如果再加上别的测试,比如确认书的数量,不同的名字,或者其他关于这个作者的东西的话,这测试就会变得很冗长。
所以,我们可以使用 let
和 subject
变量来实现 DRY:
describe Author do
let(:book_genre) { 'Historical Fiction' }
let(:book_title) { 'A Tale of Two Cities' }
let(:book) { Book.new(book_genre, book_title) }
let(:book_array) { [book] }
let(:author_name) { 'Charles Dickens' }
subject { Author.new(author_name, book_array) }
describe '#name'do
it 'has a name set' do
expect(subject.name).to eq(author_name)
end
end
describe '#books' do
context 'with books' do
it 'has books set' do
expect(subject.books).to eq(book_array)
end
end
context 'without books' do
context 'books variable is nil' do
let(:book_array) { nil }
it 'sets books to an empty array' do
expect(subject.books).to eq([])
end
end
context 'books variable is an empty array' do
let(:book_array) { [] }
it 'sets books to an empty array' do
expect(subject.books).to eq([])
end
end
end
end
end
不再需要几个 before
块来定义一个个实例变量,这段代码用了 let
,简洁易读。更具体地说,这些测试的工作机制是:每当一个 it
块运行的时候,context
里面离得最近的 let
就被用来初始化这个 subject
。
通过设置 let 变量,如果想要测试 subject.books
是不是一个数组,判断输入是nil
或者[]
,只需要简单地修改一下 let
声明:let(:book_array) { nil }
如果一个测试不关心细节,Rspec 允许使用 general expectations 和 占位符 (placeholders). 这些占位符可以只专注于真正重要的东西,从而简化测试。
1. anything
正如其名,当一个方法要求一个参数但是这个具体的参数又对测试没有影响的时候,我们可以使用 anything
这个参数匹配符。
如果一个测试,想要确认一个作者已经写过书了,但不关心这本书的标题和类型,就可以用 anything
:
describe Author do
describe '#has_written_a_book?' do
context 'when books are passed in' do
subject { Author.new(name, books) }
let(:books) { [Book.new(anything, anything)] }
it 'is true' do
expect(subject.has_written_a_book?).to eq(true)
end
end
end
end
2. hash_including
当我们要测试一个期望是 Hash
的方法,这个 Hash
中的某些元素可能比其他的元素更重要。hash_including
匹配符允许开发者 assert 一个或多个 hash 的键值对,而不用指定整个 hash。
假设 Book
类有一个方法,实例化了一个 HTTP client (用来取得一些附加信息):
class Book
# ...
def fetch_information
HTTPClient.new({ title: title, genre: genre, time: Time.now })
.get('/information')
end
end
给这个方法写测试应该 assert 这个 client 已经初始化了几个关键点,这个场景就可以用 hash_including
。
describe Book do
describe '#fetch_information' do
let(:book_genre) { 'Historical Fiction' }
let(:book_title) { 'A Tale of Two Cities' }
subject { Book.new(title, genre) }
it 'instantiates the client correctly' do
expect(HTTPClient).to receive(:new)
.with(hash_including(title: book_title,
genre: book_genre))
subject.fetch_information
end
end
end
hash_including
可以指定一个期望的 hash 中的键值对或者只是键。这里,这个测试只关心传入的书的标题 (title
) 和类型 (genre
)。
3. match_array
在 Ruby 中,当且仅当两个数组包含同样的元素且顺序相同时,这两个数组是相等 (equal) 的。在一些测试中,这个严格相等的准则可能不是必须的。那些情况下,RSpec 提供了一个叫做 match_array
的方法来让测试顺利进行。
如果 Author
类从数据库中取得了它的书的清单,由于默认的 scopes 或者记录的更新操作,书的顺序可能不是连续的。
现定义一个 fetch_books
方法:
class Author
attr_reader :name
def initialize(name)
@name = name
end
def fetch_books
BookDB.find_by(author_name: name)
end
end
使用 match_array
,测试可以确认返回了合适的书,无视顺序:
describe Author do
describe '#fetch_books' do
let(:name) { 'Jane Austen' }
let!(:books) do
Array.new(2) do
BookDB.create_book(author_name: name)
end
end
subject { Author.new(name: name) }
it 'fetches the books correctly' do
expect(subject.fetch_books).to match_array(books)
end
end
end
(原文翻译时有删改)