编写干净的测试–提防魔术

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

干净的代码易于阅读。

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

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

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

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

这是我教程的第三部分,描述了我们如何编写干净的测试。 这次,我们将学习两种可用于从测试中删除幻数的技术。

救援常量

我们使用在我们的代码常量,因为没有常量我们的代码将与被散落幻数 。 使用幻数有两个结果:

  1. 我们的代码很难阅读,因为幻数只是没有意义的值。
  2. 我们的代码很难维护,因为如果必须更改幻数的值,则必须查找该幻数的所有出现并更新每个幻数。

换一种说法,

  • 常数帮助我们用描述其存在原因的某种事物来代替幻数。
  • 常量使我们的代码更易于维护,因为如果常量的值发生变化,我们只需将该更改仅保留到一个位置即可。

如果我们考虑从测试用例中找到的幻数,我们会注意到它们可以分为两组:

  1. 与单个测试类相关的幻数。 这种幻数的典型示例是在测试方法中创建的对象的属性值。 我们应该在测试类中声明这些常量
  2. 与多个测试类别相关的幻数。 这种魔术数字的一个很好的例子是由Spring MVC控制器处理的请求的内容类型。 我们应该将这些常量添加到非实例化类中

让我们仔细看看这两种情况。

在测试类中声明常量

那么,为什么我们要在测试类中声明一些常量呢?

毕竟,如果我们考虑使用常量的好处,首先想到的是,我们应该通过创建包含测试中使用的常量的类来消除测试中的幻数。 例如,我们可以创建一个TodoConstants类,其中包含TodoControllerTestTodoCrudServiceTestTodoTest类中使用的常量。

这是一个坏主意

尽管有时候以这种方式共享数据是明智的,但我们不应轻易做出这个决定,因为在大多数情况下,我们在测试中引入常数的唯一动机是避免输入错误和幻数。

另外,如果幻数仅与单个测试类相关,则将这种依赖关系引入我们的测试是没有道理的,因为我们想最大程度地减少创建的常量的数量。

我认为,处理这种情况的最简单方法是在测试类中声明常量。

让我们找出如何改进本教程前面部分中描述的单元测试。 编写该单元测试以测试RepositoryUserService类的registerNewUserAccount()方法,并且当使用社交符号提供者和唯一的电子邮件地址创建新的用户帐户时,它验证此方法是否正常工作。

该测试用例的源代码如下所示:

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 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 RegistrationForm();registration.setEmail("john.smith@gmail.com");registration.setFirstName("John");registration.setLastName("Smith");registration.setSignInProvider(SocialMediaService.TWITTER);when(repository.findByEmail("john.smith@gmail.com")).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("john.smith@gmail.com", createdUserAccount.getEmail());assertEquals("John", createdUserAccount.getFirstName());assertEquals("Smith", createdUserAccount.getLastName());assertEquals(SocialMediaService.TWITTER, createdUserAccount.getSignInProvider());assertEquals(Role.ROLE_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail("john.smith@gmail.com");verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);}
}

问题在于,此测试用例在创建新的RegistrationForm对象,配置UserRepository模拟的行为,验证返回的User对象的信息是否正确以及验证是否调用了UserRepository模拟的正确方法时使用了幻数。在经过测试的服务方法中。

通过在测试类中声明常量来删除这些幻数之后,测试的源代码如下所示:

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 RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);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);}
}

此示例说明在测试类中声明常量具有三个好处:

  1. 我们的测试用例更易于阅读,因为魔术数字被正确命名的常量所替代。
  2. 我们的测试用例更易于维护,因为我们可以更改常量的值而无需对实际测试用例进行任何更改。
  3. RepositoryUserService类的registerNewUserAccount()方法编写新测试更加容易,因为我们可以使用常量而不是幻数。 这意味着我们不必担心拼写错误。

但是,有时我们的测试使用与多个测试类别真正相关的幻数。 让我们找出如何应对这种情况。

将常量添加到非实例性类

如果该常量与多个测试类相关,则在使用该常量的每个测试类中声明该常量是没有意义的。 让我们看一下一种情况,将常量添加到非实例化类是有意义的。

假设我们必须为REST API编写两个单元测试:

  • 第一个单元测试确保我们不能向数据库添加空的待办事项。
  • 第二个单元测试确保我们不能向数据库添加空笔记。

这些单元测试使用Spring MVC测试框架。 如果您不熟悉它,则可能要看一看我的
Spring MVC测试教程 。

第一个单元测试的源代码如下所示:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class TodoControllerTest {private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyTodoEntry_ShouldReturnHttpRequestStatusBadRequest() throws Exception {TodoDTO addedTodoEntry = new TodoDTO();mockMvc.perform(post("/api/todo").contentType(APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedTodoEntry))).andExpect(status().isBadRequest());}
}

第二个单元测试的源代码如下所示:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class NoteControllerTest {private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyNote_ShouldReturnHttpRequestStatusBadRequest() throws Exception {NoteDTO addedNote = new NoteDTO();mockMvc.perform(post("/api/note").contentType(APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedNote))).andExpect(status().isBadRequest());}
}

这两个测试类都声明一个名为APPLICATION_JSON_UTF8的常量。 该常数指定请求的内容类型和字符集。 同样,很明显,在每个测试类中都需要此常量,其中包含用于控制器方法的测试。

这是否意味着我们应该在每个这样的测试类中声明此常量?

没有!

由于以下两个原因,我们应将此常量移至非实例化类:

  1. 它与多个测试类别相关。
  2. 将其移到单独的类中使我们可以更轻松地为控制器方法编写新测试并维护现有测试。

让我们创建一个最终的WebTestConstants类,将APPLICATION_JSON_UTF8常量移动到该类,然后向创建的类添加一个私有构造函数。

WebTestConstant类的源代码如下所示:

import org.springframework.http.MediaType;public final class WebTestConstants {public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private WebTestConstants() {}
}

完成此操作后,我们可以从测试类中删除APPLICATION_JSON_UTF8常量。 我们的新测试的源代码如下所示:

import com.fasterxml.jackson.databind.ObjectMapper;
import net.petrikainulainen.spring.jooq.config.WebUnitTestContext;
import net.petrikainulainen.spring.jooq.todo.dto.TodoDTO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyTodoEntry_ShouldReturnHttpRequestStatusBadRequest() throws Exception {TodoDTO addedTodoEntry = new TodoDTO();mockMvc.perform(post("/api/todo").contentType(WebTestConstants.APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedTodoEntry))).andExpect(status().isBadRequest());}
}

我们刚刚从测试类中删除了重复的代码,并减少了为控制器编写新测试所需的工作。 太酷了吧?

如果我们更改添加到常量类的常量的值,则此更改将影响使用该常量的每个测试用例。 这就是为什么我们应该最小化添加到常量类的常量的数量

摘要

现在我们知道,常数可以帮助我们编写干净的测试,并减少编写新测试和维护现有测试所需的工作量。 将本博客文章中给出的建议付诸实践时,我们需要记住以下几点:

  • 我们必须给常量和常量类起好名字 。 如果我们不这样做,就不会利用这些技术的全部潜力。
  • 在不弄清楚我们想要用该常数实现什么的情况下,我们不应该引入新的常数。 实际情况通常比此博客文章的示例复杂得多。 如果我们在自动驾驶仪上编写代码,很可能会错过针对当前问题的最佳解决方案。

翻译自: https://www.javacodegeeks.com/2014/05/writing-clean-tests-beware-of-magic.html

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

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

相关文章

Eclipse创建Java Web项目及基本配置

https://www.cnblogs.com/zzlback/p/8552622.html转载于:https://www.cnblogs.com/aiyowei/p/10428638.html

为什么要使用Vuex?

为什么要使用Vuex? 1. 假如不使用 1.1 父子组件依赖同一个state 1.2 兄弟组件依赖同一个state 2. 用了Vuex之后 3. 方便记忆和理解 更多专业前端知识&#xff0c;请上 【猿2048】www.mk2048.com

十个习惯让你精通新的开发技术

Ben Watson&#xff0c;知名开发者。任职于GeoEye&#xff0c;是其所属开发团队的领导者。本文发表于他自己的博客&#xff0c;阐述了十种学习新技术的方法。 1、要看书 在成千上万的编程图书中&#xff0c;可能很大一部分根本毫无用处。但是仍然有很多图书对你的(编程)能力有很…

基于android平台的24点游戏设计与实现需求分析,基于Android平台的24点游戏设计与实现需求分析_毕业设计论文.doc...

基于Android平台的24点游戏设计与实现摘要随着移动设备的普及以及移动设备的硬件的提升&#xff0c;移动设备的功能越来越完善&#xff0c;移动设备的系统平台也日渐火热起来。目前国内最常见的移动开发平台有Symbian&#xff0c;iPhone&#xff0c;Windows Phone以及当下正在逐…

序列化代理模式示例

有些书可以极大地改变您的生活。 其中一本是Joshua Bloch撰写的“ Effective Java” 。 在下面您可能会发现一些小的实验&#xff0c;该实验的灵感来自于本书的第11章“串行化”。 假设我们有一个为继承而设计的类&#xff0c;它本身不是可序列化的 &#xff0c;并且没有无参数…

fit_transform和transform的区别

部分转载 https://blog.csdn.net/weixin_38278334/article/details/82971752 https://www.cnblogs.com/summer-nude/p/7380694.html 写在前面fit和transform没有任何关系&#xff0c;仅仅是数据处理的两个不同环节&#xff0c;之所以出来fit_transform这个函数名&#xff0c;仅…

使用注解配置Spring

使用注解配置Spring 1.为主配置文件引入新的命名空间(约束) 2.开启使用注解代理配置文件 3.在类中使用注解完成配置 将对象注册到容器 修改对象的作用范围 值类型注入 引用类型注入 注意: 初始化|销毁方法 转载于:https://www.cnblogs.com/HiJackykun/p/10428728.html

android监控指纹信息变化,android监听指纹变化(解决反射思路在android10不生效的问题)...

前天偶尔运行代码&#xff0c;一个段异常映入眼帘&#xff0c;我擦android10上反射机制监听不到指纹id等数据了&#xff0c;原因是android10彻底抛弃了之前指纹的api。所以反射不到了。怎么解决这个问题&#xff1f;我们换个思路当然反射依然可以&#xff0c;不过你需要在andro…

[转载]数据结构笔试题基础

第一章 数据结构与算法 一.算法的基本概念计算机解题的过程实际上是在实施某种算法&#xff0c;这种算法称为计算机算法。 1.算法的基本特征&#xff1a;可行性&#xff0c;确定性&#xff0c;有穷性&#xff0c;拥有足够的情报。 2.算法的基本要素&#xff1a;算法中对数据的运…

random_state ---summary

1-简介 random_state 相当于随机数种子random.seed() 。random_state 与 random seed 作用是相同的。可参考&#xff1a;https://www.jianshu.com/p/4deb2cb2502f 对模型没有影响&#xff0c;但是对于一些进行随机选择的过程有影响。比如随机拆分训练集和测试集。随机种子一致的…

基于cookie的SSO单点登录系统

利用COOKIE实现单点登录功能 近期公司要求帮一个项目实现单点登录功能&#xff0c;在综合考量下决定采用cookie实现&#xff0c;大概的流程如下图所&#xff1a; 转载于:https://www.cnblogs.com/buggeerWang/p/10430770.html

js的栈与堆

JavaScript中基本数据类型和引用数据类型的区别 这是我引用别人的 觉得很好 1、基本数据类型和引用数据类型 ECMAScript包括两个不同类型的值&#xff1a;基本数据类型和引用数据类型。 基本数据类型指的是简单的数据段&#xff0c;引用数据类型指的是有多个值构成的对象。 当…

休眠调试–查找查询的来源

Hibernate为什么在程序的哪个部分以及在哪个部分中生成给定的SQL查询并不总是立即的&#xff0c;尤其是当我们处理的是我们自己编写的代码时。 这篇文章将介绍如何配置Hibernate查询日志记录&#xff0c;并将其与其他技巧一起使用&#xff0c;以找出在程序中执行给定查询的原因…

Java各种对象(PO,BO,VO,DTO,POJO,DAO,Entity,JavaBean,JavaBeans)的区分

PO&#xff1a;持久对象 (persistent object)&#xff0c;po(persistent object)就是在Object/Relation Mapping框架中的Entity&#xff0c;po的每个属性基本上都对应数据库表里面的某个字段。完全是一个符合Java Bean规范的纯Java对象&#xff0c;没有增加别的属性和方法。持久…

REMBER

第一句如果我们之间有1000步的距离 你只要跨出第1步我就会朝你的方向走其余的999步 第二句通常愿意留下来跟你争吵的人 才是真正爱你的人第三句付出真心 才会得到真心 却也可能伤得彻底保持距离 就能保护自己 却也注定永远寂寞第四句有时候 不是对方不在乎你 而是你把对…

android 获取程序,Android获取桌面应用程序

转载请注明出处&#xff0c;谢谢&#xff1a;http://blog.csdn.net/harryweasley/article/details/50057029首先在看这个博客之前&#xff0c; 你可以先看下这个博客&#xff0c;http://blog.csdn.net/harryweasley/article/details/50057707里面介绍了两种方式来获取应用程序的…

等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规

随着等保 2.0 时代的到来&#xff0c;网络安全要求更加严格&#xff0c;应用场景更加丰富&#xff0c;等级保护已成为互联网企业义不容辞的责任。作为国内移动安全领域的技术创新企业&#xff0c;几维安全在积极响应等保2.0时代的战略布局&#xff0c;推出等保2.0检测、等保加固…

js中什么是对象,对象的概念是什么?

我们一直在用对象 可是你真的理解对象吗&#xff0c;js中有一个说法是一切皆对象&#xff0c;其实这里说的应该是 一切皆可看作对象 对象就是可以拥有属性和方法的一个集合 士兵就是一个对象&#xff0c;它拥有身高体重的属性&#xff0c;保家卫国&#xff0c;吃饭睡觉的动作方…

在Spring启动时与mongodb一起摇摆

我是Spring Boot的粉丝&#xff0c;这是Spring Boot上的mongodb示例项目。 大多数mongodb示例项目是如此基础&#xff0c;以至于您不会太过分。 您可以搜索普通的Spring Data示例&#xff0c;但是它们可能比您想要的复杂得多。 所以这是我的。 这是我要使用的pom。 <!--?…

android git上传出现错误,热更新上传patch包时提示上传失败,文件不合法

集成配置信息classpath com.android.tools.build:gradle:3.3.2classpath "com.tencent.bugly:tinker-support:1.1.5"distributionUrlhttps://services.gradle.org/distributions/gradle-5.0-all.zipapi com.tencent.bugly:crashreport_upgrade:1.3.6api com.tencent.…