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

好的单元测试应该仅出于一个原因而失败。 这意味着适当的单元测试仅测试一个逻辑概念。

如果我们要编写干净的测试,则必须识别这些逻辑概念,并且每个逻辑概念仅编写一个测试用例。

这篇博客文章描述了我们如何识别从测试中发现的逻辑概念,以及如何将现有的单元测试分成多个单元测试。

干净还不够好

让我们先看一下单元测试的源代码,该源代码确保当使用唯一的电子邮件地址和社交登录提供者创建新用户帐户时, 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 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);}
}

这个单元测试非常干净。 毕竟,我们的测试类,测试方法以及在测试方法内部创建的局部变量具有描述性名称。 我们还用常数替换了幻数,并创建了特定领域的语言来创建新对象和编写断言。

但是, 我们可以使这项测试更好

这个单元测试的问题是它可能由于多种原因而失败。 如果发生以下情况,它将失败:

  1. 我们的服务方法不会检查是否从我们的数据库中找不到输入到注册表中的电子邮件地址。
  2. 持久化的User对象的信息与在注册表中输入的信息不匹配。
  3. 返回的User对象的信息不正确。
  4. 我们的服务方法通过使用PasswordEncoder对象为用户创建密码。

换句话说,此单元测试测试了四个不同的逻辑概念,这导致以下问题:

  • 如果此测试失败,我们不一定知道为什么失败。 这意味着我们必须阅读单元测试的源代码。
  • 单元测试有点长,这使得阅读起来有些困难。
  • 很难描述预期的行为。 这意味着很难为我们的测试方法找到好名字。

通过确定该单元测试将失败的情况,我们可以确定单个单元测试所涵盖的逻辑概念。

这就是为什么我们需要将此测试分为四个单元测试。

一测试,一故障

下一步是将单元测试分成四个新的单元测试,并确保每个单元测试都测试一个逻辑概念。 我们可以通过编写以下单元测试来做到这一点:

  1. 我们需要确保我们的服务方法检查用户提供的电子邮件地址是否唯一。
  2. 我们需要验证持久性User对象的信息是否正确。
  3. 我们需要确保返回的User对象的信息正确。
  4. 我们需要验证我们的服务方法没有为使用社交登录提供商的用户创建编码密码。

编写完这些单元测试之后,测试类的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
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 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_ShouldCheckThatEmailIsUnique() 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);registrationService.registerNewUserAccount(registration);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() 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);registrationService.registerNewUserAccount(registration);ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);verify(repository, times(1)).save(userAccountArgument.capture());User createdUserAccount = userAccountArgument.getValue();assertThat(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() 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);}@Testpublic void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() 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);registrationService.registerNewUserAccount(registration);verifyZeroInteractions(passwordEncoder);}
}

编写仅测试一个逻辑概念的单元测试的明显好处是,很容易知道为什么测试失败。 但是,此方法还有其他两个好处:

  • 指定期望的行为很容易。 这意味着更容易为我们的测试方法找出好名字。
  • 由于这些单元测试比原始单元测试要短得多,因此更容易弄清测试方法/组件的要求。 这有助于我们将测试转换为可执行规范。

让我们继续并总结从这篇博客文章中学到的知识。

摘要

现在,我们已经成功地将单元测试分为四个较小的单元测试,它们测试了一个逻辑概念。 这篇博客文章教会了我们两件事:

  • 我们了解到,通过确定测试失败的情况,我们可以确定单个单元测试所涵盖的逻辑概念。
  • 我们了解到,编写仅测试一个逻辑概念的单元测试有助于我们将测试用例编写成可执行的规范,从而确定测试方法/组件的要求。

翻译自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-divide-and-conquer.html

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

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

相关文章

初学servlet之使用web.xml配置

先写两个servlet,之后展示web.xml配置 package app01c;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.Htt…

根本不存在 DIV + CSS 布局这回事

实际上div不是用来布局的&#xff0c;div只是用来表示一个其它元素都无法准确表达语意的一个块区&#xff0c;只有CSS是用于布局的&#xff0c;所以根本就不存在divCSS布局这回事。反过来&#xff0c;table布局的时候经常依赖于CSS定义一个单元格的布局属性&#xff0c;所以可以…

c语言空格键 key,c语言获得键盘的按键

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼TC 2.0版#include#includeint main(){int key;while(1){keygetch();/*用于去掉第一个字节*/if(key27) break; /*如果是ESC退出*/if(key>31 && key<127) /*如果不是特殊键*/{printf("You have pressed %c Press …

仔细研究Java Identity API

在深入探讨之前&#xff0c;让我们看一下有关Java Identity API JSR 351的一些快速事实。 这仍在进行中。 。 。 JSR是什么时候发起的&#xff1f; 该JSR在2011年10月通过了批准投票&#xff0c;随后在2011年11月成立了专家组。 谁负责此规范&#xff1f; Java Identity AP…

WEB页面的生命周期,DOMContentLoaded,load,beforeunload,unload

简言 理解WEB页面的生命周期&#xff0c;文档加载事件及顺序对WEB开发有十分的重要意义。如果不理解&#xff0c;在元素未加载就提前操作元素&#xff0c;则得不到想要的结果。而如果页面完全加载完成后&#xff0c;再进行操作&#xff0c;则又会影响用户体验。 一般来说&…

WinAPI: SetLayeredWindowAttributes - 设置窗口的透明

这是来宾 Dolby 在 http://www.cnblogs.com/del/archive/2008/03/08/1081295.html#1096814 询问的问题. //声明: SetLayeredWindowAttributes(Hwnd: THandle; {窗口句柄}crKey: COLORREF; {透明色}bAlpha: Byte; {Alpha 值}dwFlags: DWORD {LWA_COLORKEY(1)表示使用透明…

动态规划-线性dp-hdu-4055

https://www.cnblogs.com/31415926535x/p/10423047.html 这道题是大连的某一年的现场赛的题hdu-4055 &#xff0c;&#xff0c;&#xff0c;刚开始做线性dp的题&#xff0c;&#xff0c;看了好半天才看懂解法&#xff0c;&#xff0c; 分析 参考1参考2 题目的意思就是给出一个仅…

JQuery .net WebService 参数必须一致

$.ajax({type: "POST",contentType:"application/json; charsetutf-8",url: "/LearnJQuery/ajax1.asmx/response1",data:"{username:\""$("#inputName").val()"\"}",上面的username必须和webservice中的…

c语言按shift用户随时退出,2014年云南省“三校生”高考计算机第三次模拟试卷...

密班级&#xff1a; 姓名&#xff1a; 学号&#xff1a;密 封 线 内 不 得 答 题玉龙职高2012年高考第三次模拟试卷计算机基础总分&#xff1a;150分&#xff0c;考试时间&#xff1a;120分钟。一、单项选择题(在每小题给出的四个选项中&#xff0c;只有一个是符合题目要求的&a…

无状态EJB:池化和生命周期

无状态EJB池和生命周期的摘要视图&#xff08;注释&#xff09;。 对新手有用。 。 。 。 。 EJB池&#xff1a;快速概述 EJB实例存储在称为EJB池的位置–这不过是内存中的缓存 。 无状态EJB通常按需实例化&#xff0c;即&#xff0c;当客户端调用Bean上的方法时。 但是&…

有意思的批处理

echo off setlocal enabledelayedexpansion set b/-\ /-\ ** set 速度1 set 退格 :b for /l %%i in (0,1,200) do call :a %%i goto :b :a set/a a%1%%10 set/a c%a%%%4 if %a% EQU 0 set/p▌<nul if %c% EQU 3 (set/p^|<nul) else (set/p!b:~%a%,1!<nul) ping/n %速度…

1.原生js封装的获取某一天是当年的第几周方法

function getWeek(str){//str格式为yyy-mm-dd//周日归到了本周var dnew Date(str);var dayd.getDay();var originDated.getFullYear() - "01" - "01" 00:00:00;var nowDated.getFullYear() - ((d.getMonth() 1)>9?(d.getMonth() 1):0 (d.getMonth() 1…

代码整洁之道——有意义的命名(持续更新中)

我们给变量、参数、类、包&#xff0c;源代码和源代码所在目录命名&#xff0c;也给jar文件、war文件和ear文件命名。 We name variables, parameters, classes, packages, source code, and the directory where the source code resides, as well as jar files, war files, a…

json - json对象和json字符串直接的相互转换

一、json字符串转json对象 1.json字符串转json对象 var obj JSON.parse(str); //由json字符串转换为json对象 2.获取对象的value console.log(obj.attr); console.log(obj["attr"]); 二、json对象转json字符串 var jsonstr JSON.stringify(obj); / 转载于:https://…

android根据拍摄url获取格式,Android如何通过URI获取文件路径示例代码

前言最近在工作的过程中&#xff0c;遇到不同 Android 版本下 URI 采用不同方式来获取文件路径的问题。因为需求的原因&#xff0c;要求拍照上传或者从相册中选择图片上传&#xff0c;而且图片是需要经过压缩的&#xff0c;大小不能超过2M。很快&#xff0c;拍照的这部分就搞定…

休眠类型初学者指南

基本映射概念 学习Hibernate时&#xff0c;许多人喜欢跳到父子关联&#xff0c;而无需掌握对象关系映射的基础知识。 在开始对实体关联进行建模之前&#xff0c;了解各个实体的基本映射规则非常重要。 休眠类型 休眠类型是SQL类型和Java原语/对象类型之间的桥梁。 这些是Hibe…

fixed 语句(C# 参考)

fixed 语句禁止垃圾回收器重定位可移动的变量。fixed 语句只能出现在不安全的上下文中。Fixed 还可用于创建固定大小的缓冲区。 备注 fixed 语句设置指向托管变量的指针并在 statement 执行期间“钉住”该变量。如果没有 fixed 语句&#xff0c;则指向可移动托管变量的指针的作…

React Antd中样式的修改

如果需要对antd的样式进行修改&#xff0c; 进入你要修改的页面 注意&#xff1a;不能直接在自己的文件下面&#xff0c;加入一个css&#xff0c;修改这个class的样式&#xff0c;应该 加入global限定&#xff0c;global {} , 在{}里面写入 .classname {} 然后在设置css样式…

android /data/data/数据作用,android 清除data/data/ 下其他应用的数据

// 需在源码下编译// 实现。。。private ClearUserDataObserver mClearDataObserver;class ClearUserDataObserver extends IPackageDataObserver.Stub {public void onRemoveCompleted(final String packageName, final boolean succeeded) {/*final Message msg mHandler.ob…

使用Akka简化交易系统

我的同事正在开发一种交易系统&#xff0c;该系统可以处理大量的传入交易。 每笔交易都涵盖一种Instrument &#xff08;例如债券或股票&#xff09;&#xff0c;并且具有某些&#xff08;现在&#xff09;不重要的属性。 他们坚持使用Java&#xff08;<8&#xff09;&#…