1. 概述

JUnit 和 TestNG 这类测试框架提供了基础的断言方法(如 assertTrueassertNotNull)。而 Hamcrest、AssertJ 和 Truth 这类断言框架则提供了更丰富的流式断言方法,通常以 assertThat 开头。

JSpec 是另一个断言框架,它允许我们用接近自然语言规范的方式编写流式断言,尽管风格与其他框架略有不同。

本文将介绍 JSpec 的使用方法,包括编写规范所需的方法以及测试失败时的错误信息。

2. Maven 依赖

添加 javalite-common 依赖(包含 JSpec):

<dependency>
    <groupId>org.javalite</groupId>
    <artifactId>javalite-common</artifactId>
    <version>1.4.13</version>
</dependency>

最新版本请查看 Maven 中央仓库

3. 断言风格对比

传统断言基于规则,而 JSpec 直接描述行为规范。快速对比 JUnit、AssertJ 和 JSpec 的相等性断言:

JUnit 写法:

assertEquals(1 + 1, 2);

AssertJ 写法:

assertThat(1 + 1).isEqualTo(2);

JSpec 写法:

$(1 + 1).shouldEqual(2);

JSpec 采用流式断言风格,但省略了 assert/assertThat 前缀,改用 should。这种写法更贴近真实规范描述,促进 TDD/BDD 实践。

看这个自然语言风格的示例:

String message = "Welcome to JSpec demo";
the(message).shouldNotBe("empty");
the(message).shouldContain("JSpec");

4. 规范的结构

规范语句包含两部分:期望创建器期望方法

4.1. 期望创建器

通过静态导入方法生成 Expectation 对象:a()the()it()$()

$(1 + 2).shouldEqual(3);
a(1 + 2).shouldEqual(3);
the(1 + 2).shouldEqual(3);
it(1 + 2).shouldEqual(3);

这些方法本质相同,仅提供不同表达方式。唯一区别:it() 是类型安全的,只允许同类型对象比较:

it(1 + 2).shouldEqual("3"); // 编译错误

4.2. 期望方法

规范语句的第二部分是期望方法(如 shouldEqualshouldContain),描述具体规范要求。测试失败时抛出 javalite.test.jspec.TestException 异常,附带描述性错误信息(后续章节会展示)。

5. 内置期望

JSpec 提供多种期望方法,以下是各类别及失败场景示例:

5.1. 相等性期望

shouldEqual(), shouldBeEqual(), shouldNotBeEqual()

使用 Object.equals() 判断对象是否相等:

$(1 + 2).shouldEqual(3);

失败场景:

$(1 + 2).shouldEqual(4);

错误信息:

Test object:java.lang.Integer == <3>
and expected java.lang.Integer == <4>
are not equal, but they should be.

5.2. 布尔属性期望

shouldHave(), shouldNotHave()

指定对象的布尔属性是否应返回 true

Cage cage = new Cage();
cage.put(tomCat, boltDog);
the(cage).shouldHave("animals");

要求 Cage 类包含方法:

boolean hasAnimals() {...}

失败场景:

the(cage).shouldNotHave("animals");

错误信息:

Method: hasAnimals should return false, but returned true

shouldBe(), shouldNotBe()

指定对象是否应满足某状态:

the(cage).shouldNotBe("empty");

要求 Cage 类包含方法:

boolean isEmpty() {...}

失败场景:

the(cage).shouldBe("empty");

错误信息:

Method: isEmpty should return true, but returned false

5.3. 类型期望

shouldBeType(), shouldBeA()

指定对象类型:

cage.put(boltDog);
Animal releasedAnimal = cage.release(boltDog);
the(releasedAnimal).shouldBeA(Dog.class);

失败场景:

the(releasedAnimal).shouldBeA(Cat.class);

错误信息:

class com.baeldung.jspec.Dog is not class com.baeldung.jspec.Cat

5.4. 可空性期望

shouldBeNull(), shouldNotBeNull()

指定对象是否应为 null

cage.put(boltDog);
Animal releasedAnimal = cage.release(dogY);
the(releasedAnimal).shouldBeNull();

失败场景:

the(releasedAnimal).shouldNotBeNull();

错误信息:

Object is null, while it is not expected

5.5. 引用期望

shouldBeTheSameAs(), shouldNotBeTheSameAs()

指定对象引用是否相同:

Dog firstDog = new Dog("Rex");
Dog secondDog = new Dog("Rex");
$(firstDog).shouldEqual(secondDog);
$(firstDog).shouldNotBeTheSameAs(secondDog);

失败场景:

$(firstDog).shouldBeTheSameAs(secondDog);

错误信息:

references are not the same, but they should be

5.6. 集合和字符串内容期望

shouldContain(), shouldNotContain()

指定 Collection/Map 是否包含元素:

cage.put(tomCat, felixCat);
the(cage.getAnimals()).shouldContain(tomCat);
the(cage.getAnimals()).shouldNotContain(boltDog);

失败场景:

the(animals).shouldContain(boltDog);

错误信息:

tested value does not contain expected value: Dog [name=Bolt]

也可用于字符串包含检查:

$("Welcome to JSpec demo").shouldContain("JSpec");

⚠️ 特殊用法:通过 toString() 扩展到其他对象:

cage.put(tomCat, felixCat);
the(cage).shouldContain(tomCat); // 检查 cage.toString() 是否包含 tomCat.toString()

6. 自定义期望

6.1. 差异期望

确保代码执行结果与特定值不同:

expect(new DifferenceExpectation<Integer>(4) {
    @Override
    public Integer exec() {
        return 2 + 3;
    }
});

实际应用:验证状态变化(如释放动物后笼子数量变化):

cage.put(tomCat, boltDog);
expect(new DifferenceExpectation<Integer>(cage.size()) {
    @Override
    public Integer exec() {
        cage.release(tomCat);
        return cage.size();
    }
});

失败场景(释放不存在的动物):

cage.release(felixCat); // 数量未变化

错误信息:

Objects: '2' and '2' are equal, but they should not be

6.2. 异常期望

验证代码是否抛出指定异常:

expect(new ExceptionExpectation<ArithmeticException>(ArithmeticException.class) {
    @Override
    public void exec() throws ArithmeticException {
        System.out.println(1 / 0);
    }
});

失败场景 #1(未抛出异常):

System.out.println(1 / 1);

错误信息:

Expected exception: class java.lang.ArithmeticException, but instead got nothing

失败场景 #2(抛出异常类型不符):

Integer.parseInt("x");

错误信息:

class java.lang.ArithmeticException,
but instead got: java.lang.NumberFormatException: For input string: "x"

7. 结论

虽然其他流式断言框架在集合断言、异常断言和 Java 8 集成方面更完善,但 JSpec 提供了独特的规范式断言写法。其 API 简洁,支持自然语言风格断言,并提供清晰的测试失败信息。

完整示例代码见 GitHub 仓库com.baeldung.jspec 包)。


原始标题:Guide to JSpec | Baeldung