Rails是一个几乎包含了所有东西的框架,专注于惯例而不是配置。 Minitest就是这些约定之一。Minitest小而快,它提供了许多断言,使测试可读而干净。
然而,有许多Minitest的替代品。最流行的一个是 RSpec。
RSpec的目标与Minitest相同,但侧重于可读的规范,描述应用程序应该如何表现,并与英语紧密结合。Minitest声称,如果你对Ruby很了解,应该就足够了。
RSpec项目专注于行为驱动开发(BDD)和规范编写。测试不仅验证了你的应用程序代码,而且还提供了详细的表达方式,解释了应用程序应该如何表现。
Minitest也支持测试驱动开发(TDD)、BDD、嘲弄和基准测试。然而,它们有细微的差别,所以我们将通过RSpec和Minitest中的一个例子应用来展示它们的差别。在我们开始之前,让我们介绍一下所有测试框架的共同点。
让我们回顾一下:什么是测试?
TDD和BDD都鼓励以测试为先的方法,这意味着你从编写测试开始。你运行测试,测试应该是失败的。然后你写代码,使测试通过。
通过编写大量的测试来覆盖我们整个程序的次要方面,我们最终应该达到一个几乎完美的系统。当然,这是软件,所以这在现实中不会发生。然而,我们可以通过许多测试对我们的软件进行大幅度的修改,减少风险。自动测试可以成为小型创业公司为寻找市场契合点而进行的成功与失败之间的区别。在一个大公司里,创新不需要放慢速度。
什么是测试?我们可以通过在软件世界中把测试分成三大类来回答这个问题。
单元测试
单元测试是由软件开发人员编码的小型自动测试,以验证一小段生产代码--一个单元--是否按预期运行。
关于单元测试的文章很多,但你应该知道一个缩写是 FIRST,它是由Tim Ottinger和Jeff Langr首先使用:
- F - 快速 - 测试必须快速运行。他们运行得越快,你就越有可能运行它们。
- I - 隔离 - 每个测试都应该有一个失败的原因。
- R - 可重复的 - 测试应该在每次运行时有相同的结果。
- S - 自我验证 - 结果不应该由人类解释。它们应该是二进制的(例如,红色/绿色)。
- T - 及时 - 在写代码之前写测试。
集成测试
下一个大类别是集成测试。这些测试验证了小块的代码是否工作。
因此,我们可以确定A、B、C和D单元测试独立工作,但你怎么知道它们一起工作?
集成测试有时会很难写,可能是作为修复错误过程的一部分来创建。
UI测试
这个测试分支通常具有最高的计算成本,运行模拟的用户体验来测试错误、流程和其他一切。它最常被用来取代或协助专业的质量保证测试人员。
现在我们已经把软件测试分成了大的类别,让我们更进一步,在比较RSpec和Minitest之前探索另一个重要的概念。
红色、绿色和重构的生命周期
在我第一份实行TDD的开发工作中,高级开发人员使用了三个 "G"。
"让它失败,让它工作,然后让它更好"。
这使得我们的工作流程看起来像这样:
基本上,我们先写一个测试,然后运行它。测试会失败。接下来,我们写代码,使测试通过。测试通过后,我们就可以继续前进,改进实现。慢慢地,但肯定的是,我们到达了我们的理想实现。
这个简单的工作流程似乎有悖常理,因为它最初比较慢。然而,从长远来看,它是更快的。
比较Minitest和RSpec
如果你是在一个现有的代码库上工作,很可能已经为你做了决定。如果你正在开始一个项目,你可能想知道你是否应该探索使用RSpec或坚持使用Minitest。如果你想知道你是否应该从一个切换到另一个,这是更细微的差别,而且根据你的代码库的大小,可能不值得这样做。
我希望你能看到你将要写的实际代码,这将有助于你的决策。
以下是我们要比较的主题
设置 - Minitest Vs RSpec
首先,我们将创建两个类似的应用程序并设置RSpec和Minitest。
由于Minitest是与Rails一起安装的,所以它只是使用Rails的new命令:
rails new rails_minitest
然而,在设置RSpec时,我们要做的事情更多:
rails new rails_rspec
在应用程序生成后,我们必须添加RSpec的宝石。这就是我们遇到RSpec的第一个困难的地方。
我们应该添加RSpec核心宝石还是rspec-rails宝石?
选择rspec-rails
,似乎很明显,因为我们要处理的是一个Rails应用,但这是一个错误,在我做顾问的时候,我已经不止一次地看到这个错误。
设置rspec-rails
包括将其添加到我们的Gemfile中的两个位置。
这个项目的readme涵盖了所有这些。
# Gemfile
## For all the generators
group :development, :test do
gem 'rspec-rails', '~> 5.0.0'
end
在将其添加到你的gem文件后,你可以做以下工作:
bundle install
安装后,我们可以运行rspec:install命令:
rails generate rspec:install
这将创建一个spec
文件夹,一个 .rspec
文件,以及两个额外的文件:spec/spec_helper.rb
和spec/rails_helper.rb
。
这两个文件有什么区别?
spec/spec_helper
加载了Rails应用程序,而spec/spec_helper
是RSpec的一个轻量级配置文件。
作为一个简短的旁白,如果你避免为特定的文件加载Rails,你可以加快一些RSpec测试的速度。例如,如果你在应用中使用不需要Rails的普通Ruby对象(PORO),你可以在运行测试时避免将Rails添加到该规范中。这加快了编写代码时的反馈循环,并有助于实现 Tim Ottinger和Jeff Langr所解释的F(ast)in our FIRST原则 。
下面是一个例子。
# app/services/hello_world.rb
class HelloWorld
def say
'Hello World'
end
end
# spec/services/hello_world_spec.rb
# Usually, we require 'rails_helper' here, but there’s no need if we are not using Rails.
require './app/services/hello_world'
RSpec.describe HelloWorld do
describe '#hello' do
it 'returns hello world' do
expect(HelloWorld.new.say).to eq('Hello World')
end
end
end
单元测试:RSpec vs. Minitest
现在我们已经建立了我们的测试套件,我们可以比较我们如何在每个框架中编写测试。
让我们看一下RSpec中的模型测试例子。惯例是先用普通英语写测试,然后用代码写。
require 'rails_helper'
RSpec.describe Article, type: :model do
context 'validations' do
article = Article.new
article.valid?
it 'must have a title' do
expect(article.errors.messages[:title]).to include("can't be blank")
end
it 'must have a body' do
expect(article.errors.messages[:body]).to include("can't be blank")
end
end
end
运行该测试:
rspec --format documentation spec/models/article_spec.rb
当测试失败时,我们得到以下输出:
Article
validations
must have a title (FAILED - 1)
must have a body (FAILED - 2)
Failures:
1) Article validations must have a title
Failure/Error: expect(article.errors.messages[:title]).to include("can't be blank")
expected ["is too short (minimum is 5 characters)"] to include "can't be blank"
# ./spec/models/article_spec.rb:9:in `block (3 levels) in <top (required)>'
2) Article validations must have a body
Failure/Error: expect(article.errors.messages[:body]).to include("can't be blank")
expected [] to include "can't be blank"
# ./spec/models/article_spec.rb:13:in `block (3 levels) in <top (required)>'
Finished in 0.02552 seconds (files took 1.3 seconds to load)
2 examples, 2 failures
Failed examples:
rspec ./spec/models/article_spec.rb:8 # Article validations must have a title
rspec ./spec/models/article_spec.rb:12 # Article validations must have a body
为了得到这个输出,你必须用—format documentation
选项运行RSpec命令。默认的是内联。
接下来,让我们用Minitest编写同样的测试:
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
setup do
@article = articles(:one)
end
def test_validates_title
@article.title = nil
assert @article.valid?
assert_equal ["can't be blank"], @article.errors[:title]
end
def test_validates_body
@article.body = nil
assert @article.valid?
assert_equal ["can't be blank"], @article.errors[:body]
end
end
现在运行该测试:
rails test test/models/article_test.rb
# Running:
F
Failure:
ArticleTest#test_validates_body [/Users/williamkennedy/projects/honeybadger/test_minitest/test/models/article_test.rb:17]:
Expected: ["can't be blank"]
Actual: []
rails test test/models/article_test.rb:14
F
Failure:
ArticleTest#test_validates_title [/Users/williamkennedy/projects/honeybadger/test_minitest/test/models/article_test.rb:11]:
Expected: ["can't be blank"]
Actual: []
rails test test/models/article_test.rb:8
Finished in 0.015373s, 130.0982 runs/s, 260.1964 assertions/s.
2 runs, 4 assertions, 2 failures, 0 errors, 0 skips
直接,你会注意到一件事。Minitest只是Ruby代码,而RSpec是一种需要学习的新语言。虽然语法相似,但RSpec测试的长度会增加。当涉及到RSpec时,还有更多的精神开销,因为你的团队必须为测试的结构定义一个惯例。
对我来说,Minitest只是Ruby,这是一个很大的胜利。Rails通过设置方法在Minitest中建立了一个惯例。
默认的Minitest文件的设置已经考虑到了FIRST原则。
UI测试:RSpec vs. Minitest
现在让我们转到测试的另一个支柱。再一次,Rails默认设置了嵌套在test/system
文件夹下的UI测试。然而,RSpec涉及一些设置,而且没有标准的最佳做法。
一个UI测试是相当复杂的。你编写指令,如click here
和fill_in
,以驱动交互。他们通常使用网络驱动器,如Selenium,来指导交互。
他们帮助测试JavaScript行为,重新创建可能导致错误的用户旅程,甚至确保特定的用户流是密不可分的。
就其性质而言,它们比常规测试的计算成本更高,这就是为什么我们可能会选择无头CI工具。
自从Rails引入系统测试后,RSpec就受益匪浅。在此之前,我们必须手动设置。
让我们举一个测试的例子。创建一个名为spec/system/article_system_spec.rb
的文件,并添加以下代码。
require 'rails_helper'
RSpec.describe 'Article', type: :system do
it 'can be created' do
visit '/articles/new'
fill_in 'article[title]', with: 'Hello'
fill_in 'article[body]', with: 'World'
click_button 'Create'
expect(page).to have_content 'Article was successfully created.'
end
end
运行这个测试将产生你所期望的结果。在写这篇文章的时候,它将钩住默认的网络驱动,即Selenium。RSpec允许你通过手动调用driven_by
方法来改变每个测试或甚至所有的规格。
下面是方法:
require 'rails_helper'
RSpec.describe 'Article', type: :system do
before do
driven_by(:selenium_chrome_headless)
end
it 'should create article' do
visit '/articles/new'
fill_in 'article[title]', with: 'Hello'
fill_in 'article[body]', with: 'World'
click_button 'Create'
expect(page).to have_content 'Article was successfully created.'
end
end
现在测试将使用Selenium_chrome_headless而不是Selenium运行,这可以加快你的测试速度。
由于 Capybara库驱动底层测试,Minitest也有同样的语法。
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
setup do
@article = articles(:one)
end
test "should create article" do
visit articles_url
click_on "New article"
fill_in "Body", with: @article.body
fill_in "Title", with: @article.title
click_on "Create Article"
assert_text "Article was successfully created"
click_on "Back"
end
end
然而,在每个测试中改变你的网络驱动有一个微妙的区别。
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
driven_by :selenium, using: :headless_chrome
setup do
@article = articles(:one)
end
test "should create article" do
visit articles_url
click_on "New article"
fill_in "Body", with: @article.body
fill_in "Title", with: @article.title
click_on "Create Article"
assert_text "Article was successfully created"
click_on "Back"
end
end
你也可以在全局范围内改变这一点:
# test/application_system_test_case.rb
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
end
结论
让我们希望这能帮助你在RSpec和Minitest之间做出选择,或者了解其中的区别。我已经尽可能地避免了偏见。这两个框架都有其优点,但都有相同的原则。
先写测试,让它们通过,从长远来看,你会有一个更快乐的代码库。
两者的区别是微妙的。使用RSpec,你会写更多的代码,而且有一个更大的插件生态系统来使事情更容易。然而,当生态系统更大并且由不同的库组成时,当涉及到升级你的应用程序时,依赖树偶尔会造成困难。
不能忽视的一个方面是性能。
Minitest比RSpec快,但魔鬼在细节中。
快多少取决于你如何衡量。这是很难确定的,因为你的测量方式很重要。采样偏差(即用小规模的样本来得出结论)可能有利于Minitest比RSpec快10倍的因素。在其他情况下,差异可能只有10%。
有一个 有趣的库,使用Ruby Benchmark 测量Minitest、RSpec和Cucumber的原始性能,发现Ruby 3的情况如下。
$ bundle exec ruby ./compare.rb
user system total real
cucumber: 585.035884 22.566803 607.602687 ( 608.237973)
minitest: 18.208514 7.893526 26.102040 ( 26.430622)
rspec: 2406.162561 12.497706 2418.660267 (2418.889164)
test_bench: 29.517226 8.272563 37.789789 ( 38.133189)
看这个可能会让你觉得Minitest是压倒性的赢家,因为它运行的时间更短。然而,在实际应用中,情况 可能并非如此。差异可能只有10%,或者根本就没有差异。在比较程序员工具时,由于无数的文化、个人和其他原因,抽样偏见是普遍存在的。
如果你的测试很慢,可能是由于其他一些因素造成的,比如数据库调用、内存,甚至可能是网络调用。
在RSpec和Minitest之间的选择可能只是归结于个人的偏好。