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

如果我们的代码有明显的错误,我们很有动力进行改进。 但是,在某些时候,我们认为我们的代码“足够好”并继续前进。

通常,当我们认为改进现有代码的好处小于所需的工作时,就会发生这种情况。 当然,如果我们低估了投资的回报,我们可能会打错电话,这会伤害我们。

这就是发生在我身上的事情,我决定写这篇文章,以便您避免犯同样的错误。

编写“良好”单元测试

如果我们要编写“好的”单元测试,则必须编写以下单元测试:

  • 只测试一件事 。 好的单元测试只能因一个原因而失败,并且只能断言一件事。
  • 被正确命名 。 测试方法的名称必须显示测试失败的原因。
  • 模拟外部依赖关系(和状态) 。 如果单元测试失败,我们将确切知道问题出在哪里。

补充阅读:

  • 单元测试只能测试一件事情
  • 编写干净的测试:命名问题
  • 编写干净的测试:分而治之
  • 编写干净的测试:验证或不验证

如果我们编写满足这些条件的单元测试,我们将编写好的单元测试。 对?

我曾经这样认为。 现在我对此表示怀疑

善意铺平地狱之路

我从未见过决定编写糟糕的单元测试的软件开发人员。 如果开发人员正在编写单元测试,则他/她很有可能要编写好的单元测试。 但是,这并不意味着该开发人员编写的单元测试是好的。

我想编写既易于阅读又易于维护的单元测试。 我什至写了一个教程,描述了如何编写干净的测试 。 问题在于,本教程中给出的建议还不够好(尚未)。 它可以帮助我们入门,但是并没有显示出兔子洞的真正深度。

我的教程中描述的方法存在两个主要问题:

命名标准是FTW吗?

如果我们使用Roy Osherove引入的“命名标准”,则会注意到很难描述被测状态和预期行为。

当我们为简单场景编写测试时,此命名标准非常有效。 问题在于,真正的软件并不简单。 通常,我们最终使用以下两个选项之一来命名测试方法:

首先 ,如果我们尝试尽可能具体,则测试方法的方法名称会变得太过糟糕。 最后,我们必须承认我们不能像我们想要的那样具体,因为方法名称会占用太多空间。

其次 ,如果我们尝试使方法名称尽可能短,则方法名称将不会真正描述测试状态和预期行为。

选择哪个选项实际上并不重要,因为无论如何我们都会遇到以下问题:

  • 如果测试失败,则方法名称不一定描述要出错的方法。 我们可以使用自定义断言来解决此问题,但是它们不是免费的。
  • 很难对我们的测试涵盖的场景进行简要概述。

以下是我们在“ 编写干净测试”教程中编写的测试方法的名称:

  • registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException()
  • registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount()
  • registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider()
  • registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount()
  • registerNewUserAccount_SocialSignInAnquequeEmail_ShouldNotCreateEncodedPasswordForUser()

这些方法的名称不是很长,但是我们必须记住,编写这些单元测试是为了测试一种简单的注册方法。 当我使用这种命名约定为现实生活中的软件项目编写自动化测试时,最长的方法名称是我们最长的示例名称的两倍。

那不是很干净或可读。 我们可以做得更好

没有通用配置

在本教程中,我们使单元测试变得更好了 。 尽管如此,他们仍然遭受这样的事实,即没有“自然的”方式在不同的单元测试之间共享配置。

这意味着我们的单元测试包含许多重复的代码,这些代码配置了我们的模拟对象并创建了在单元测试中使用的其他对象。

另外,由于没有“自然”的方式表明某些常量仅与特定的测试方法相关,因此我们必须将所有常量添加到测试类的开头。

我们的测试类的源代码如下(突出显示有问题的代码):

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 com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
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_SocialSignInAndDuplicateEmail_ShouldThrowException() 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(new User());catchException(registrationService).registerNewUserAccount(registration);assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() 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(new User());catchException(registrationService).registerNewUserAccount(registration);verify(repository, never()).save(isA(User.class));}@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();assertThatUser(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);assertThatUser(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);}
}

一些开发人员认为看起来像上面示例的单元测试足够干净。 我理解这种情绪,因为我曾经是其中之一。 但是,这些单元测试存在三个问题:

  1. 该案的实质并没有那么清楚 。 因为每种测试方法在调用被测试方法并验证预期结果之前都会进行自我配置,所以我们的测试方法变得比必要的更长。 这意味着我们不能只看一眼随机测试方法并弄清楚它要测试什么。
  2. 编写新的单元测试很慢 。 因为每个单元测试都必须自行配置,所以向我们的测试套件中添加新的单元测试比它可能要慢得多。 另一个“意外”的缺点是,这种单元测试鼓励人们练习复制和粘贴编程 。
  3. 维持这些单元测试是一件痛苦的事情 。 如果我们向注册表单添加新的必填字段,或者更改registerNewUserAccount()方法的实现,则必须对每个单元测试进行更改。 这些单元测试太脆弱了。

换句话说,这些单元测试很难阅读,很难编写和维护。 我们必须做得更好

摘要

这篇博客文章教会了我们四件事:

  • 即使我们认为我们正在编写好的单元测试,也不一定是正确的。
  • 如果由于必须更改许多单元测试而导致更改现有功能的速度很慢,那么我们就不会编写好的单元测试。
  • 如果添加新功能的速度很慢,因为我们必须向单元测试中添加大量重复的代码,那么我们就不会编写好的单元测试。
  • 如果我们看不到单元测试所涵盖的情况,那么我们就没有编写好的单元测试。

本教程的下一部分将回答这个非常相关的问题:

如果现有的单元测试很烂,我们该如何解决?

如果要编写干净的测试,则应阅读我的“ 编写干净的测试”教程 。

翻译自: https://www.javacodegeeks.com/2015/03/writing-clean-tests-trouble-in-paradise.html

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

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

相关文章

使用Mockito时遇到的一些问题

最近在使用Mockito时遇到了几个比较tricking的问题&#xff0c;在这里记录一下。 1.如果方法的参数或者返回类型是泛型通配符相关的&#xff08;如<?>&#xff0c;<? extends XXX>&#xff09;&#xff0c;不管你定义的对象类型是否正确匹配&#xff0c;用any(ma…

c语言实现linux下的top命令来获取cpu利用率_有用的一篇笔记,linux 调优各项监控指标...

自开始负责生产环境部署&#xff0c;中间遇到了若干线上环境内存以及CPU的问题。由于微服务以及容器的流行&#xff0c;现在已经可以很方便的使用 K8s prometheus grafana alert 的方式进行监控&#xff0c;这足以覆盖大部分场景。最重要的事情已经交由最适合的组件去做&…

android config.mk,android编译分析之10—config.mk

config.mk可以说是android编译系统中关于配置环境的一个总的makefile&#xff0c;定义了编译环境的方方面面。执行完config.mk就完成了android编译系统的所有准备工作&#xff0c;即准备好了所有的编译需要的全局变量&#xff0c;下一步直接执行make&#xff0c;即可产生镜像文…

在Spring中记录JAX-WS SOAP消息

每当在Spring中使用JAX-WS时&#xff0c;您可能都希望记录传入和传出的SOAP消息-如果仅用于开发过程中的调试。 因此&#xff0c;第一件事就是增加日志级别&#xff0c;对吗&#xff1f; 不幸的是&#xff0c;这将无效。 您将要做的是利用javax.xml.ws.handler.HandlerResolver…

WordPress /wp-admin/includes/post.php user_ID 参数操作权限提升漏洞

漏洞版本: WordPress 3.6 漏洞描述: Bugtraq ID:62346 CVE ID:CVE-2013-4340WordPress是一种使用PHP语言开发的博客平台&#xff0c;用户可以在支持PHP和MySQL数据库的服务器上架设自己的网志WordPress wp-admin/includes/post.php脚本在处理user_ID参数时存在一个安全漏洞&…

rip协议中周期性广播路由信息的报文_技术实操||距离矢量路由协议-RIP

距离矢量路由协议—RIP01距离矢量路由协议概述路由信息协议RIP(RoutingInformation Protocol)的简称&#xff0c;它是一种基于距离矢量(Distance-Vector)算法的协议&#xff0c;使用跳数作为度量来衡量到达目的网络的距离。RIP主要应用于规模较小的网络中。RIP是一种比较简单的…

android中可以有两个焦点吗,android – 如何通过焦点在屏幕上调整两个片段的大小?...

我有三个片段,前两个填充80&#xff05;的屏幕,最后一个填充其余的片段(这个片段永远不会改变大小).我希望,在用户(焦点)输入片段后,调整片段的大小,使其填满屏幕的70&#xff05;(将10&#xff05;留给另一个).像这样&#xff1a;可以通过动态改变碎片的重量吗&#xff1f;或者…

使用tinylog 1.0简化您的日志记录

tinylog的大小仅为75 KB&#xff0c;是广泛使用的经典日志记录框架Log4j和Logback的轻型替代方案。 经过三年的开发&#xff0c;最终版本1.0刚刚于 3月底发布 。 在几个设计问题中&#xff0c;tinylog采取了与Java中经典日志记录框架完全不同的方法。 本文将介绍与Log4j和Logba…

MySQL : 报错:1130-host ... is not allowed to connect to this MySql server 开放mysql远程连接 不使用localhost...

MySQL : 报错:1130-host ... is not allowed to connect to this MySql server 开放mysql远程连接 不使用localhost 摘自: http://www.cnblogs.com/xyzdw/archive/2011/08/11/2135227.html 报错:1130-host ... is not allowed to connect to this MySql server 解决方法&#x…

华谊兄弟出现什么问题_什么是语言训练?这就要从语言问题的出现说起了

开口、发音是每一个孩子在语言发展过程中所不可少的经历&#xff0c;他们从周围环境中获取到的各种信息转化为想要表达的内容。虽然一开始孩子的语言并没有成年人那么流畅和准确&#xff0c;但随着时间的推移&#xff0c;他们的说话发音愈发成熟。只是&#xff0c;并不是所有孩…

【算法提升—力扣每日一刷】五日总结【12/18--12/22】

文章目录 2023/12/18LeetCode每日一刷&#xff1a;[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) 2023/12/19LeetCode每日一刷&#xff1a;[150. 逆波兰表达式求值&#xff08;后缀表达式运算&#xff09;](https://leetcode.cn/problems/evaluate-rever…

CompletableFuture不能被打断

我已经写了很多有关InterruptedException和中断线程的文章 。 简而言之&#xff0c;如果您没有Future.cancel()调用Future.cancel()那么Future将终止待处理的get() &#xff0c;但还将尝试中断基础线程。 这是一个非常重要的功能&#xff0c;可以更好地利用线程池。 我还写信总…

Nginx 禁止部分 User-Agrent 访问

经常有一些垃圾爬虫&#xff0c;完全不管你受得了受不了&#xff0c;完全不管你的 robots.txt &#xff0c;拼命的爬你的网站&#xff0c;用下面方面可以帮这帮垃圾干掉。 location / { root /home/www/; if ($http_user_agent ~* "qihoobot") { retu…

crio电压采集 labview_NI cDAQ917采集温度方法

NI cDAQ-9171是一款由总线供电的单槽NI CompactDAQ USB机箱&#xff0c;适合较小的便携式传感器测量系统。cDAQ-9171可与50多款专用测量模块配合使用&#xff0c;用于构建模拟输出、数字I/O或计数器/定时器测量系统。这些模块适用于各种传感器测量&#xff0c;包括热电偶、RTD、…

JPA 2.1如何成为新的EJB 2.0

美丽在于情人眼中。 “轻松”也是如此&#xff1a; 了解有关SQL结果集映射的更多信息&#xff0c;并轻松处理本机查询结果&#xff1a; http : //t.co/WH4BTlClIP #JPA #Java #JavaEE — Thorben Janssen&#xff08; thjanssen123&#xff09; 2015年4月15日 Thorben撰写了…

常见问题摘要(生活篇)

1 lingoes软件安装了&#xff0c;却没发现怎么取词&#xff1f; 答&#xff1a;在软件设置里面&#xff0c;修改取词模式&#xff0c;一般以鼠标左键按下ctrl 为标准操作。转载于:https://www.cnblogs.com/lastshadow/p/3330146.html

android finish 区别,Android Activity类finish、onDestory和System.exit介绍

image.pngfinish函数Activity.finish()Call this when your activity is done and should be closed.在你的activity动作完成的时候&#xff0c;或者Activity需要关闭的时候&#xff0c;调用此方法。当调用此方法的时候&#xff0c;系统只是将最上面的Activity移出了栈&#xf…

github哪些协议能商用_GitHub 上有哪些一般人也可以用的项目?

​之前有不少粉丝来信询问&#xff1a;能否给初步接触 GitHub 的用户&#xff0c;推荐几个比较实用、容易看懂&#xff0c;无需具备太多专业知识便能上手使用的项目呢&#xff1f;答案是&#xff1a;有的。今天&#xff0c;我们就上述问题&#xff0c;来聊聊 GitHub 上有哪几个…

在JVM中记录世界停顿

不同的事件可能导致JVM暂停所有应用程序线程。 这种暂停称为世界停止&#xff08;STW&#xff09;暂停。 触发STW暂停的最常见原因是垃圾回收&#xff08; 例如github中的示例 &#xff09;&#xff0c;但是不同的JIT操作&#xff08; 示例 &#xff09;&#xff0c;偏向锁吊销…

目睹鸿蒙开创四大至高位面,吞噬星空 绝非鸿蒙系列,完结前最后的分析【申精】...

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼先说说空间的划分&#xff1a;第一要说的是空间等级的划分&#xff0c;盘龙里分的很清楚&#xff0c;物质位面&#xff0c;七大神位面&#xff0c;四大至高位面&#xff0c;说得很清楚&#xff0c;物质位面虽然是最低等的位面&…