跳至内容

测试 Crystal 代码

Crystal 在 Spec 模块 中附带了一个功能齐全的规范库。它为编写代码行为的可执行示例提供了一种结构。

它受到 Rspec 的启发,包含一个领域特定语言 (DSL),允许您以类似于普通英语的方式编写示例。

一个基本的规范看起来像这样

require "spec"

describe Array do
  describe "#size" do
    it "correctly reports the number of elements in the Array" do
      [1, 2, 3].size.should eq 3
    end
  end

  describe "#empty?" do
    it "is true when no elements are in the array" do
      ([] of Int32).empty?.should be_true
    end

    it "is false if there are elements in the array" do
      [1].empty?.should be_false
    end
  end
end

规范文件剖析

要使用规范模块和 DSL,您需要在规范文件中添加 require "spec"。许多项目使用自定义的 规范助手 来组织这些包含的内容。

具体的测试用例在 it 块中定义。一个可选的(但强烈推荐的)描述性字符串说明了它的目的,一个块包含执行测试的主要逻辑。

已经定义或概述但尚未预期工作 的测试用例可以使用 pending 而不是 it 来定义。它们不会被运行,但会在规范报告中显示为挂起。

一个 it 块包含一个应该调用要测试的代码并定义对其期望的示例。每个示例可以包含多个期望,但它应该只测试一个特定的行为。

当包含 spec 时,每个对象都有实例方法 #should#should_not。这些方法在要测试的值上调用,并带有一个期望作为参数。如果满足期望,代码执行将继续。否则,示例已失败,此块中的其他代码将不会被执行。

在测试文件中,规范由示例组结构化,这些组由 describecontext 部分定义。通常,顶级 describe 定义规范要测试的外部单元(例如类)。更进一步的 describe 部分可以嵌套在外部单元中,以指定要测试的更小的单元(例如单个方法)。

对于单元测试,建议遵循方法命名约定:外部 describe 是类的名称,内部 describe 目标是方法。实例方法以 # 为前缀,类方法以 . 为前缀。

要建立某些上下文 - 考虑空数组包含元素的数组 - context 方法可用于向读者传达这一点。它具有不同的名称,但行为与 describe 完全相同。

describecontext 接受一个描述作为参数(通常应该是一个字符串)和一个包含单个规范或嵌套分组的块。

期望

期望定义要测试的值(实际)是否与某个值或特定条件匹配。

等价性、标识和类型

有一些方法可以创建期望,这些期望测试等价性 (eq)、标识 (be)、类型 (be_a) 和 nil (be_nil)。请注意,标识期望使用 .same?,它测试 #object_id 是否相同。这只有在预期值指向同一个对象而不是一个等效的对象时才为真。这仅对引用类型有效,对结构或数字等值类型无效。

actual.should eq(expected)   # passes if actual == expected
actual.should be(expected)   # passes if actual.same?(expected)
actual.should be_a(expected) # passes if actual.is_a?(expected)
actual.should be_nil         # passes if actual.nil?

真值

actual.should be_true   # passes if actual == true
actual.should be_false  # passes if actual == false
actual.should be_truthy # passes if actual is truthy (neither nil nor false nor Pointer.null)
actual.should be_falsey # passes if actual is falsey (nil, false or Pointer.null)

比较

actual.should be < expected  # passes if actual <  expected
actual.should be <= expected # passes if actual <= expected
actual.should be > expected  # passes if actual >  expected
actual.should be >= expected # passes if actual >= expected

其他匹配器

actual.should be_close(expected, delta) # passes if actual is within delta of expected:
#                                         (actual - expected).abs <= delta
actual.should contain(expected) # passes if actual.includes?(expected)
actual.should match(expected)   # passes if actual =~ expected

期望错误

这些匹配器运行一个块,如果它引发了某个异常,则通过。

expect_raises(MyError) do
  # Passes if this block raises an exception of type MyError.
end

expect_raises(MyError, "error message") do
  # Passes if this block raises an exception of type MyError
  # and the error message contains "error message".
end

expect_raises(MyError, /error \w{7}/) do
  # Passes if this block raises an exception of type MyError
  # and the error message matches the regular expression.
end

expect_raises 返回已恢复的异常,因此它可以用于进一步的期望,例如验证异常的特定属性。

ex = expect_raises(MyError) do
  # Passes if this block raises an exception of type MyError.
end
ex.my_error_value.should eq "foo"

专注于一组规范

describecontextit 块可以使用 focus: true 标记,例如

it "adds", focus: true do
  (2 + 2).should_not eq(5)
end

如果任何带有 focus: true 的内容都被标记,那么只有那些示例会被运行。

标记规范

标签可用于对规范进行分组,允许在向规范运行器提供 --tag 参数时仅运行规范的子集(参见 使用编译器)。

describecontextit 块可以被标记,例如

it "is slow", tags: "slow" do
  sleep 60
  true.should be_true
end

it "is fast", tags: "fast" do
  true.should be_true
end

标记一个示例组 (describecontext) 将扩展到所有包含的示例。

多个标签可以通过给出 Enumerable 来指定,例如 ArraySet

运行规范

Crystal 编译器有一个 spec 命令,它包含用于约束运行哪些示例并调整输出的工具。项目的所有规范都是通过命令 crystal spec 编译和执行的。

按照惯例,规范位于项目的 spec/ 目录中。规范文件必须以 _spec.cr 结尾,以便编译器命令能够识别它们。

您可以从文件夹树、单个文件或文件中特定行编译和运行规范。如果指定的行是 describecontext 部分的开头,则该组中的所有规范都会运行。

默认格式化程序输出失败规范的文件和行样式命令,这使得很容易重新运行仅此单个规范。

您可以使用开关 --no-color 关闭颜色。

随机化规范顺序

规范默认情况下按定义的顺序运行,但可以通过将 --order random 传递给 crystal spec 来随机运行。

随机顺序运行的规范将在完成后显示一个种子值。此种子值可用于通过将种子值传递给 --order 来以相同顺序重新运行规范。

示例

# Run all specs in files matching spec/**/*_spec.cr
crystal spec

# Run  all specs in files matching spec/**/*_spec.cr without colors
crystal spec --no-color

# Run all specs in files matching spec/my/test/**/*_spec.cr
crystal spec spec/my/test/

# Run all specs in spec/my/test/file_spec.cr
crystal spec spec/my/test/file_spec.cr

# Run the spec or group defined in line 14 of spec/my/test/file_spec.cr
crystal spec spec/my/test/file_spec.cr:14

# Run all specs tagged with "fast"
crystal spec --tag 'fast'

# Run all specs not tagged with "slow"
crystal spec --tag '~slow'

还有其他选项用于按名称运行规范、调整输出格式、进行试运行等,请参见 使用编译器

规范助手

许多项目使用自定义规范助手文件,通常命名为 spec/spec_helper.cr

此文件用于需要 spec 和其他包含的内容,例如每个规范文件所需的项目中的代码。这也是定义全局助手方法的好地方,这些方法使编写规范更容易并避免代码重复。

spec/spec_helper.cr
require "spec"
require "../src/my_project.cr"

def create_test_object(name)
  project = MyProject.new(option: false)
  object = project.create_object(name)
  object
end
spec/my_project_spec.cr
require "./spec_helper"

describe "MyProject::Object" do
  it "is created" do
    object = create_test_object(name)
    object.should_not be_nil
  end
end