编写数据访问代码测试–单元测试是浪费

几年前,我是为我的数据访问代码编写单元测试的那些开发人员之一。 我正在孤立地测试所有内容,我对自己感到非常满意。 老实说,我认为自己做得很好。 哦,男孩,我错了! 这篇博客文章描述了为什么我们不应该为数据访问代码编写单元测试,并解释为什么我们应该用集成测试代替单元测试。 让我们开始吧。

单元测试错误问题的答案

我们为数据访问代码编写测试,因为我们想知道它可以按预期工作。 换句话说,我们想找到这些问题的答案:

  1. 是否将正确的数据存储到使用的数据库?
  2. 我们的数据库查询是否返回正确的数据?

单元测试可以帮助我们找到想要的答案吗? 好吧, 单元测试的最基本规则之一是单元测试不应使用诸如数据库之类的外部系统 。 此规则不适用于当前情况,因为存储正确信息和返回正确查询结果的责任由我们的数据访问代码和使用的数据库划分。 例如,当我们的应用程序执行单个数据库查询时,职责划分如下:

  • 负责创建执行的数据库查询的数据访问代码。
  • 数据库负责执行数据库查询,并将查询结果返回给数据访问代码。

问题是,如果我们将数据访问代码与数据库隔离,则可以测试数据访问代码是否创建了“正确的”查询,但是我们无法确保所创建的查询返回正确的查询结果。 这就是为什么单元测试不能帮助我们找到想要的答案

告诫故事:假装是问题的一部分

有段时间我为数据访问代码编写了单元测试。 当时我有两个规则:

  1. 每段代码都必须单独进行测试。
  2. 让我们使用模拟。

我当时在一个使用Spring Data JPA的项目中工作,而动态查询是通过使用JPA条件查询构建的。 如果您不熟悉Spring Data JPA,则可能需要阅读Spring Data JPA教程的第四部分,该教程介绍了如何使用Spring Data JPA创建JPA条件查询 。 无论如何,我创建了一个规范构建器类来构建Specification <Person>对象。 创建Specification <Person>对象后,将其转发给我的Spring Data JPA存储库,该存储库执行查询并返回查询结果。 规范构建器类的源代码如下所示:

import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;public class PersonSpecifications {public static Specification<Person> lastNameIsLike(final String searchTerm) {return new Specification<Person>() {@Overridepublic Predicate toPredicate(Root<Person> personRoot, CriteriaQuery<?> query, CriteriaBuilder cb) {String likePattern = getLikePattern(searchTerm);              return cb.like(cb.lower(personRoot.<String>get(Person_.lastName)), likePattern);}private String getLikePattern(final String searchTerm) {return searchTerm.toLowerCase() + "%";}};}
}

让我们看一下“验证”规范构建器类创建“正确”查询的测试代码。 请记住,我是按照自己的规则编写此测试类的,这意味着结果应该很棒。 PersonSpecificationsTest类的源代码如下所示:

import org.junit.Before;
import org.junit.Test;
import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.*;import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.*;public class PersonSpecificationsTest {private static final String SEARCH_TERM = "Foo";private static final String SEARCH_TERM_LIKE_PATTERN = "foo%";private CriteriaBuilder criteriaBuilderMock;private CriteriaQuery criteriaQueryMock;private Root<Person> personRootMock;@Beforepublic void setUp() {criteriaBuilderMock = mock(CriteriaBuilder.class);criteriaQueryMock = mock(CriteriaQuery.class);personRootMock = mock(Root.class);}@Testpublic void lastNameIsLike() {Path lastNamePathMock = mock(Path.class);       when(personRootMock.get(Person_.lastName)).thenReturn(lastNamePathMock);Expression lastNameToLowerExpressionMock = mock(Expression.class);when(criteriaBuilderMock.lower(lastNamePathMock)).thenReturn(lastNameToLowerExpressionMock);Predicate lastNameIsLikePredicateMock = mock(Predicate.class);when(criteriaBuilderMock.like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN)).thenReturn(lastNameIsLikePredicateMock);Specification<Person> actual = PersonSpecifications.lastNameIsLike(SEARCH_TERM);Predicate actualPredicate = actual.toPredicate(personRootMock, criteriaQueryMock, criteriaBuilderMock);verify(personRootMock, times(1)).get(Person_.lastName);verifyNoMoreInteractions(personRootMock);verify(criteriaBuilderMock, times(1)).lower(lastNamePathMock);verify(criteriaBuilderMock, times(1)).like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN);verifyNoMoreInteractions(criteriaBuilderMock);verifyZeroInteractions(criteriaQueryMock, lastNamePathMock, lastNameIsLikePredicateMock);assertEquals(lastNameIsLikePredicateMock, actualPredicate);}
}

这有意义吗? 没有! 我必须承认,此测试对任何人都没有价值,应该尽快删除。 该测试存在三个主要问题:

  • 它不能帮助我们确保数据库查询返回正确的结果。
  • 很难理解并使情况更糟,它描述了查询的构建方式,但没有描述查询应返回的内容。
  • 这样的测试很难编写和维护。

事实是,此单元测试是不应编写的测试的教科书示例。 它对我们没有任何价值,但我们仍然必须维护它。 因此, 这是浪费! 但是,如果我们为数据访问代码编写单元测试,就会发生这种情况。 我们最终得到了一个测试套件,无法测试正确的东西。

数据访问测试正确完成

我是单元测试的忠实拥护者,但是在某些情况下,它并不是工作的最佳工具。 这是其中一种情况。 数据访问代码与使用的数据存储有非常密切的关系。 这种关系是如此紧密,以至于没有数据存储,数据访问代码本身就没有用。 这就是为什么将我们的数据访问代码与使用的数据存储区分开来是没有意义的。 解决这个问题很简单。 如果我们要为数据访问代码编写全面的测试,则必须将数据访问代码与使用的数据存储一起进行测试。 这意味着我们必须忘记单元测试并开始编写集成测试 。 我们必须了解,只有集成测试才能验证

  • 我们的数据访问代码创建正确的数据库查询。
  • 我们的数据库返回正确的查询结果。

如果您想知道如何编写针对Spring支持的存储库的集成测试,则应阅读我的博客文章“ Spring Data JPA教程:集成测试” 。 它描述了如何为Spring Data JPA存储库编写集成测试。 但是,在为使用关系数据库的任何存储库编写集成测试时,可以使用相同的技术。 例如, 为测试“ 将jOOQ与Spring结合使用”教程中的示例应用程序而编写的集成测试使用该博客文章中描述的技术。

摘要

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

  • 我们了解到,单元测试无法帮助我们验证数据访问代码是否正常运行,因为我们无法确保将正确的数据插入到数据存储中或查询返回正确的结果。
  • 我们了解到,应该使用集成测试来测试数据访问代码,因为数据访问代码与使用的数据存储之间的关系是如此紧密,以至于没有必要将它们分开。

只剩下一个问题:您是否还在为数据访问代码编写单元测试?

翻译自: https://www.javacodegeeks.com/2014/07/writing-tests-for-data-access-code-unit-tests-are-waste.html

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

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

相关文章

[JSON].typeOf( keyPath )

语法&#xff1a;[JSON].typeOf( keyPath ) 返回&#xff1a;[String | Number | Boolean | Json | Array | Function | 空字符] 说明&#xff1a;获取指定键名值的类型 示例&#xff1a; Set jsonObj toJson("{a: test, b: 1, c:true, d:[1,2,3,4], e:{a1:2}}"…

简单电商购物程序

sum0i1""shuruinput("请输入“手机”或“电脑”&#xff1a;")if shuru"手机": while True: sp{"iphoneX"7998,"华为P30"6998} print(sp) ainput("输入Buy进入结算,继续购买请输入物品名称&#xff1a;") if i&qu…

ie 浏览器布局中的 offset

出现原因&#xff1a; 此处的offset的值表示的是盒子模型经过计算后的实际偏移量&#xff0c;通常是margin及定位偏移量之和&#xff08;flex布局导致的偏移也会计算在内&#xff09;。在此处也无需消除。 解决办法&#xff1a; 父元素设置宽高。设置margin为负数&#xff0…

【Set jsonObj = toJson( jsonString )】创建JSON实例

创建JSON实例&#xff1a; 原型: toJson( jsonString ) 说明: 创建JSON实例 返回: [JSON] 参数:jsonString [可选] 可以用json格式字符串创建实例 示例&#xff1a; <% 方法一&#xff1a;创建一个空的JSON实例 Set jsonObj1 toJson() 方法二&#xff1a;用JSON字符串创建…

当我们的代码遇到问题的时候....;要想不遇到问题,写代码的时候要.....

当我们的代码遇到问题的时候&#xff1a;1&#xff0c;不要怨天怨地。出了问题&#xff0c;当然有可能是系统的bug&#xff0c;API的问题&#xff0c;但是那些几率往往比你犯低级错误的几率要低多了&#xff0c;先从自己身上找原因&#xff0c;是不是自己写错了。   2&#x…

为什么我不信任通配符,以及为什么我们仍然需要通配符

在将子类型多态性&#xff08;面向对象&#xff09;与参数多态性&#xff08;泛型&#xff09;相结合的任何编程语言中&#xff0c;都会出现方差问题。 假设我有一个字符串列表&#xff0c;键入List<String> 。 我可以将其传递给接受List<Object>的函数吗&#xff…

MySQL集群(PXC)入门

一、学习动机 伴随互联网行业的兴起&#xff0c;越来越多的领域需要相应的技术方案&#xff0c;比如&#xff1a;打出软件、电商平台、直播平台、电子支付、媒体社交。 身边常见的&#xff0c;校园出成绩那一年&#xff0c;我们会感觉网站异常的卡顿&#xff0c;因为访问人数太…

滚动条那些事

一、滚动条样式 1. ie8浏览器 名称描述scrollbar-arrow-color三角箭头的颜色scrollbar-face-color立体滚动条的颜色&#xff08;包括箭头部分的背景色&#xff09;scrollbar-3dlight-color立体滚动条亮边的颜色scrollbar-highlight-color滚动条的高亮颜色&#xff08;左阴影&…

【自定义组件】如何引用自定义组件

1. 可以在APP.JSON内引用自定义组件&#xff0c;此时该组件为所有页面共享。 2. 可以在页面的JSON文件内引用自定义组件&#xff0c;此时为该页面独享。 引入代码如下&#xff1a; /** * myTag 自定义组件名称 * path/to/the/custom/component 自定义组件所在路径 **/ {"…

Javascript高级程序设计第二版第十四章--异常--笔记

chaepter 14 错误异常分享。 其实主要是就是try{}catch(error){} finally{}这个语句的理解。主要一点&#xff1a;finally 在 return 之后 运行。这跟java里边的如出一辙。 比如&#xff1a;try{return1;}catch(error){return2;} finally{return0;}返回 return 0;然后接着就是 …

Java并发教程–原子性和竞争条件

原子性是多线程程序中的关键概念之一。 我们说一组动作是原子的&#xff0c;如果它们都以不可分割的方式作为一个单一的操作执行。 认为多线程程序中的一组操作将被串行执行是理所当然的&#xff0c;可能会导致错误的结果。 原因是由于线程干扰&#xff0c;这意味着如果两个线程…

sqlite3 数据库(一)

SQLite 数据库&#xff0c;是一个非常轻量级自包含(lightweight and self-contained)的DBMS&#xff0c;它可移植性好&#xff0c;很容易使用&#xff0c;很小&#xff0c;高效而且可靠。 SQLite嵌入到使用它的应用程序中&#xff0c;它们共用相同的进程空间&#xff0c;而不是…

HTML引入vue.js,在ie浏览器中不显示

因为只有两个页面&#xff0c;所以我没有用 vue-cli 搭框架&#xff0c;直接在 HTML 中引入vue.js 文件。发现其他浏览器都能正常显示&#xff0c;ie 下显示不正常&#xff0c;而且还报错&#xff0c;错误信息如下&#xff1a; 原因&#xff1a; 主要是因为 ie 不支持 ES6 的语…

【button】 按钮组件说明

原型&#xff1a; <buttonsize"[default | mini]"type"[primary | default | warn]"plain"[Boolean]"disabled"[Boolean]"loading"[Boolean]"form-type"[submit | reset]"open-type"[contact | share | g…

具有Infinispan的聚集幂等消费者模式

我创建了一个小项目 &#xff0c;该项目展示了如何将JBoss Infinispan与Apache Camel和幂等消费者模式一起使用&#xff0c;以确保消息不会在集群环境中被处理两次。 假设您有一个应用程序&#xff0c;该应用程序必须通过将其部署在多个容器上才能轻松扩展。 但是应用程序必须…

UVa OJ 128 - Software CRC (软件CRC)

Time limit: 3.000 seconds限时&#xff1a;3.000秒 Problem问题 You work for a company which uses lots of personal computers. Your boss, Dr Penny Pincher, has wanted to link the computers together for some time but has been unwilling to spend any money on the…

ipv4编址

IPv4的编址&#xff1a; IPv4的地址有32位&#xff0c;通过使用点分十进制法&#xff0c;将其划分成4个由“.”隔断的部分&#xff0c;每一个部分的取值是0~255 {2^0~(2^8)-1} IP地址是32位类似这样的二进制串&#xff1a;1100 0000 1111 1111 1111 1111 1111 1110&#xff08;…

基于 vue 的验证码组件

登录页面有个验证码&#xff0c;暂时没用到后台&#xff0c;在网上找了两个博客&#xff0c;记录一下。 一、直接写&#xff08;参考-UIEngineer&#xff09; 这个样式比较简单&#xff0c;直接在需要验证码的地方添加就行了。如果这个页面比较复杂&#xff0c;用组件会比较好…

Java 8 Friday:更多功能关系转换

过去&#xff0c;我们一直在每个星期五为您提供有关Java 8的新内容的新文章。这是一个非常令人兴奋的博客系列 &#xff0c;但我们想再次将重点放在Java和SQL的核心内容上。 我们仍然偶尔会写关于Java 8的博客&#xff0c;但不再是每个星期五&#xff08;有些人已经注意到&…

【WXS全局对象】Date

属性&#xff1a; 名称说明Date.parse( [dateString] )解析一个日期时间字符串&#xff0c;并返回 1970/1/1 午夜距离该日期时间的毫秒数。Date.UTC(year,month,day,hours,minutes,seconds,ms) 根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数。 参数&#xff1a;year/m…