Spring陷阱:事务测试被认为是有害的

Spring杀手级功能之一是容器内集成测试 。 尽管EJB多年来一直缺乏此功能(Java EE 6终于解决了这个问题,但是我还没测试过),但是Spring从一开始就允许您从Web层开始,通过所有服务来测试整个堆栈。到数据库的方式。

数据库是有问题的部分。 首先,您需要使用内存中的独立数据库(例如H2)将测试与外部数据库分离。 Spring在很大程度上帮助了这一点,尤其是现在有了配置文件和嵌入式数据库支持 。 第二个问题更加微妙。 虽然典型的Spring应用程序几乎是完全无状态的(无论好坏),但数据库固有地是有状态的。 这使集成测试变得复杂,因为编写测试的第一个原则是它们应该彼此独立且可重复。 如果一个测试将某些内容写入数据库,则另一个测试可能会失败; 而且由于数据库更改,相同的测试可能在后续调用中失败。

显然,Spring还可以通过一个非常巧妙的技巧来处理此问题 :在运行每个测试之前,Spring启动一个新事务。 整个测试(包括其设置和拆除)在同一事务中运行,该事务在最后回滚。 这意味着测试期间所做的所有更改在数据库中都是可见的,就像它们是持久的一样。 但是,每次测试后的回滚将清除所有更改,并且下一个测试将在全新的数据库上进行。 辉煌!

不幸的是,这不是关于Spring集成测试优势的另一篇文章。 我想我已经编写了成百上千个这样的测试,而我真的很感谢Spring框架提供的透明支持。 但是我也遇到了这个舒适功能带来的众多怪异和不一致之处。 更糟的是,通常所谓的事务测试实际上隐藏了错误,使开发人员确信该软件可以工作,而部署后却会失败! 这是一个不尽详尽但令人大开眼界的问题集合:

@Test
public void shouldThrowLazyInitializationExceptionWhenFetchingLazyOneToManyRelationship() throws Exception {//givenfinal Book someBook = findAnyExistingBook();//whentry {someBook.reviews().size();fail();} catch(LazyInitializationException e) {//then}
}

这是Hibernate和spring集成测试中的一个已知问题。 Book是一个数据库实体,与“评论”具有一对多的关系,默认情况下是惰性的。 findAnyExistingBook()只是从事务服务中读取测试书。 现在有一点理论:只要将实体绑定到会话(如果使用JPA,则为EntityManager),它就可以延迟和透明地加载关系。 对我们而言,这意味着:只要它在交易范围之内。 实体离开交易的那一刻,它就变得分离。 在此生命周期阶段,实体不再连接到session / EntityManager(已经提交并关闭),并且任何获取懒惰属性的方法都将引发可怕的LazyInitializationException。 此行为实际上是在JPA中标准化的(异常类本身除外,后者是特定于供应商的)。

在本例中,我们正在调用.reviews()(Scala风格的“ getter”,我们也将尽快将测试用例转换为ScalaTest),并期望看到Hibernate异常。 但是不会引发异常,并且应用程序继续运行。 这是因为整个测试都在事务内运行,并且Book实体永远不会超出事务范围。 延迟加载在Spring集成测试中始终有效。

公平地说,我们在现实生活中永远不会看到这样的测试(除非您要进行测试以确保给定的集合是惰性的-不太可能)。 在现实生活中,我们正在测试仅在测试中起作用的业务逻辑。 但是,在部署之后,我们开始遇到LazyInitializationException。 但是我们测试了! Spring集成测试支持不仅隐藏了该问题 ,而且还鼓励开发人员使用OpenSessionInViewFilter或OpenEntityManagerInViewFilter 。 换句话说:我们的测试不仅没有发现代码中的错误,而且还大大恶化了我们的整体体系结构和性能。 不是我所期望的。

目前,实现某些端到端功能时,我通常的工作流程是编写后端测试,实现包括REST API的后端,并在一切运行顺利时进行部署并继续使用GUI。 后者是完全使用AJAX / JavaScript编写的,因此我只需要部署一次并经常替换便宜的客户端文件。 在此阶段,我不想回到服务器来修复未发现的错误。

抑制LazyInitializationException是Spring集成测试中最著名的问题之一。 但这只是冰山一角。 这是一个更复杂的示例(它再次使用JPA,但是此问题在普通JDBC和任何其他持久性技术中也很明显):

@Test
public void externalThreadShouldSeeChangesMadeInMainThread() throws Exception {//givenfinal Book someBook = findAnyExistingBook();someBook.setAuthor("Bruce Wayne");bookService.save(someBook);//whenfinal Future<Book> future = executorService.submit(new Callable<Book>() {@Overridepublic Book call() throws Exception {return bookService.findBy(someBook.id()).get();}});//thenassertThat(future.get().getAuthor()).isEqualTo("Bruce Wayne");
}

第一步,我们从数据库中加载一些书籍并修改作者,然后保存一个实体。 然后,我们通过id在另一个线程中加载相同的实体。 该实体已经保存,因此可以保证该线程可以看到更改。 但是,情况并非如此,最后一步中的断言证明了这一点。 发生了什么?

我们刚刚在ACID事务属性中观察到“ I”。 在提交事务之前,测试线程所做的更改对其他线程/连接不可见。 但是我们知道测试事务已提交! 这个小展示展示了在事务支持下编写多线程集成测试有多么困难。 几周前,当我想对启用JDBCJobStore的 Quartz调度程序进行集成测试时,我学到了很难的方法。 无论我多么努力,这些工作从未被解雇。 事实证明,我正在Spring事务范围内安排在Spring托管测试中的工作。 由于从未提交过事务,因此外部调度程序和工作线程无法在数据库中看到新的作业记录。 您花了几个小时调试此类问题?

在谈论调试时,对数据库相关的测试失败进行故障排除时会弹出相同的问题。 我可以将此简单的H2 Web控制台(浏览到localhost:8082)bean添加到我的测试配置中:

@Bean(initMethod = "start", destroyMethod = "stop")
def h2WebServer() = Server.createWebServer("-webDaemon", "-webAllowOthers")

但是在逐步进行测试时,我将永远不会看到测试所做的更改。 我无法手动运行查询以查明为什么返回错误结果。 另外,在进行故障排除时,我无法即时修改数据以更快地周转。 我的数据库位于另一个维度。

请仔细阅读下一个测试,时间不长:

@Test
public void shouldNotSaveAndLoadChangesMadeToNotManagedEntity() throws Exception {//givenfinal Book unManagedBook = findAnyExistingBook();unManagedBook.setAuthor("Clark Kent");//whenfinal Book loadedBook = bookService.findBy(unManagedBook.id()).get();//thenassertThat(loadedBook.getAuthor()).isNotEqualTo("Clark Kent");
}

我们正在加载一本书并修改作者, 而没有明确地坚持下去。 然后,我们再次从数据库中加载它,并确保更改未保留。 猜猜是什么,我们已经以某种方式更新了对象!

如果您是经验丰富的JPA / Hibernate用户,那么您将确切地知道如何发生。 还记得我在上面描述附着/分离的实体时的情况吗? 当实体仍附加到基础EntityManager /会话时,它也具有其他权力。 JPA提供者有义务跟踪对此类实体所做的更改,并在实体分离时将其自动传播到数据库(所谓的脏检查)。

这意味着使用JPA实体修改的惯用方式是从数据库中加载对象,使用setter执行必要的更改,仅此而已。 当实体分离时,JPA将发现它已被修改并为您发出UPDATE。 不需要merge()/ update(),可爱的对象抽象。 只要管理实体,此方法就起作用。 对分离的实体所做的更改将被静默忽略,因为JPA提供程序对此类实体一无所知。 现在最好的部分–您几乎不知道您的实体是否已附加,因为事务管理是透明的并且几乎是不可见的。 这意味着只修改内存中的POJO实例,同时仍然认为更改是持久的,反之亦然,这太容易了!

我们可以测试吗? 当然,我们只是做了–失败了。 在我们的测试中,交易涉及整个测试方法,因此每个实体都受到管理。 同样由于Hibernate L1缓存,即使尚未发布数据库更新,我们也可以获取完全相同的图书实例。 这是事务测试隐藏问题而不是揭示问题的另一种情况(请参阅LazyInitializationException示例)。 更改将如测试中所预期的那样传播到数据库,但是在部署后将被静默忽略……

顺便说一句,我是否提到过,一旦您放弃了对测试用例类的@Transactional注释,到目前为止所有测试都通过了? 看看,来源永远是可用的 。

这是令人兴奋的。 我有一个事务性的deleteAndThrow(book)业务方法,该方法删除给定的书并引发OppsException。 这是我通过的测试,证明代码正确:

@Test
public void shouldDeleteEntityAndThrowAnException() throws Exception {//givenfinal Book someBook = findAnyExistingBook();try {//whenbookService.deleteAndThrow(someBook);fail();} catch (OppsException e) {//thenfinal Option<Book> deletedBook = bookService.findBy(someBook.id());assertThat(deletedBook.isEmpty()).isTrue();}}

返回了Scala的Option <Book> (您是否已经注意到Java代码与用Scala编写的服务和实体交互得很好吗?),而不是null。 如果deleteBook.isEmpty()的结果为true,则表示未找到结果。 因此,似乎我们的代码是正确的:实体已删除,并且引发了异常。 是的,您是正确的,它在再次部署后会静默失败! 这次,Hibernate L1缓存知道该特定的book实例已删除,因此即使在刷新更改到数据库之前,它也返回null。 但是,从服务抛出的OppsException会回滚事务,并丢弃DELETE! 但是测试通过了,只是因为Spring管理着这个微小的额外事务,并且断言发生在该事务内。 毫秒后,事务回滚,恢复已删除的实体。

显然,解决方案是为OppsException添加noRollbackFor属性(这是我在放弃事务性测试以支持其他解决方案后在代码中发现的实际错误,目前尚待解释)。 但这不是重点。 关键是– 您真的可以负担起编写和维护正在生成错误肯定结果的测试,说服您的应用程序正常运行的能力,而事实并非如此?

哦,我是否提到过跨语言测试实际上在这里和那里泄漏,并且不会阻止您修改测试数据库? 第二次测试失败,您知道为什么吗?

@Test
public void shouldStoreReviewInSecondThread() throws Exception {final Book someBook = findAnyExistingBook();executorService.submit(new Callable<Review>() {@Overridepublic Review call() throws Exception {return reviewDao.save(new Review("Unicorn", "Excellent!!!1!", someBook));}}).get();
}
@Test
public void shouldNotSeeReviewStoredInPreviousTest() throws Exception {//given//whenfinal Iterable<Review> reviews = reviewDao.findAll();//thenassertThat(reviews).isEmpty();
}

线程再次陷入困境。 当您尝试在显然已提交的后台线程中进行外部事务处理后进行清理时,它会变得更加有趣。 自然的地方是在@After方法中删除创建的Review。 但是@After是在同一测试事务中执行的,因此清理将…回滚。

当然,我并不是在抱怨我最喜欢的应用程序堆栈弱点。 我在这里提供解决方案和提示。 我们的目标是完全摆脱事务测试,仅依赖于应用程序事务。 这将有助于我们避免上述所有问题。 显然,我们不能放弃测试的独立性和可重复性功能。 每个测试必须在同一数据库上工作才能可靠。 首先,我们将把JUnit测试转换为ScalaTest。 为了获得Spring依赖注入支持,我们需要这个微小的特征:

trait SpringRule extends Suite with BeforeAndAfterAll { this: AbstractSuite =>override protected def beforeAll() {new TestContextManager(this.getClass).prepareTestInstance(this)super.beforeAll();}}

现在是时候揭示我的想法了(如果您不耐烦,请在此处查看完整的源代码 )。 它远非独创性或独创性,但我认为它值得关注。 无需在一个巨大的事务中运行所有内容并将其回滚,只需让经过测试的代码在需要和配置的任何地方,任何时间启动和提交事务即可。 这意味着数据实际上已写入数据库,并且持久性与部署后的工作原理完全相同。 哪里有收获? 每次测试后,我们都必须以某种方式清理混乱……

事实证明它并不那么复杂。 只需转储干净的数据库,然后在每次测试后将其导入! 转储包含在部署和应用程序启动之后,第一次测试运行之前立即存在的所有表,约束和记录。 就像备份并从中还原一样! 看看H2有多简单:

trait DbResetRule extends Suite with BeforeAndAfterEach with BeforeAndAfterAll { this: SpringRule =>@Resource val dataSource: DataSource = nullval dbScriptFile = File.createTempFile(classOf[DbResetRule].getSimpleName + "-", ".sql")override protected def beforeAll() {new JdbcTemplate(dataSource).execute("SCRIPT NOPASSWORDS DROP TO '" + dbScriptFile.getPath + "'")dbScriptFile.deleteOnExit()super.beforeAll()}override protected def afterEach() {super.afterEach()new JdbcTemplate(dataSource).execute("RUNSCRIPT FROM '" + dbScriptFile.getPath + "'")}}trait DbResetSpringRule extends DbResetRule with SpringRule

SQL转储(请参阅H2 SCRIPT命令)执行一次并导出到临时文件。 然后在每次测试后执行SQL脚本文件。 信不信由你,就是这样! 我们的测试不再是事务性的(因此,所有Hibernate和多线程的极端情况都已发现并测试了),而我们并没有牺牲事务性测试设置的简便性(不需要额外的清理)。 我最终还可以在调试时查看数据库内容! 这是进行中的先前测试之一:

@RunWith(classOf[JUnitRunner])
@ContextConfiguration(classes = Array[Class[_]](classOf[SpringConfiguration]))
class BookServiceTest extends FunSuite with ShouldMatchers with BeforeAndAfterAll with DbResetSpringRule {@Resourceval bookService: BookService = nullprivate def findAnyExistingBook() = bookService.listBooks(new PageRequest(0, 1)).getContent.headtest("should delete entity and throw an exception") {val someBook = findAnyExistingBook()intercept[OppsException] {bookService.deleteAndThrow(someBook)}bookService findBy someBook.id should be (None)}
}

请记住,这不是一个库/实用程序,而是一个想法。 对于您的项目,您可能会选择略有不同的方法和工具,但是总体思路仍然适用:让您的代码在与部署后完全相同的环境中运行,然后从备份中清除混乱。 您可以使用JUnit, HSQLDB或任何您喜欢的方法获得完全相同的结果。 当然,您还可以添加一些巧妙的优化方法-标记或发现未修改数据库的测试,选择更快的转储,导入方法等。

老实说,还有一些弊端,以下是我脑海中的一些缺点:

  • 性能 :尽管这种方法并不总是比回滚事务慢得多(某些数据库回滚特别慢),但并不明显,但可以肯定地说。 当然,内存数据库可能具有一些意外的性能特征,但要为速度变慢做好准备。 但是,我没有在一个小项目中观察到每100个测试有巨大的差异(大约10%)。
  • 并发性 :您不再可以同时运行测试。 一个线程(测试)所做的更改对其他线程可见,从而使测试执行无法预测。 对于上述性能问题,这甚至变得更加痛苦。

就是这样。 如果您有兴趣,请给这种方法一个机会。 采用您现有的测试基础可能需要一些时间,但是即使发现一个隐藏的bug也值得,您是不是认为呢? 并且还要注意其他Spring陷阱 。

参考: Spring陷阱: NoBlogDefFound博客上的 JCG合作伙伴 Tomasz Nurkiewicz 认为有害的事务测试 。

相关文章 :
  • Spring陷阱:代理
  • Spring声明式事务示例
  • Spring依赖注入技术的发展
  • Spring和AspectJ的领域驱动设计
  • Spring 3使用JUnit 4进行测试– ContextConfiguration和AbstractTransactionalJUnit4SpringContextTests
  • 使用Spring AOP进行面向方面的编程
  • Java教程和Android教程列表

翻译自: https://www.javacodegeeks.com/2011/12/spring-pitfalls-transactional-tests.html

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

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

相关文章

python xlwt写入已有表_Python中,添加写入数据到已经存在的Excel文件

1.安装xlrd、xlwt、xlutilshttps://pypi.org/project/xlutils/pip安装&#xff1a;cmd下输入&#xff1a;pip install xlrd #读取exclepip install xlwt #写入exclepip install xlutils #操作 Excel 文件的实用工具&#xff0c;如复制、分割、筛选等2.代码主要部分实现import x…

java线程“生产/消费”模型2

/* 资源类 */ class ShareValue {private int total;//判断对象是否为空private boolean isEmptytrue;//判断对象是否已满private boolean isFulltrue;public ShareValue(int total) {this.total total;if(total>0) isEmptyfalse;if(total<1000) isFullfalse;}/** sync…

linux更改用户名_破旧安卓手机第二春,在安卓手机上使用Linux_deploy运行Linux

由于服务器位于国外&#xff0c;害怕被墙掉导致数据丢失&#xff0c;所以在本地写了脚本每小时从服务器上导出并下载到本地。但是电脑不可能二十四小时开机&#xff0c;所以很想买一个树莓派4玩玩。但是太贵辽&#xff0c;还好搜索到了Android运行Linux的方法&#xff0c;下面记…

关于Cocos2d-x中init方法和onEnter方法的区别

init()和onEnter()这两个方法都是写实例化对象的类(比如继承自Node的一些类等等)的时候用到的方法。 一般都是public类型下面的 bool init(); void onEnter(); 两个方法在实现的时候都要先执行父类方法&#xff0c;比如 Node::init(); Node::onEnter(); 注意&#xff1a; 1.ini…

在5分钟内在MacOSX Lion中设置JAVA_HOME,MAVEN_HOME,ANT_HOME

人们一直试图通过Mac上的Java开发世界来解决这个问题&#xff0c;这一直是我一直遇到的问题。 他们必须解决的第一件事就是设置适当的工具和环境。 幸运的是&#xff0c;足够多的MacOSX &#xff08;Lion或以前的版本&#xff09;仍然有许多重要的工具可用于Java开发&#xff0…

MCUXpress IDE常用设置

NXP的开发工具Xpress是基于eclipse制作的&#xff0c;我们如果需要设置一些东西可以直接搜索eclipse是怎么设置的。 1、字体大小 搜索eclipse字体大小&#xff0c;菜单Window > Preference 而Xpress是汉化了的&#xff0c;英语不好的同学可能懵逼&#xff0c;其实就是菜单栏…

C语言中空格符、空字符、字符数组结束符、换行、回车的区别

空格符和空字符是不一样的,在ASCII里面,空格(space)符号的ASCII码是32,而空字符是0, 2个是完全不一样的2个字符 空字符 一般来描述一个字符串的结尾,其实是控制符的一种,但不能理解为没有字符,应该理解为代表什么都没有的字符.好比回车0x0A和换行0x0D虽然不显示,但是也是控制字…

hdu5823 (附带数的二进制子集)

二进制数子集的取法,结果不会输出0&#xff0c;且从大到小 for(int i0 i;i0;i0(i0-1)&i)cout<<i0<<endl; 题意&#xff1a; 给定一个 N个点的图&#xff0c; 求它的每一个子图的最小染色数 染色方法是所有子图中相连接两点颜色不一致 其中 N≤18 题解&…

anaconda如何卸载库_小白必看!Anaconda安装全攻略

本文作者&#xff1a;戴 雯文字编辑&#xff1a;方 言技术总编&#xff1a;张馨月爬虫俱乐部云端课程来袭&#xff01;爬虫俱乐部将于2020年8月25日至28日在线上举行Stata数据分析法律与制度专题训练营&#xff0c;主要是为了让学员掌握Stata软件进阶操作&#xff0c;涉及…

RESTful Web服务可发现性,第4部分

这是有关使用Spring 3.1和Spring Security 3.1和基于Java的配置来建立安全的RESTful Web Service的系列文章的第四篇 。 本文将重点介绍REST API&#xff0c;HATEOAS的可发现性以及由测试驱动的实际方案。 引入REST可发现性 API的可发现性是一个值得引起足够关注的主题&#x…

10位IT领袖给应届毕业生的10条忠告

10位IT领袖给应届毕业生的10条忠告&#xff0c;在走向独立和自主的伟大征程中&#xff0c;吸取他们的经验。 在毕业生们迈出象牙塔之时&#xff0c;他们应该听从哪些人的建议&#xff1f;在走向独立和自主的伟大征程中&#xff0c;他们该吸取哪些教训&#xff1f;听一听各领域…

ubuntu安装好后常用软件安装和配置

1、安装vim sudo apt install vim 安装好后进入路径打开vimrc文件&#xff0c;这里需要注意一定要用sudo不然编辑后无法保存&#xff01; cd /etc/vim sudo vim vimrc 在最下面加入 set nu set ts4 set softtabstop4 set shiftwidth4 set expandtab set autoindent 依次是…

Objective-c 数据类型

这里列出Objective-c中独有数据类型&#xff1a; 一、字符串 在Objective-c中&#xff0c;字符串常量是由和一对从引号括起的字符串序列。比如&#xff1a;"China"、"objective-c"等都是合法的字符串常量。 二、id类型 id类型是Objective-c中一个比较独…

JBoss AS 7 EJB3池配置

现在&#xff0c;AS 7.0.1已经发布&#xff0c;让我们看一下可用的EJB3新功能。 就像我在上一篇文章中提到的那样 &#xff0c;AS 7.0.1现在允许您为无状态会话bean和MDB配置池。 当前&#xff0c;我们允许在子系统级别配置池&#xff0c;这意味着该池将适用于服务器上部署的所…

iOS开发网络篇—文件的上传

说明&#xff1a;文件上传使用的时POST请求&#xff0c;通常把要上传的数据保存在请求体中。本文介绍如何不借助第三方框架实现iOS开发中得文件上传。 由于过程较为复杂&#xff0c;因此本文只贴出部分关键代码。 主控制器的关键代码&#xff1a; YYViewController.m 1 #import…

var模型的matlab实现_Eviews中VAR模型的操作、脉冲响应分析和方差分解的实现

打开文件所在位置&#xff0c;获取数据。选中变量右键open打开var操作EViews,在VAR对象的工具栏中选择“View”|“Lag Structure”|“AR Roots Table/ AR Roots Graph”选项&#xff0c;得到AR根的表和图。结果显示&#xff1a;VAR模型所有根模的倒数都小于1&#xff0c;即都在…

一个程序员的爱情表白书

我能抽象出整个世界 但是我不能抽象出你 因为你在我心中是那么的具体 所以我的世界并不完整 我可以重载甚至覆盖这个世界里的任何一种方法 但是我却不能重载对你的思念 也许命中注定了 你在我的世界里永远的烙上了静态的属性 而我不慎调用了爱你这个方法 当我义无返顾的…

结构体、枚举类型

一、结构体 结构体&#xff1a;就是一个自定义的集合&#xff0c;里面可以放各种类型的元素&#xff0c;用法大体跟集合一样。 1、定义的方法&#xff1a; struct student { public int nianling; public int fenshu; public string name; public string sex; public int sum; …

NXP KW38开发杂记(一)MCUXpress 运行进入NMI_Handler

这里是大佬的具体分析过程&#xff0c;感兴趣可以看看 https://www.cnblogs.com/wenhao-Web/p/13618703.html 解决办法&#xff1a; 在startup_mkw38a4.c文件里&#xff0c;定位到Flash_Config {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFE}; 把最后一个参数0xFFFFFFFE改…

25个让Java程序员更高效的Eclipse插件

Eclipse提供了一个可扩展插件的开发系统。这就使得Eclipse在运行系统之上可以实现各种功能。这些插件也不同于其他的应用&#xff08;插件的功能是最难用代码实现的&#xff09;。拥有合适的Eclipse插件是非常重要的&#xff0c;因为它们能让Java开发者们无缝的开发基于J2EE和服…