c语言断言
断言是我们单元测试的重要组成部分。 但是,忽略它们是如此容易。 很遗憾,因为如果我们忽略断言的重要性,则测试的断言部分会变得冗长而混乱。 可悲的是,我见过(和写过)的大多数测试都遇到了这个问题。
这篇博客文章描述了如何摆脱混乱的断言。 我们学习使用领域专家理解的语言来编写断言。
测试了什么?
让我们快速看一下测试过的类。
Person类是包含单个人的信息的类。 它具有四个字段( id , email , firstName和lastName ),我们可以使用构建器模式创建新的Person对象。
Person类的源代码如下所示:
public class Person {
private Long id;
private String email;
private String firstName;
private String lastName;
private Person() {
}
public static PersonBuilder getBuilder(String firstName, String lastName) {
return new PersonBuilder(firstName, lastName);
}
//Getters are omitted for the sake of clarity
public static class PersonBuilder {
Person build;
private PersonBuilder(String firstName, String lastName) {
build = new Person();
build.firstName = firstName;
build.lastName = lastName;
}
public PersonBuilder email(String email) {
build.email = email;
return this;
}
public PersonBuilder id(Long id) {
build.id = id;
return this;
}
public Person build() {
return build;
}
}
}
何必呢?
为了了解使用标准JUnit断言有什么问题,我们必须分析使用它们的单元测试。 我们可以按照以下步骤编写单元测试,以确保新的Person对象的构造工作正常:
- 通过使用builder类创建一个新的Person对象。
- 通过使用Assert类的assertEquals()方法编写断言。
我们的单元测试的源代码如下所示:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class PersonTest {
@Test
public void build_JUnitAssertions() {
Person person = Person.getBuilder("Foo", "Bar")
.email("foo.bar@email.com")
.id(1L)
.build();
assertEquals(1L, person.getId().longValue());
assertEquals("Foo", person.getFirstName());
assertEquals("Bar", person.getLastName());
assertEquals("foo.bar@email.com", person.getEmail());
}
}
这个单元测试很短而且很干净,但是标准的JUnit断言有两个大问题:
- 当断言数量增加时,测试方法的长度也会增加。 这看起来似乎很明显,但是较大的断言部分使测试更难理解。 很难理解我们希望通过此测试实现什么。
- 标准的JUnit断言使用错误的语言。 标准的JUnit断言使用“技术”语言。 这意味着领域专家无法理解我们的测试(我们也无法理解)。
我们可以做得更好。 好了很多。
紧急救援
FEST-Assert是一个库,使我们可以在测试中编写流畅的断言。 通过执行以下步骤,我们可以使用FEST-Assert 1.4创建一个简单的断言:
- 调用Assertions类的静态assertThat()方法,并将实际值作为方法参数传递。 此方法返回一个断言对象。 断言对象是扩展GenericAssert类的类的实例。
- 使用断言对象的方法指定断言。 我们可以使用的方法取决于返回对象的类型( Assertions类的assertThat()方法是重载方法,返回对象的类型取决于method参数的类型)。
当我们使用FEST-Assert重写单元测试时,其源代码如下:
import org.junit.Test;
import static org.fest.assertions.Assertions.assertThat;
public class PersonTest {
@Test
public void build_FESTAssert() {
Person person = Person.getBuilder("Foo", "Bar")
.email("foo.bar@email.com")
.id(1L)
.build();
assertThat(person.getId()).isEqualTo(1L);
assertThat(person.getFirstName()).isEqualTo("Foo");
assertThat(person.getLastName()).isEqualTo("Bar");
assertThat(person.getEmail()).isEqualTo("foo.bar@email.com");
}
}
这比使用标准JUnit断言的测试更具可读性。 然而, 它也面临着同样的问题 。
另一个问题是断言失败时显示的默认消息不是很可读。 例如,如果用户的名字是“ Bar”,则会显示以下消息:
expected:<'[Foo]'> but was:<'[Bar]'>
我们可以通过在声明中添加自定义消息来解决此问题。 让我们看看这是如何完成的。
为FEST声明断言指定自定义消息
通过执行以下步骤,我们可以编写一个包含自定义错误消息的断言:
- 通过使用String类的format()方法创建错误消息。
- 调用Assertions类的静态assertThat()方法,并将实际值作为方法参数传递。 此方法返回一个断言对象。 断言对象是扩展GenericAssert类的类的实例。
- 调用GenericAssert类的overridingErrorMessage ()方法,并将创建的错误消息作为方法参数传递。
- 使用断言对象的方法指定断言。 我们可以使用的方法取决于返回对象的类型( Assertions类的assertThat()方法是重载方法,返回对象的类型取决于method参数的类型)。
我们的单元测试的源代码如下所示:
import org.junit.Test;
import static org.fest.assertions.Assertions.assertThat;
public class PersonTest {
@Test
public void build_FESTAssert_CustomMessages() {
Person person = Person.getBuilder("Foo", "Bar")
.email("foo.bar@email.com")
.id(1L)
.build();
String idMessage = String.format("Expected id to be <%d> but was <%d>", 1L, person.getId());
assertThat(person.getId())
.overridingErrorMessage(idMessage)
.isEqualTo(1L);
String firstNameMessage = String.format("Expected firstName to be <%s> but was <%s>", "Foo", person.getFirstName());
assertThat(person.getFirstName())
.overridingErrorMessage(firstNameMessage)
.isEqualTo("Foo");
String lastNameMessage = String.format("Expected lastName to be <%s> but was <%s>", "Bar", person.getLastName());
assertThat(person.getLastName())
.overridingErrorMessage(lastNameMessage)
.isEqualTo("Bar");
String emailMessage = String.format("Expected email to be <%s> but was <%s>", "foo.bar@email.com", person.getEmail());
assertThat(person.getEmail())
.overridingErrorMessage(emailMessage)
.isEqualTo("foo.bar@email.com");
}
}
如果用户的名字是“ Bar”,则显示以下消息:
Expected firstName to be <Foo> but was <Bar>
我们修复了一个问题,但我们的修复导致了另一个问题:
该测试不可读! 比我们以前的测试差很多!
但是,所有的希望并没有失去。 让我们了解如何使用在本博文中学到的信息来创建特定领域的语言。
创建特定领域的语言
Wikipedia定义术语领域特定语言如下:
域特定语言(DSL)是专用于特定应用程序域的计算机语言。
当我们遵循此定义时,对于域特定语言我们将获得以下要求:
- 它必须使用领域专家理解的语言。 例如,一个人的名字不等于“ Foo”。 一个人的名字叫“ Foo”。
- 断言必须具有使用域特定语言的自定义错误消息。
- 它必须具有流畅的API。 换句话说,必须有可能链接断言。
注意:如果要获取有关使用Java实现特定于域的语言的更多信息,请阅读标题为《使用Java 内部特定于域的语言的方法》和Java Fluent API Designer Crash Course的文章 。
我们可以通过创建自定义断言来为单元测试创建特定领域的语言。 我们可以按照以下步骤进行操作:
- 创建一个PersonAssert类。
- 扩展GenericAssert类,并提供以下类型参数:
- 第一个type参数指定自定义断言类的类型。 将此类型参数的值设置为PersonAssert 。
- 第二个type参数指定实际值的类型。 将此类型参数的值设置为Person 。
- 创建一个将Person对象作为构造函数参数的构造函数。 通过调用GenericAssert类的构造函数并传递以下对象作为构造函数参数来实现此构造函数:
- 第一个构造函数参数指定自定义断言的类。 将此构造函数参数的值设置为PersonAssert.class 。
- 第二个构造函数参数是实际值。 将作为构造函数参数提供的Person对象传递给超类的构造函数。
- 向创建的类中添加一个assertThat()方法。 此方法将Person对象作为方法参数,并返回PersonAssert对象。 通过执行以下步骤来实现此方法:
- 创建一个新的PersonAssert对象,并将Person对象作为构造函数参数传递。
- 返回创建的PersonAssert对象。
- 创建用于针对实际的Person对象编写断言的方法。 我们需要为email , firstName , id和lastName字段创建断言方法。 我们可以按照以下步骤实现每种方法:
- 通过调用GenericAssert类的isNotNull()方法,确保实际的Person对象不为null。
- 通过使用String类的format()方法创建自定义错误消息。
- 确保Person对象的字段的值等于期望值。 我们可以按照以下步骤进行操作:
- 调用Assertions类的assertThat()方法,并提供实际的字段值作为方法参数。
- 通过调用GenericAssert类的overridingErrorMessage ()方法来覆盖默认错误消息。 将自定义错误消息作为方法参数传递。
- 确保实际属性值等于预期值。 我们可以通过调用GenericAssert类的isEqualTo()方法并传递期望值作为方法参数来实现。
- 返回对PersonAssert对象的引用。 这确保了我们可以在单元测试中链接断言。
PersonAssert类的源代码如下所示:
package example;
import org.fest.assertions.Assertions;
import org.fest.assertions.GenericAssert;
public class PersonAssert extends GenericAssert<PersonAssert, Person> {
protected PersonAssert(Person actual) {
super(PersonAssert.class, actual);
}
public static PersonAssert assertThat(Person actual) {
return new PersonAssert(actual);
}
public PersonAssert hasEmail(String email) {
isNotNull();
String errorMessage = String.format(
"Expected email to be <%s> but was <%s>",
email,
actual.getEmail()
);
Assertions.assertThat(actual.getEmail())
.overridingErrorMessage(errorMessage)
.isEqualTo(email);
return this;
}
public PersonAssert hasFirstName(String firstName) {
isNotNull();
String errorMessage = String.format(
"Expected first name to be <%s> but was <%s>",
firstName,
actual.getFirstName()
);
Assertions.assertThat(actual.getFirstName())
.overridingErrorMessage(errorMessage)
.isEqualTo(firstName);
return this;
}
public PersonAssert hasId(Long id) {
isNotNull();
String errorMessage = String.format(
"Expected id to be <%d> but was <%d>",
id,
actual.getId()
);
Assertions.assertThat(actual.getId())
.overridingErrorMessage(errorMessage)
.isEqualTo(id);
return this;
}
public PersonAssert hasLastName(String lastName) {
isNotNull();
String errorMessage = String.format(
"Expected last name to be <%s> but was <%s>",
lastName,
actual.getLastName()
);
Assertions.assertThat(actual.getLastName())
.overridingErrorMessage(errorMessage)
.isEqualTo(lastName);
return this;
}
}
现在,我们可以使用PersonAssert类重写单元测试。 我们的单元测试的源看起来如下:
import org.junit.Test;
import static net.petrikainulainen.junit.dsl.PersonAssert.assertThat;
public class PersonTest {
@Test
public void build_FESTAssert_DSL() {
Person person = Person.getBuilder("Foo", "Bar")
.email("foo.bar@email.com")
.id(1L)
.build();
assertThat(person)
.hasId(1L)
.hasFirstName("Foo")
.hasLastName("Bar")
.hasEmail("foo.bar@email.com");
}
}
为什么如此重要?
现在,我们已经将凌乱的断言变成了特定领域的语言。 这使我们的测试更具可读性,但所做的更改并不完全是表面上的。
我们的解决方案具有三个主要优点:
- 我们将实际的断言逻辑从测试方法移至PersonAssert类。 如果Person类的API发生更改,我们只需更改PersonAssert类。 我们只是使测试不那么脆弱,更易于维护。
- 因为我们的断言使用领域专家理解的语言,所以我们的测试成为我们文档的重要组成部分。 我们的测试准确定义了我们的应用程序在特定情况下的行为。 它们是可执行的规范 ,始终是最新的。
- 自定义错误消息和更具可读性的API确保了我们不必浪费时间来尝试找出测试失败的原因。 我们立即知道为什么失败。
实现特定于域的语言需要做一些额外的工作,但是正如我们所看到的,值得付出努力。 一个好的单元测试既可读又易于维护,但是一个好的单元测试也说明了其存在的原因。
将断言转换为特定领域的语言使我们离目标更近了一步。
- PS此博客文章的示例应用程序可在Github上获得 。
翻译自: https://www.javacodegeeks.com/2013/11/turning-assertions-into-a-domain-specific-language.html
c语言断言