简介

榜样很重要。

— 警官亚历克斯·J·墨菲 / 机器战警

本 Minitest 样式指南概述了现实世界程序员编写可由其他现实世界程序员维护的代码的推荐最佳实践。

RuboCop,一个静态代码分析器(linter)和格式化程序,有一个 rubocop-minitest 扩展,提供了一种方法来强制执行本指南中概述的规则。

您可以使用 AsciiDoctor PDF 生成本指南的 PDF 版本,以及使用 以下命令 AsciiDoctor 生成 HTML 版本

# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc

# Generates README.html
asciidoctor
提示

安装 rouge gem 以在生成的文档中获得漂亮的语法高亮。

gem install rouge

活文档

本指南仍在不断完善中 - 现有指南不断改进,新增指南,偶尔也会删除一些指南。

布局

本节讨论结构化测试的惯用方式。

注意
本节目前是一个占位符。欢迎贡献!

断言

本节讨论 Minitest 提供的断言的惯用用法。

断言为 nil

如果期望为 nil,请使用 assert_nil

# bad
assert_equal(nil, actual)

# good
assert_nil(actual)

断言不为 nil

如果期望不为 nil,请使用 refute_nil

# bad
assert(!actual.nil?)
refute(actual.nil?)

# good
refute_nil(actual)

断言相等参数顺序

assert_equal 应该始终将期望值作为第一个参数,因为如果断言失败,错误消息会显示期望为 "rubocop-minitest",实际为 "rubocop",而不是相反。

注意
如果您习惯使用 RSpec,那么这与 RSpec 的顺序相反。
# bad
assert_equal(actual, "rubocop-minitest")

# good
assert_equal("rubocop-minitest", actual)

断言不相等

如果 expectedactual 不应该相同,请使用 refute_equal

# bad
assert("rubocop-minitest" != actual)

# good
refute_equal("rubocop-minitest", actual)

断言相同

使用 assert_same 代替 assertequal?

注意
仅在需要按身份比较时才使用 assert_same。否则,请使用 assert_equal
# bad
assert(expected.equal?(actual))

# good
assert_same(expected, actual)

断言不相同

使用 refute_same 代替 refuteequal?

注意
仅在需要按身份比较时才使用 refute_same。否则,请使用 refute_equal
# bad
refute(expected.equal?(actual))
assert(!expected.equal?(actual))

# good
refute_same(expected, actual)

断言为真

如果期望为真值,请使用 assert

# bad
assert_equal(true, actual)

# good
assert(actual)

断言不为假

如果期望为假,请使用 refute

# bad
assert_equal(false, actual)

# bad
assert(!something)

# good
refute(actual)

断言包含

使用 assert_includes 断言对象是否包含在集合中。

# bad
assert(collection.include?(object))

# good
assert_includes(collection, object)

断言不包含

如果对象未包含在集合中,请使用 refute_includes

# bad
refute(collection.include?(object))
assert(!collection.include?(object))

# good
refute_includes(collection, object)

断言在 Delta 内

如果比较 floats,请使用 assert_in_delta。如果预期值在 actual 值的 delta 内,则断言通过。

# bad
assert_equal(Math::PI, actual)

# good
assert_in_delta(Math::PI, actual, 0.01)

反驳在 Delta 内

如果比较 floats,请使用 refute_in_delta。如果预期值不在 actual 值的 delta 内,则断言通过。

# bad
refute_equal(Math::PI, actual)

# good
refute_in_delta(Math::PI, actual, 0.01)

断言为空

如果期望对象为空,请使用 assert_empty

# bad
assert(object.empty?)

# good
assert_empty(object)

反驳为空

如果期望对象不为空,请使用 refute_empty

# bad
assert(!object.empty?)
refute(object.empty?)

# good
refute_empty(object)

断言运算符

如果使用运算符比较预期对象和实际对象,请使用 assert_operator

# bad
assert(expected < actual)

# good
assert_operator(expected, :<, actual)

反驳运算符

如果期望预期对象不是实际对象的二元运算符,请使用 refute_operator。如果预期对象不是实际对象的二元运算符(例如:大于),则断言通过。

# bad
assert(!(expected > actual))
refute(expected > actual)

# good
refute_operator(expected, :>, actual)

断言输出

使用 assert_output 断言方法的输出。如果预期输出或错误与标准输出/错误匹配或相等,则断言通过。预期值可以是正则表达式、字符串或 nil。

# bad
$stdout = StringIO.new
puts object.method
$stdout.rewind
assert_match expected, $stdout.read

# good
assert_output(expected) { puts object.method }

断言静默

使用 assert_silent 断言没有写入 stdout 和 stderr。

# bad
assert_output('', '') { puts object.do_something }

# good
assert_silent { puts object.do_something }

断言路径存在

如果期望路径存在,请使用 assert_path_exists

# bad
assert(File.exist?(path))

# good
assert_path_exists(path)

反驳路径存在

如果期望路径不存在,请使用 refute_path_exists

# bad
assert(!File.exist?(path))
refute(File.exist?(path))

# good
refute_path_exists(path)

断言匹配

如果期望匹配器正则表达式与实际对象匹配,请使用 assert_match

# bad
assert(pattern.match?(object))

# good
assert_match(pattern, object)

反驳匹配

如果预期匹配器正则表达式不匹配实际对象,请使用refute_match

# bad
assert(!pattern.match?(object))
refute(pattern.match?(object))

# good
refute_match(pattern, object)

断言谓词

如果预期在预期对象上测试谓词,并且在应用谓词时返回 true,请使用assert_predicate。使用assert_predicate相对于assertassert_equal的优势在于,当断言失败时,用户友好的错误消息。

# bad
assert expected.zero?     # => Expected false to be truthy
assert_equal 0, expected  # => Expected: 0 Actual: 2

# good
assert_predicate expected, :zero? # => Expected 2 to be zero?.

反驳谓词

如果预期在预期对象上测试谓词,并且在应用谓词时返回 false,请使用refute_predicate

# bad
assert(!expected.zero?)
refute(expected.zero?)

# good
refute_predicate expected, :zero?

断言响应方法

如果预期对象响应方法,请使用assert_respond_to

# bad
assert(object.respond_to?(some_method))

# good
assert_respond_to(object, some_method)

反驳响应方法

如果预期对象不响应方法,请使用refute_respond_to

# bad
assert(!object.respond_to?(some_method))
refute(object.respond_to?(some_method))

# good
refute_respond_to(object, some_method)

断言实例

优先使用assert_instance_of(class, object)而不是assert(object.instance_of?(class))

# bad
assert('rubocop-minitest'.instance_of?(String))

# good
assert_instance_of(String, 'rubocop-minitest')

反驳实例

优先使用refute_instance_of(class, object)而不是refute(object.instance_of?(class))

# bad
refute('rubocop-minitest'.instance_of?(String))

# good
refute_instance_of(String, 'rubocop-minitest')

断言类型

优先使用assert_kind_of(class, object)而不是assert(object.kind_of?(class))

# bad
assert('rubocop-minitest'.kind_of?(String))

# good
assert_kind_of(String, 'rubocop-minitest')

反驳类型

优先使用refute_kind_of(class, object)而不是refute(object.kind_of?(class))

# bad
refute('rubocop-minitest'.kind_of?(String))

# good
refute_kind_of(String, 'rubocop-minitest')

未指定异常

通过assert_raises指定要捕获的异常。这避免了当引发的异常与用户预期不一致时出现误报。

# bad
assert_raises { do_something }

# good
assert_raises(FooException) { do_something }

期望

本节讨论Minitest提供的期望的惯用用法。

注意
本节目前是一个占位符。欢迎贡献!

全局期望

如果使用全局期望(已弃用的方法),请使用_()包装器。

# bad
do_something.must_equal 2
{ raise_exception }.must_raise TypeError

# good
_(do_something).must_equal 2
value(do_something).must_equal 2
expect(do_something).must_equal 2
_ { raise_exception }.must_raise TypeError

有关其用法的更多信息,请查看Minitest::Expectations 文档

钩子

如果使用包含 setupteardown 方法的模块,请确保在测试类 setupteardown 中调用 super

# bad
class TestMeme < Minitest::Test
  include MyHelper

  def setup
    do_something
  end

  def teardown
    clean_something
  end
end

# good
class TestMeme < Minitest::Test
  include MyHelper

  def setup
    super
    do_something
  end

  def teardown
    clean_something
    super
  end
end

钩子排序

按执行顺序排列钩子。

# bad
class SomethingTest < Minitest::Test
  def teardown; end
  def setup; end
end

# good
class SomethingTest < Minitest::Test
  def setup; end
  def teardown; end
end

扩展钩子

before_*after_* 钩子用于扩展 minitest 的库。它们不适合测试开发人员使用。

# bad
class SomethingTest < Minitest::Test
  def before_setup; end
  def before_teardown; end
  def after_setup; end
  def after_teardown; end
end

# good
class SomethingTest < Minitest::Test
  def setup; end
  def teardown; end
end

跳过可运行方法

对于跳过以 test_ 开头的可运行方法,建议使用 skip 而不是 return

# bad
def test_something
  return if condition?
  assert_equal(42, something)
end

# good
def test_something
  skip if condition?
  assert_equal(42, something)
end

文件命名

对于测试文件的命名,请使用一致的命名模式,即使用 test_ 前缀或 _test 后缀。

对于 Rails 应用程序,请遵循 Rails 生成器使用的 _test 后缀约定。

对于 gem,请遵循 bundle gem 生成器使用的 test_ 前缀约定。

测试替身

Minitest 包含 minitest/mock,这是一个简单的模拟/存根系统。

# example
service = Minitest::Mock.new
service.expect(:execute, true)

一个常见的替代方案是 Mocha

# example
service = mock
service.expects(:execute).returns(true)

只选择一种使用 - 避免在一个项目中混合使用两种方法。

测试用例的子类化

Minitest 使用 Ruby 类,如果一个 Minitest 类继承自另一个类,它也会继承其方法,导致 Minitest 两次运行父类的测试。

# bad (unless multiple runs are the intended behavior)
class ParentTest < Minitest::Test
  def test_1
    #... Run twice
  end
end

class ChildTest < ParentTest
  def test_2
    #...
  end
end

在极少数情况下,我们可能希望两次运行测试,但通常情况下,请避免对测试用例进行子类化。

注意:minitest/spec 替代语法禁用测试类之间的继承,因此没有这种行为。

贡献

该指南仍在不断完善中 - 一些指南缺乏示例,一些指南的示例不足以清楚地说明它们。改进这些指南是帮助 Ruby 社区的一个很棒(且简单)的方法!

随着时间的推移,这些问题将(希望)得到解决 - 现在请记住它们。

本指南中所写的内容并非一成不变。我们希望与所有对 Ruby 编码风格感兴趣的人一起努力,以便最终创建一个对整个 Ruby 社区都有益的资源。

欢迎您提交工单或发送代码请求以进行改进。感谢您的帮助!

您也可以通过 Patreon 对项目(以及 RuboCop)进行财务支持。

如何贡献?

很简单,只需遵循以下贡献指南

许可证

传播消息

社区驱动的风格指南对于一个不知道其存在的社区来说毫无用处。在 Twitter 上发布有关该指南的信息,与您的朋友和同事分享。我们收到的每条评论、建议或意见都会使该指南变得更好一点。我们想要拥有最好的指南,不是吗?