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

断言工具的编写

很难为干净的代码找到一个好的定义,因为我们每个人都有自己的单词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

断言工具的编写

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/341496.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

指针的意义与作用

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 本文链接&#xff1a;http://blog.csdn.net/zhanshen112/article/details/80265830

网管型工业交换机如何创建网络冗余

与非网管型工业交换机相比&#xff0c;使用杭州飞畅网管型工业交换机的其中一个好处是其冗余功能。这允许您使用带有额外连接的以太网&#xff0c;因此如果网络上两点之间的一条路径出现故障&#xff0c;则可以使用另一条路径来传递消息。如果一个链路或工业交换机发生故障&…

使用var,Lombok和Fluxtion轻松处理事件

介绍 在本文中&#xff0c;我将结合使用Lombok和Fluxtion这两种产品&#xff0c;以演示工具如何在减少代码编写和交付时间的同时提高代码的可读性。 使用Java 10中的var可以进一步改善这种情况。 产品和var都在构建时使用推断来加速开发。 Fluxtion的精神是最大程度地减少浪费…

Ubuntu安装谷歌拼音输入法

https://blog.csdn.net/xiaokingzi/article/details/88396590?utm_mediumdistribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.edu_weight&depth_1-utm_sourcedistribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.edu_weight

网管型工业交换机如何提高网路流量过滤?

虽然一个非网管型工业交换机会从一个设备端过滤出许多数据包&#xff0c;但还是有很多数据包非网管型工业交换机无法处理&#xff0c;而这些数据包又必须从端口传输到所有设备上。当一个设备接收到不是特定给该设备的数据包时&#xff0c;它必须先耗费资源来处理这些信息数据最…

html5中meter讲解_Java中的得墨meter耳定律–最少知识原理–实际示例

html5中meter讲解得墨meter耳定律&#xff08;也称为最少知识定律&#xff09;是一种编码原理&#xff0c;它表示模块不应该知道其操作的对象的内部细节。 如果代码取决于特定对象的内部细节&#xff0c;则很有可能一旦该对象的内部发生更改&#xff0c;它就会中断。 由于封装是…

C++中双冒号(::) 和 冒号(:) 的用法

C中双冒号(::) 和 冒号(:) 的用法

网络交换机的分类介绍

从广义上来看&#xff0c;交换机分为两种&#xff1a;广域网交换机和局域网交换机。广域网交换机主要应用于电信领域&#xff0c;提供通信基础平台。而局域网交换机则应用于局域网络&#xff0c;用于连接终端设备&#xff0c;如PC机及网络打印机等。 按照现在复杂的网络构成方…

网络交换机的作用有哪些?

现如今&#xff0c;随着通信业的发展以及国民经济信息化的推进&#xff0c;网络交换机市场呈稳步上升态势&#xff0c;以太网技术已成为当今最重要的一种局域网组网技术&#xff0c;网络交换机也就成为了最普及的交换机。那么&#xff0c;网络交换机的作用有哪些呢&#xff1f;…

什么是序列化? 您需要通过示例解释的有关Java序列化的所有知识

在上一篇文章中&#xff0c;我们介绍了在Java中创建对象的5种不同方法 &#xff0c;我解释了如何对序列化对象进行反序列化以创建新对象&#xff0c;并且在此博客中&#xff0c;我将详细讨论序列化和反序列化。 我们将以下面的Employee类对象为例进行说明 // If we use Serial…

CopyTranslator 翻译神器的安装与使用

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 本文链接&#xff1a;https://blog.csdn.net/tianweidadada/article/details/102641908

网络交换机功能介绍

交换机的主要功能包括物理编址、网络拓扑结构、错误校验、帧序列以及流控。目前交换机还具备了一些新的功能&#xff0c;如对VLAN&#xff08;虚拟局域网&#xff09;的支持、对链路汇聚的支持&#xff0c;甚至有的还具有防火墙的功能。 交换机除了能够连接同种类型的网络之外…

C/C++头文件与变量的声明和定义

版权声明&#xff1a;本文为博主转载文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 原文链接&#xff1a;https://blog.csdn.net/mountzf/article/details/51767353 最近遇到了变量重复包含的问题&#xff0c;才发现自己有好多知…

工业以太网交换机的接口知识详解

工业交换机作为局域网节点连接的网络设备&#xff0c;它的接口类型是随着各种局域网和传输介质类型的发展而变化的&#xff0c;分析一下局域网的主要网络类型和传输介质发展过程&#xff0c;我们就不难发现各种工业交换机接口类型。接下来就由飞畅科技的小编来为大家详细介绍下…

C++应用过程中使用知识点

一 读代码中遇到虚函数,此处总结虚函数的用法 转载链接 https://blog.csdn.net/hackbuteer1/article/details/7558868 二 C语言枚举类型&#xff08;C语言enum用法&#xff09;详解 转载链接:http://c.biancheng.net/view/2034.html 三 C运算符重载 转载链接 https://www.…

HOW-TO:具有MySQL的JEE应用程序中具有集群功能的Quartz Scheduler

Quartz Scheduler是Java世界中最流行的调度库之一。 过去&#xff0c;我主要在Spring应用程序中使用Quartz。 最近&#xff0c;我一直在研究要在云中部署的JBoss 7.1.1上运行的JEE 6应用程序中的调度。 我考虑的一种选择是Quartz Scheduler&#xff0c;因为它提供了与数据库的集…

办公网络对工业交换机的功能要求

如今&#xff0c;随着社会的发展&#xff0c;很多公司对网络的要求越来越高&#xff0c;系统越来越复杂&#xff0c;很多老线路需要改造升级&#xff0c;对工业交换机的要求也越来越高。但是&#xff0c;很多企业并不懂如何改造升级。今天飞畅科技的小编就来为大家详细讲解下公…

Ubuntu文件上锁了,怎么打开???亲测有效

第一步&#xff1a;你需要用root权限进入你要开锁的那个文件的目录下 第二步&#xff1a;使用下面的命令&#xff1a; ps&#xff1a;abc是你的用户名字 models是你的目标解锁文件名字 sudo chown abc models

鸡肉和鸡蛋–测试前解决Spring属性

考虑一个负责进行远程调用和获取详细信息的服务类&#xff1a; ... public class CitiesService { private final WebClient.Builder webClientBuilder; private final String baseUrl; public CitiesService( WebClient.Builder webClientBuilder, Value ( "${cityservi…