七大罪过与如何避免

在整个本文中,我将在代码片段中使用Java,同时还将使用JUnit和Mockito 。

本文旨在提供示例测试代码,这些示例可以是:

  • 难以阅读
  • 难以维护

在这些示例之后,本文将尝试提供替代方法,这些替代方法可用于增强测试的可读性,从而有助于使其在将来更易于维护。

创建良好的示例具有挑战性,因此,作为读者,我鼓励您将示例仅用作了解本文基本信息的工具,以力求实现可读的测试代码。

1.通用测试名称

您可能已经看到了如下命名的测试

@Test
void testTranslator() {String word = new Translator().wordFrom(1);assertThat(word, is("one"));
}

现在这是非常通用的,不会通知代码的读者测试实际上正在测试什么。 Translator可能有多种方法,我们如何知道测试中正在使用哪种方法? 通过查看测试名称并不清楚,这意味着我们必须查看测试本身才能看到。

我们可以做得更好,因此可以看到以下内容:

@Test
void translate_from_number_to_word() {String word = new Translator().wordFrom(1);assertThat(word, is("one"));
}

从上面我们可以看到,它在解释该测试实际上在做什么方面做得更好。 此外,如果你的名字你的测试文件类似TranslatorShould你可以当你把测试文件和单个测试名称形成在你心目中是合理的一句话: Translator should translate from number to word

2.测试设置中的变异

在测试中很有可能会希望将测试中使用的对象构造为处于特定状态。 有不同的方法,下面显示了一种这样的方法。 在此代码段中,我们基于该对象中包含的信息来确定某个字符是否实际上是“ Luke Skywalker”(想象这就是isLuke()方法的作用):

@Test
void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character luke = new Character();luke.setName("Luke Skywalker");Character vader = new Character();vader.setName("Darth Vader");luke.setFather(vader);luke.setProfession(PROFESSION.JEDI);boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke);
}

上面构造了一个Character对象来表示“ Luke Skywalker”,此后发生的事涉及相当比例的突变。 它继续在随后的行中设置名称,父母身份和职业。 当然,这忽略了与我们的朋友“达斯·维达”发生的类似事情。

这种突变水平分散了测试中正在发生的事情。 如果我们再回顾一下我先前的句子:

在测试中很有可能您希望将测试中使用的对象构造为处于特定状态

但是,上述测试实际上发生了两个阶段:

  • 构造对象
  • 使其处于某种状态

这是不必要的,我们可以避免。 可能有人建议,为了避免发生突变,我们可以简单地将所有内容都移植并转储到构造函数中,以确保我们以给定状态构造对象,从而避免发生突变:

@Test
void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character vader = new Character("Darth Vader");Character luke = new Character("Luke Skywalker", vader, PROFESSION.JEDI);boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke);
}

从上面我们可以看到,我们减少了代码行的数量以及对象的变异。 但是,在此过程中,我们已经失去了Character (现在为Character参数)在测试中表示的含义。 为了使isLuke()方法返回true,我们传入的Character对象必须具有以下内容:

  • “卢克·天行者”的名字
  • 有一个父亲叫“达斯·维达”
  • 成为绝地武士

但是,从这种情况的测试中还不清楚,我们必须检查Character的内部以了解这些参数的用途(或者您的IDE会告诉您)。

我们可以做的更好,我们可以利用Builder模式在所需状态下构造一个Character对象,同时还可以保持测试的可读性:

@Test
void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character luke = CharacterBuilder().aCharacter().withNameOf("Luke Skywalker").sonOf(new Character("Darth Vader")).employedAsA(PROFESSION.JEDI).build();boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke);
}

通过上面的内容,可能还会有几行内容,但是它试图解释测试中的重要内容。

3.断言疯狂

在测试期间,您将断言/验证系统中是否发生了某些事情(通常位于每次测试结束时)。 这是测试中非常重要的一步,可能很想添加许多断言,例如断言返回的对象的值。

@Test
void successfully_upgrades_user() {UserService service = new UserService();User someBasicUser = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.BASIC).build();User upgradedUser = service.upgrade(someBasicUser);assertThat(upgradedUser.name(), is("Basic Bob"));assertThat(upgradedUser.type(), is(UserType.SUPER_USER));assertThat(upgradedUser.age(), is(23));
}

(在上面的示例中,我向构建器提供了其他信息,例如名称和年龄,但是如果对测试不重要,通常不会包含此信息,请在构建器中使用明智的默认值)

如我们所见,存在三个断言,在更极端的示例中,我们谈论的是数十行断言。 我们不一定需要执行三个断言,有时我们可以合而为一:

@Test
void successfully_upgrades_user() {UserService service = new UserService();User someBasicUser = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.BASIC).build();User expectedUserAfterUpgrading = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.SUPER_USER).build();User upgradedUser = service.upgrade(someBasicUser);assertThat(upgradedUser, is(expectedUserAfterUpgrading));
}

现在,我们将升级后的用户与我们希望对象在升级后的外观进行比较。 为此,您将需要比较的对象( User )具有覆盖的equalshashCode

4.神奇的价值观

您是否曾经看过数字或字符串并想知道它代表什么? 我已经过了,那些不得不解析代码行的宝贵时间可以很快加起来。 我们在下面有这样的代码示例。

@Test
void denies_entry_for_someone_who_is_not_old_enough() {Person youngPerson = PersonBuilder.aPerson().withAgeOf(17).build();NightclubService service = new NightclubService(21);String decision = service.entryDecisionFor(youngPerson);assertThat(decision, is("No entry. They are not old enough."));
}

阅读以上内容,您可能会遇到一些问题,例如:

  • 17是什么意思?
  • 21在构造函数中是什么意思?

如果我们可以向代码的读者表示它们的含义,那不是很好,那么他们不必考虑太多吗? 幸运的是,我们可以:

private static final int SEVENTEEN_YEARS = 17;
private static final int MINIMUM_AGE_FOR_ENTRY = 21;
private static final String NO_ENTRY_MESSAGE = "No entry. They are not old enough.";@Test
void denies_entry_for_someone_who_is_not_old_enough() {Person youngPerson = PersonBuilder.aPerson().withAgeOf(SEVENTEEN_YEARS).build();NightclubService service = new NightclubService(MINIMUM_AGE_FOR_ENTRY);String decision = service.entryDecisionFor(youngPerson);assertThat(decision, is(NO_ENTRY_MESSAGE));
}

现在,当我们看以上内容时,我们知道:

  • SEVENTEEN_YEARS是用来表示17年的值,毫无疑问,我们已经在读者的脑海中留下了疑问。 不是秒或分钟,而是年。
  • MINIMUM_AGE_FOR_ENTRY是必须允许某人进入夜总会的值。 读者甚至不必关心该值是什么,而只是了解测试背景下的含义。
  • NO_ENTRY_MESSAGE是返回的值,表示不允许某人进入夜总会。 从本质上讲,字符串通常具有更好的描述性,但是请始终检查代码以找出可以改进的地方。

这里的关键是减少代码阅读器尝试解析代码行所花费的时间。

5.难以阅读的测试名称

@Test
void testingNumberOneAndNumberTwoCanBeAddedTogetherToProduceNumberThree() {...
}

您花了多长时间阅读以上内容? 它易于阅读吗?您能快速了解一下此处正在测试的内容吗?还是需要解析许多字符?

幸运的是,我们可以尝试以更好的方式命名测试,方法是将测试减少到实际测试的水平,并删除试图添加的华夫饼:

@Test
void twoNumbersCanBeAdded() {...
}

它的阅读效果更好吗? 我们减少了这里的单词数量,更易于解析。 如果我们可以更进一步,问我们是否可以放弃使用骆驼箱怎么办:

@Test
void two_numbers_can_be_added() {...
}

这是一个优先事项,应该由对给定代码库做出贡献的人员同意。 使用蛇形小写字母(如上所述)可以帮助提高测试名称的可读性,因为您很可能打算模仿书面句子。 因此,蛇形格的使用紧随普通书面句子中存在的物理空间。 但是,Java不允许在方法名称中使用空格,这是我们所拥有的最好的方法,缺少使用Spock之类的东西。

6.依赖注入的设置器

通常,对于测试,您希望能够为给定对象(也称为“协作对象”或简称为“协作者”)注入依赖关系。 为了达到这个目的,您可能已经看到了类似以下内容的内容:

@Test
void save_a_product() {ProductService service = new ProductService();TestableProductRepository repository = mock(TestableProductRepository.class);service.setRepository(repository);Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct);
}

上面使用了setter方法,即setRepository() ,以便注入TestableProductRepository的模拟,因此我们可以验证服务和存储库之间是否发生了正确的协作。

类似于围绕突变的观点,这里我们对ProductService进行突变,而不是将对象构造为所需的状态。 可以通过将协作者注入构造函数中来避免这种情况:

@Test
void save_a_product() {TestableProductRepository repository = mock(TestableProductRepository.class);ProductService service = new ProductService(repository);Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct);
}

因此,现在我们将协作者注入了构造函数中,现在我们在构造时就知道对象将处于什么状态。但是,您可能会问“在此过程中我们是否没有丢失某些上下文?”。

我们已经从

service.setRepository(repository);

ProductService service = new ProductService(repository);

前者更具描述性。 因此,如果您不喜欢这种上下文丢失的情况,则可以选择类似构建器的内容,并创建以下内容:

@Test
void save_a_product() {TestableProductRepository repository = mock(TestableProductRepository.class);ProductService service = ProductServiceBuilder.aProductService().withRepository(repository).build();Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct);
}

该解决方案使我们能够避免在通过withRepository()方法记录协作者注入的情况下改变ProductService

7.非描述性验证

如前所述,您的测试通常会包含验证语句。 不用自己动手,您通常会利用库来执行此操作。 但是,您必须注意不要掩盖验证的意图。 要了解我在说什么,请看以下示例。

@Test
void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verifyZeroInteractions(component);
}

现在,如果您看上面的内容,您是否立即知道该断言表明没有错误显示给用户? 可能是因为它是测试的名称,但是您可能不将该代码行与测试名称相关联 。 这是因为它是Mockito的代码,并且通用以适应许多不同的用例。 它按照它说的做,检查与UIComponent的模拟是否没有交互。

但是,这意味着您的测试有所不同。 我们如何努力使其更加清晰。

@Test
void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verify(component, times(0)).addErrorMessage("Invalid user");
}

这样会更好一些,因为此代码的读者有很大的潜力可以快速了解此行的工作。 但是,在某些情况下,可能仍然很难阅读。 在这种情况下,请按照以下说明提取一种方法,以更好地解释您的验证。

@Test
void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verifyNoErrorMessageIsAddedTo(component);
}private void verifyNoErrorMessageIsAddedTo(UIComponent component) {verify(component, times(0)).addErrorMessage("Invalid user");
}

上面的代码并不完美,但是在当前测试的范围内,它肯定可以提供我们正在验证的内容的高层次概述。

结束语

我希望您喜欢这篇文章,下次您完成编写测试时将花费一到两个重构步骤。 在下一次之前,我给你以下报价:

“必须编写程序供人们阅读,并且只能偶然地使机器执行。” ― Harold Abelson,计算机程序的结构和解释

翻译自: https://www.javacodegeeks.com/2019/08/seven-testing-sins-and-how-to-avoid-them.html

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

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

相关文章

4 指针运算_C++用指针访问数组元素(学习笔记:第6章 08)

用指针访问数组元素[1]数组是一组连续存储的同类型数据,可以通过指针的算术运算,使指针依次指向数组的各个元素,进而可以遍历数组。定义指向数组元素的指针定义与赋值例:int a[10], *pa; pa&a[0]; 或 paa;等效的形式经过上述定…

asyncexec_如何安全使用SWT的显示器asyncExec

asyncexec大多数用户界面(UI)工具箱都是单线程的, SWT也不例外。 这意味着必须仅从单个线程(即所谓的UI线程)访问UI对象。 另一方面,应在后台线程中执行长时间运行的任务,以使UI保持响应。 这使…

属性面板 脚本_3.1 创建和使用脚本

在unity中,游戏物体的行为是通过组件来驱动的,我们可以通过内建的组件来给我们的游戏物体组合各种能力,尽管如此,要知道我们的需求永远是动态的,很快我们就会发现,内建的组件功能已经无法满足我们的需求&am…

新的JMetro JavaFX 11兼容版本

你好,我们又见面了! 这次是一个新版本,该版本与JavaFX 11兼容。 继续阅读以获取详细信息。 JMetro 8.5.7和11.5.7版本 JMetro代码已分为2个分支。 master分支具有Java 8兼容的JMetro版本,“ 11”分支具有Java 11兼容的版本。 以…

vue加跨域代理静态文件404_解决vue本地环境跨域请求正常,版本打包后跨域代理不起作用,请求不到数据的方法——针对vue2.0...

问题:在本地使用了proxyTable代理可以正常跨域请求后台数据,打包上传后就无法获得后台的json文件。查看了相关资料可以用nginx进行解决。还可以使用命名环境变量,请求的时候进行判断,话不多说上干货module.exports merge(prodEnv…

tomee_使用Vysper,TomEE和PrimeFaces将XMPP服务器嵌入JSF Web应用程序内部

tomee我有一个需要在完成某些工作时通知用户的应用程序。 它使用JSF和Primefaces,因此可以使用大气 (也称为Push)来实现这种通知。 但是另一个有趣的方法是使用嵌入在Java Web应用程序中的XMPP服务器。 好的,好的,您不…

板框导入_板框结构导入有问题?这几个问题最常见,附解决方法!

对于一些比较复杂的结构,Altium的处理能力有限,通常采用AutoCAD来进行设计,然后在Altium中执行菜单栏中“文件”→“导入”→DWG/DXF命令,选择需要导入的DXF文件即可。如果导入过程中出现了乱码,报错等问题要如何解决呢…

您如何使用硒来计算自动化测试的投资回报率?

跨浏览器测试是一种测试,需要大量的精力和时间。 通过不同的浏览器,操作系统,设备,屏幕分辨率测试Web应用程序,以评估针对各种受众的Web内容呈现的过程是一项活动。 特别是如果手动处理。 使用Selenium进行的自动跨浏览…

流量复制_详解Linux系统流量复制--gor、tcpcopy、nginx模块流量复制等

概述对于一些有并发要求的业务,特别是对接外部流量时,产品上线前一定要做的就是压力测试,但是常规的压力测试并不能覆盖所有情况。以gemeter、ab,、webbench、http_load为例,这些通过模拟请求的压测工具,只能发送特定的…

mongodb+java_Java EE + MongoDb与Apache TomEE和Jongo Starter项目

mongodbjava知道MongoDB和Java EE ,但是您不确切地知道如何将它们集成在一起? 您是否阅读了很多有关该主题的内容,但没有找到适合该目的的解决方案? 这个入门项目适合您: 您将学习如何以一种时尚的方式使用MongoDB和J…

C语言天才!想法奇异?还是逼格满满?一份国外C语言写的传奇简历

C语言天才!想法奇异?还是逼格满满?一份国外C语言写的传奇简历作者用代码更新了自己的简历,是不是很接地气,特符合程序员的逼格。这是一份可读可执行的语言源文件,也是作者编码风格的体现。C语言源码&#x…

hash值 更改git_Git切换版本

Git切换版本有三种方式:1.基于哈希值切换》基于哈希值切换(推荐),命令:git reset --hard 哈希值,哈希值从哪来,git reflog查看下就知道了,切换版本后,git reflog会发现有两个HEAD,别…

devc++ 文件未编译问题

点击 文件 然后点击关闭全部文件, 重新打开一下软件件就好了

候选JEP:记录和密封类型

马克赖因霍尔德(Mark Reinhold )本周在OpenJDK琥珀色开发者邮件列表上宣布了两个新的紧密相关的候选 JDK增强提案( JEP) ,其帖子分别为“ 新候选JEP:359:记录(预览) ”和…

fedora mysql_Fedora server 安装Mysql8

导读MySQL是一种关系数据库管理系统(RDBMS),作为服务器运行,提供对多个数据库的多用户访问。 这是指导,如何在Fedora 28/27/26,CentOS 7.5 / 6.10和Red Hat(RHEL)7.5 / 6.10上安装或升级MySQL社区服务器最新版本8.0(8.0.12)/5.7(5…

lombok 生成代码_使用Project Lombok减少Java应用程序中的样板代码

lombok 生成代码对Java编程语言最常提出的批评之一是它需要大量的样板代码 。 对于简单的类尤其如此,该类只需要存储一些值就可以。 您需要这些值的getter和setter方法,也许您还需要一个构造函数,覆盖equals()和 hash…

让C/C++程序员告诉你什么叫浪漫,表白黑科技,炫酷多彩求爱利器

前言缘是美丽的邂逅,爱是心跳的感觉,情是心灵的交会,恋是甜蜜的思念,走在爱与被爱的边缘,你见或者不见,爱你的心始终不改变!C语言诠释爱——为TA写下心中情,生成程序传给TA&#xff…

mysql 支持json_MySQL 5.7 对 JSON 的支持

最近有个业务需要能够存储Json并做一些简单的业务逻辑处理。业务找到我说json的数据分析很难用mysql 5.6,这样的纯粹行存来处理难度很大,问我有没啥办法。我第一想到的是mongodb,第二想到的就是mysql 5.7 。 然后一查,哎呀,已经GA…

Java十六进制浮点文字

我如何遇到十六进制浮点数 我正在Java :: Geci中开发一种新功能,以减少代码重新格式化的可能性。 如果重新格式化,当前版本的代码将覆盖原本相同的代码。 这很烦人,因为按下重新格式化键的快捷键相当容易,而且许多项目甚至要求开发…

mysql内部损坏_mysql表损坏故障案例

开发人员反映猎豹有个功能报500错误,让我查一下服务器上有没有做过什么调整,额,不会吧,今天元旦啊,谁会闲的蛋疼去调试服务器啊,最后他们查到了和一个表有关的sql都执行不了,那肯定是这个表损坏…