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

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

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

相关文章

如何让MFC程序关闭按钮失效,也无法右击任务栏关闭窗口来关闭?

如何让MFC程序关闭按钮失效&#xff0c;也无法右击任务栏关闭窗口来关闭&#xff0c;即右键任务栏的关闭窗口失效呢&#xff1f;很简单&#xff0c;有一个小窍门就是&#xff1a;响应IDCANCEL消息&#xff0c;具体实现如下&#xff1a; 首先定义消息映射&#xff1a;ON_BN_CLIC…

令人眼睛一亮的履历表

令人眼睛一亮的履历表 你辛辛苦苦写的一份简历&#xff0c;可在人事经理眼里最多只是停留几十秒的时间。如果时机拿捏不好&#xff0c;它会给你造成麻烦&#xff1a;它可能暴露你的短处&#xff0c;而且基本目的都是供人淘汰之用。然而&#xff0c;当你必须做出履历表时&#…

angularjs封装bootstrap官网的时间插件datetimepicker

背景:angular与jquery类库的协作 第三方类库中&#xff0c;不得不提的是大名鼎鼎的jquery,现在基本上已经是国内web开发的必修工具了。它灵活的dom操作&#xff0c;让很多web开发人员欲罢不能。再加上已经很成熟的jquery UI 库和大量jquery 插件&#xff0c;几乎是一个取之不尽…

Java中的得墨meter耳定律–最少知识原理–实际示例

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

课后作业1

自我介绍 我叫张阔&#xff0c;我的爱好是旅行&#xff0c;游览世界的美好风光&#xff1b; 我的码云个人主页是&#xff1a;https://gitee.com/ZkTt0428&#xff1b; 我的第一个项目地址是&#xff1a;https://gitee.com/ZkTt0428/Frist&#xff1b; 目前代码量有10000行了&am…

记录6月28日的体验,自己现实的感触

2016年6月28日&#xff0c;是我自己要求的要去湖北的日子&#xff0c;可是现在&#xff0c;这个只能成为过去式&#xff0c;只能是提一提&#xff01; 2016年5月17日&#xff0c;我在想&#xff0c;我要通宵加班&#xff0c;做好自己最好&#xff0c;最期待完成的3.0&#xff0…

lucene索引

1。lucene的索引尽量不要频繁而小量的编制&#xff0c;比如&#xff1a;用户每发一个贴子&#xff0c;就加入索引&#xff0c;那样对索引的结构和效率不利。 可以采用定时或者定量&#xff0c;批量处理索引的方式。 2。在批量处理的基础上&#xff0c;解决冲突的问题的方案之一…

针对新手的Java EE7和Maven项目-第4部分-定义Ear模块

从前面的部分恢复 第1部分 第2部分 第3部分 我们正在恢复第四部分&#xff0c;目前我们的简单项目有 Web Maven模块&#xff08;战争&#xff09; 一个ejb模块&#xff08;ejb&#xff09;&#xff0c;其中包含我们的无状态会话bean&#xff08;EJB 3.1&#xff09; 第二…

合并两个有序数组,并输出中间值

示例1&#xff1a; nums1 [1,3] nums2 [2,4] output: (23) / 2 2.5 示例2&#xff1a; nums1 [2,5,7] nums2 [3,6] output:5 Python解决方案&#xff1a; def findMedianSortedArrays(self, nums1, nums2):""":type nums1: List[int]:type nums2: List[int…

Python中关于文件路径的简单操作 [转]

1: os.listdir(path) #path为目录 功能相当于在path目录下执行dir命令&#xff0c;返回为list类型 举例&#xff1a; print os.listdir(..) 输出&#xff1a; [a,b,c,d] 2: os.path.walk(path,visit,arg) path &#xff1a;是将要遍历的目录 visit &#xff1…

生产上完成TopN统计流程

背景 现有城市信息和产品信息两张表在MySQL中&#xff0c;另外有用户点击产品日志以文本形式存在hdfs上&#xff0c;现要求统计每个个城市区域下点击量前三的产品名&#xff0c;具体信息见下方。 mysql> show tables; --------------------------------- | Tables_in_d7 …

最大公因数和最小公倍数

一丶 最大公因数求法&#xff1a;辗转相除法(也称欧几里得算法)原理: 二丶最小公倍数求法&#xff1a;两个整数的最小公倍数等于两整数之积除以最大公约数1 #include <iostream>2 3 using namespace std;4 5 //辗转相除法(欧几里得算法)6 7 int gcd(int a, int b)8 {9…

css实现div内一段文本的两端对齐

在一个固定宽度的div内&#xff0c;使得P标签内的文本两端对齐&#xff1a; text-align: justify;text-justify:inter-ideograph; <!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><title>justify</title>…

JPA 2.1实体图–第2部分:在运行时定义延迟/急切加载

这是我关于JPA 2.1实体图的第二篇文章。 第一篇文章描述了命名实体图的用法。 这些可用于定义在编译时将使用查找或查询方法获取的实体和/或属性的图形。 动态实体图以相同的方式但以动态方式这样做。 这意味着您可以在运行时使用EntityGraph API定义实体图。 如果您错过了第一…

HDU1166-敌兵布阵

http://acm.hdu.edu.cn/showproblem.php?pid1166 线段树第一题 #include<cstdio> #define lson l,m,rt<<1 #define rson m1,r,rt<<1|1 const int maxn55555; int sum[maxn<<2]; void PushUP(int rt) {sum[rt]sum[rt<<1]sum[rt<<1|1]; } …

js对象序列化为json字符串

网上找了找将js对象序列化为json字符串的方法。结果都不近人意&#xff0c;最后自己写了一个。 注意你得自己为Date增加toString()方法。 function Serialize(obj){switch(obj.constructor){case Object:var str "{";for(var o in obj){str o ":" Seri…

QT学习三 标准对话框 QMessageBox

QMessageBox内置了几种static方法,例如 QMessageBox::question() 返回值:StandardButton 参数:QWidget * 父窗口&#xff0c;标题名&#xff0c;内容&#xff0c;按钮 YES|NO,默认选中按钮) 示例: 1 #include "mainwindow.h"2 #include <QApplication>3 #incl…

react学习笔记2

1.build文件介绍 &#xff08;1&#xff09;react.js 是react的核心库 &#xff08;2&#xff09;react-dom.js 提供与DOM相关功能 &#xff08;3&#xff09;browser.js 是将JSX语法转为javascript语法 2.组件的继续学习 注意&#xff1a;组件的第一个字母必须大写&…

Spring4:没有默认构造函数的基于CGLIB的代理类

在Spring中&#xff0c;如果要代理的目标对象的类未实现任何接口&#xff0c;则将创建基于CGLIB的代理。 在Spring 4之前&#xff0c;基于CGLIB的代理类需要默认的构造函数。 这不是CGLIB库的限制&#xff0c;而是Spring本身。 幸运的是&#xff0c;从Spring 4开始&#xff0c;…

linux里面i386 i686 i486 i586代表什么?是什么意思

URL:http://hi.baidu.com/software_one/blog/item/85c7ccedd70d6925acafd5e0.html 在linux里面&#xff0c;我们经常会遇到i386 i686 i486 I586 这些代码&#xff0c;例如查看内核版本&#xff1a; [rootlocalhost logs]# uname -a Linux localhost.localdomain 2.6.18-164.el5…