断言工具的编写

很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义。 但是,有一个似乎是通用的定义:

简洁的代码易于阅读。

这可能会让您感到有些惊讶,但我认为该定义也适用于测试代码。 使测试尽可能具有可读性是我们的最大利益,因为:

  • 如果我们的测试易于阅读,那么很容易理解我们的代码是如何工作的。
  • 如果我们的测试易于阅读,那么如果测试失败(不使用调试器),很容易发现问题。

编写干净的测试并不难,但是需要大量的实践,这就是为什么如此多的开发人员为此苦苦挣扎的原因。

我也为此感到挣扎,这就是为什么我决定与您分享我的发现的原因。

这是本教程的第五部分,介绍了如何编写干净的测试。 这次,我们将使用特定于域的语言替换断言。

数据不是那么重要

在我以前的博客文章中,我确定了以数据为中心的测试引起的两个问题。 尽管该博客文章讨论了新对象的创建,但是这些问题对于断言也有效。

让我们刷新内存,看一下单元测试的源代码,该代码可确保当使用唯一电子邮件地址和社交符号创建新用户帐户时, RepositoryUserService类的registerNewUserAccount(RegistrationForm userAccountData)方法能够按预期工作在提供者中。

我们的单元测试如下所示(相关代码突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}
}

如我们所见,从单元测试中找到的断言可确保返回的User对象的属性值正确。 我们的主张确保:

  • email属性的值正确。
  • firstName属性的值正确。
  • lastName属性的值正确。
  • signInProvider的值正确。
  • 角色属性的值正确。
  • 密码为空。

这当然很明显,但是以这种方式重复这些断言很重要,因为它可以帮助我们确定断言的问题。 我们的断言是以数据为中心的 ,这意味着:

  • 读者必须知道返回对象的不同状态 。 例如,如果我们考虑示例,读者必须知道,如果返回的RegistrationForm对象的emailfirstNamelastNamesignInProvider属性具有非null值,并且password属性的值为null,则意味着对象是通过使用社交登录提供程序进行的注册。
  • 如果创建的对象具有许多属性,则我们的断言会乱码我们测试的源代码。 我们应该记住,即使我们要确保返回对象的数据正确无误,但描述返回对象的状态也更为重要。

让我们看看如何改善断言。

将断言转变为特定领域的语言

您可能已经注意到,开发人员和领域专家通常在相同的事情上使用不同的术语。 换句话说,开发人员讲的语言与领域专家讲的语言不同。 这在开发人员和领域专家之间造成了不必要的混乱和摩擦

域驱动设计(DDD)为该问题提供了一种解决方案。 埃里克·埃文斯(Eric Evans)在他的《 域驱动设计 》( Domain-Driven Design)一书中引入了泛在语言一词。

维基百科指定了普遍使用的语言 ,如下所示:

无处不在的语言是围绕领域模型构造的语言,所有团队成员都使用该语言将团队的所有活动与软件联系起来。

如果我们想写断言使用“正确的”语言,那么我们必须弥合开发人员和领域专家之间的鸿沟。 换句话说,我们必须创建一种特定于域的语言来编写断言。

实施我们的领域特定语言

在实现我们特定领域的语言之前,我们必须对其进行设计。 当我们为断言设计特定领域的语言时,我们必须遵循以下规则:

  1. 我们必须放弃以数据为中心的方法,而应该更多地考虑从用户对象中找到其信息的真实用户。
  2. 我们必须使用领域专家所说的语言。

我不会在这里进行详细说明,因为这是一个巨大的主题,不可能在单个博客中进行解释。 如果要了解有关特定于域的语言和Java的更多信息,可以通过阅读以下博客文章开始:

  • Java Fluent API设计器速成课程
  • 用Java创建DSL,第1部分:什么是领域特定语言?
  • 用Java创建DSL,第2部分:流利性和上下文
  • 用Java创建DSL,第3部分:内部和外部DSL
  • 用Java创建DSL,第4部分:元编程很重要

如果遵循这两个规则,则可以为特定于域的语言创建以下规则:

  • 用户具有名字,姓氏和电子邮件地址。
  • 用户是注册用户。
  • 用户是使用社交符号提供者注册的,这意味着该用户没有密码。

现在,我们已经指定了特定领域语言的规则,我们已经准备好实现它。 我们将通过创建一个自定义的AssertJ断言来实现此目的,该断言实现我们特定于域的语言的规则。

我不会在此博客文章中描述所需的步骤,因为我已经写了一篇博客来描述这些步骤 。 如果您不熟悉AssertJ,建议您先阅读该博客文章,然后再阅读本博客文章的其余部分。

我们的自定义断言类的源代码如下所示:

mport org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;public class UserAssert extends AbstractAssert<UserAssert, User> {private UserAssert(User actual) {super(actual, UserAssert.class);}public static UserAssert assertThat(User actual) {return new UserAssert(actual);}public UserAssert hasEmail(String email) {isNotNull();Assertions.assertThat(actual.getEmail()).overridingErrorMessage( "Expected email to be <%s> but was <%s>",email,actual.getEmail()).isEqualTo(email);return this;}public UserAssert hasFirstName(String firstName) {isNotNull();Assertions.assertThat(actual.getFirstName()).overridingErrorMessage("Expected first name to be <%s> but was <%s>",firstName,actual.getFirstName()).isEqualTo(firstName);return this;}public UserAssert hasLastName(String lastName) {isNotNull();Assertions.assertThat(actual.getLastName()).overridingErrorMessage( "Expected last name to be <%s> but was <%s>",lastName,actual.getLastName()).isEqualTo(lastName);return this;}public UserAssert isRegisteredByUsingSignInProvider(SocialMediaService signInProvider) {isNotNull();Assertions.assertThat(actual.getSignInProvider()).overridingErrorMessage( "Expected signInProvider to be <%s> but was <%s>",signInProvider,actual.getSignInProvider()).isEqualTo(signInProvider);hasNoPassword();return this;}private void hasNoPassword() {isNotNull();Assertions.assertThat(actual.getPassword()).overridingErrorMessage("Expected password to be <null> but was <%s>",actual.getPassword()).isNull();}public UserAssert isRegisteredUser() {isNotNull();Assertions.assertThat(actual.getRole()).overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",actual.getRole()).isEqualTo(Role.ROLE_USER);return this;}
}

现在,我们已经创建了一种特定于域的语言,用于将断言写入User对象。 下一步是修改单元测试,以使用我们新的领域特定语言。

用特定于域的语言替换JUnit断言

在重写断言以使用特定于域的语言之后,单元测试的源代码如下所示(相关部分已突出显示):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertThat(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}
}

我们的解决方案具有以下优点:

  • 我们的断言使用领域专家可以理解的语言。 这意味着我们的测试是可执行的规范,它易于理解并且始终是最新的。
  • 我们不必浪费时间弄清楚测试失败的原因。 我们的自定义错误消息可确保我们知道失败的原因。
  • 如果User类的API发生了变化,我们不必修复所有将断言写入User对象的测试方法。 我们唯一需要更改的类是UserAssert类。 换句话说,将实际的断言逻辑从测试方法中移开会使我们的测试不那么脆弱,更易于维护。

让我们花点时间总结一下我们从此博客文章中学到的知识。

摘要

现在,我们已将断言转换为特定领域的语言。 这篇博客文章教会了我们三件事:

  • 遵循以数据为中心的方法会在开发人员和领域专家之间造成不必要的混乱和摩擦。
  • 为我们的断言创建一种特定于域的语言会使我们的测试不那么困难,因为实际的断言逻辑已移至自定义断言类。
  • 如果我们使用特定领域的语言编写断言,则会将测试转换为可执行的规范,这些规范易于理解和说出领域专家的语言。

翻译自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-replace-assertions-with-a-domain-specific-language.html

断言工具的编写

断言工具的编写_编写干净的测试–用特定领域的语言替换断言相关推荐

  1. 编写干净的测试–用特定领域的语言替换断言

    很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 干净的代码易于阅读. 这可能会让您感到有些惊讶,但是我认为该定义也适用于测试代码. 使 ...

  2. python hello world程序编写_编写高质量代码 改善Python程序的91个建议

    建议1:理解Pythonic概念 建议2:编写Pythonic代码 建议3:理解Python与C语言的不同之处 建议4:在代码中适当添加注释 建议5:通过适当添加空行使代码布局更为优雅.合理 建议6: ...

  3. 程序设计文档编写_编写有效的设计系统文档的6个技巧

    程序设计文档编写 重点 (Top highlight) I wrote this article to document what I'm learning professionally while ...

  4. 单元测试编写_编写详尽的单元测试

    单元测试编写 As software developers we all know how important it is to unit test the code that we write. S ...

  5. python自动化测试脚本怎么编写_编写自动化测试脚本心得---菜鸟入门篇

    编写自动化测试脚本心得 -------- 菜鸟入门篇 本文中将不会讲解 ISEE 的测试原理.不说明 Python 的常用语法.不介绍 OTP 测试平 台的架构, 自动化测试组的牛人们已经为我们编写了 ...

  6. 服务器编写_编写下载服务器。 第六部分:描述您发送的内容(内容类型等)...

    服务器编写 就HTTP而言,客户端下载的只是一堆字节. 但是,客户真的很想知道如何解释这些字节. 它是图像吗? 还是ZIP文件? 本系列的最后一部分描述了如何向客户端提示她下载的内容. 设置 内容类型 ...

  7. maven插件编写_编写Maven插件的提示

    maven插件编写 最近,我花了很多时间为Maven编写插件或在其中工作. 它们简单,有趣且有趣. 我以为我会分享一些技巧,使编写它们时的生活更轻松. 提示1:将任务与Mojo分开 最初,您将把moj ...

  8. 分而治之_编写干净的测试–分而治之

    分而治之 好的单元测试应该仅出于一个原因而失败. 这意味着适当的单元测试仅测试一个逻辑概念. 如果我们要编写干净的测试,则必须识别那些逻辑概念,并且每个逻辑概念只编写一个测试用例. 这篇博客文章描述了 ...

  9. 编写干净的测试–分而治之

    好的单元测试应该仅出于一个原因而失败. 这意味着适当的单元测试仅测试一个逻辑概念. 如果我们要编写干净的测试,则必须识别这些逻辑概念,并且每个逻辑概念仅编写一个测试用例. 这篇博客文章描述了我们如何识 ...

最新文章

  1. eclipse 工程复制
  2. MySQL测试环境遇到 mmap(xxx bytes) failed; errno 12解决方法
  3. PHP_define和const的区别/获取所有常量get_defined_constant()
  4. CSS基础教程(企业内部培训)
  5. 有三AI知识星球官宣,BAT等大咖等你来撩
  6. 操作系统(九)进程控制
  7. oracle修改时区无效,Linux 7 修改时区不生效
  8. 英雄会被表彰,这些技术与代码也将被历史铭记
  9. UART串口协议详解
  10. CodeForces 139C Literature Lesson(模拟)
  11. Flask中的 url_for() 函数
  12. java 如何重写迭代器,如何用Java按需定制自己的迭代器
  13. CAN总线技术 | 物理层04 - 终端电阻与双绞线(特性阻抗120欧)
  14. Java(19)JDBC
  15. 深入AsyncTask
  16. Python爬虫实战之解密HTML
  17. 自己动手写Docker系列 -- 3.1构造实现run命令版本的容器
  18. python查看数据库存在表_python sqlite3查看数据库所有表(table)
  19. MATLAB求解3对角系数矩阵方程,实验5.3 用追赶法求解三对角方程组
  20. 实验吧_网站综合渗透_Discuz!

热门文章

  1. P1063-能量项链【区间dp】
  2. 【拓扑排序】【DP】旅行计划(luogu 1137)
  3. Spark入门(八)之WordCount
  4. 一篇文章彻底了解清楚什么是负载均衡
  5. 通往大神之路,百度Java面试题前200页。
  6. 【LSB】图片隐写文档大纲
  7. 使用阿里云火车票查询接口案例——CSDN博客
  8. JQuery 表单校验
  9. React中的唯一标识key(用index VS id)和key的选择
  10. 2020蓝桥杯省赛---java---C---1(约数个数)