很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词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. 分而治之_编写干净的测试–分而治之

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

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

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

  4. 怎样编写测试类测试分支_编写干净的测试–天堂中的麻烦

    怎样编写测试类测试分支 如果我们的代码有明显的错误,我们很有动力对其进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所 ...

  5. 怎样编写测试类测试分支_编写干净的测试-被认为有害的新内容

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

  6. 编写干净的测试–天堂中的麻烦

    如果我们的代码有明显的错误,我们很有动力进行改进. 但是,在某些时候,我们认为我们的代码"足够好"并继续前进. 通常,当我们认为改进现有代码的好处小于所需的工作时,就会发生这种情况 ...

  7. 编写干净的测试-被认为有害的新内容

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

  8. 怎样编写测试类测试分支_编写干净的测试–从配置开始

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

  9. c++返回指针时候注意提防_编写干净的测试–提防魔术

    c++返回指针时候注意提防 很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词clean的定义. 但是,有一个似乎是通用的定义: 简洁的代码易于阅读. 这可能会让您感到有些惊讶,但我认为该 ...

最新文章

  1. ADAS虚拟车道边界生成
  2. GSM中时隙、信道、突发序列、帧的解释
  3. 小苗快速培养盆景苗方法_九里香盆景的制作与养护
  4. 博为峰Java技术题 ——JavaSE Java 方法Ⅰ
  5. shell与export命令
  6. ncurses输出修饰:attron(),attroff(),attrset(),attr_get(),attr_,wattr_,chgat(),mvchgat()
  7. Web 前端开发框架收集
  8. Mybatis 拦截器简述
  9. MFC建立简单绘图板
  10. Android 锁屏通知 权限获取
  11. 程序员的高考题又来了
  12. 解决Chrome谷歌浏览器Adobe Flash Player 插件已被屏蔽的问题
  13. 算法笔记随笔:分数的化简,四则运算和输出
  14. [体感游戏]关于体感游戏的一些思考(二) --- POV和基本场景
  15. 【Designing ML Systems】第 7 章 :模型部署和预测服务
  16. 手拉手微商俱乐部 微信营销实战课程开讲啦
  17. [推荐系统]个性化推荐的十大挑战
  18. 贾跃亭要回国圆“造车梦”?FF关联公司广州拿地601亩
  19. 5言律诗-望月(原创新作)
  20. windows获取系统显卡信息(一)

热门文章

  1. tomcat 实现 文件共享,查看文件目录
  2. excel如何生成mysql的sql语句_excel 批量生成SQL语句
  3. ES报错:Connection reset by peer 解决经历
  4. mod auth mysql_Apache使用mysql认证用户
  5. Java Web开发与实战_Java Web开发技术与实战项目
  6. Integer和Int的比较,谈谈拆卸和装箱
  7. 带有Prometheus的弹簧靴和Micrometer第5部分:旋转Prometheus
  8. gradle junit5_JUnit 5和Selenium –使用Gradle,JUnit 5和Jupiter Selenium设置项目
  9. 随机数生成java代码_Java Bullshifier –生成大量随机代码库
  10. jboss8日志级别设置_罐中研讨会:设置JBoss BPM Suite全日研讨会