针对故障场景的血液,汗液和书写自动集成测试

去年冬天,我为仍在工作的客户编写并发布了一项服务。 总体而言,该服务满足了业务需求和性能要求,但是使用该服务的一个团队告诉我,他们定期遇到一个问题,该问题是该服务将返回500个错误,并且在重新启动该服务之前不会恢复正常。 我问这是什么时候发生的, 戴上了侦探的帽子。

在此博客中,我将介绍诊断错误并确定正确的集成测试解决方案以正确方式进行修复的过程。 为此,我必须创建一个测试,以准确再现服务在PROD中遇到的情况。 我必须创建一个修复程序,使测试从失败到通过。 最后,我努力提高对所有未来发行版代码正确性的信心,这只有通过自动测试才能实现。

诊断错误

在500个错误开始发生时,我会仔细阅读服务的日志文件。 他们很快发现了一个非常严重的问题:在星期六的午夜之前,我的服务将开始引发错误。 最初,所有SQLException都发生了各种各样的错误,但最终根本原因是相同的:

org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is java.sql.SQLRecoverableException: IO Error: The Network Adapter could not establish the connectionat org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80)

此过程持续了几个小时,直到次日凌晨重新启动服务,服务恢复正常为止。

与检查 洞穴巨魔 DBA,我发现要连接的数据库已关闭以进行维护。 确切的细节使我无所适从,但我认为这是数据库关闭的大约30分钟的窗口。 因此,很明显,一旦数据库从中断中恢复,我的服务就无法重新连接到数据库。

修复此错误(和我过去经常去过的错误)的最直接方法是使用Google“从数据库中断中恢复”,这很可能导致我遇到一个Stack Overflow线程,该线程可以回答我的问题。 然后,我将在提供的答案中“复制并粘贴”并推送要测试的代码。

如果生产受到错误的严重影响,则在短期内可能需要使用此方法。 就是说,应该在不久的将来留出时间来用自动测试来覆盖更改。

因此,通常情况下,“正确的方式”做事通常意味着大量的字体加载时间投资,这句话在这里肯定是正确的。

但是,投资回报是花费在修复错误上的时间减少了,对代码正确性的信心增加了,此外,测试可以作为文档在给定场景下的行为的重要形式。

尽管这个特定的测试用例有些深奥,但在设计和编写测试(无论是单元测试还是集成测试)时要牢记这一重要因素:给测试起好名字,确保测试代码可读性,等等。

解决方案1:模拟一切

我为该问题编写测试的第一个步骤是尝试“模拟一切”。 尽管Mockito和其他模拟框架非常强大,并且变得越来越容易使用,但在考虑了此解决方案之后,我很快得出结论,就是我永远不会有信心,除了模拟之外,我不会进行任何测试已经写了。

获得“绿色”结果并不会增加我对代码正确性的信心,而这首先是编写自动化测试的全部要点! 转到另一种方法。

解决方案2:使用内存数据库

我编写测试的下一个尝试是使用内存数据库。 我是H2的忠实拥护者,过去我广泛使用H2,希望它可以再次满足我的需求。 我在这里的时间可能比我应该花费的时间多。

虽然最终这种方法没有成功,但花费的时间并没有完全浪费,我确实学到了更多有关H2的知识。 以“正确的方式”做事的好处之一(尽管此刻通常很痛苦)是您可以学到很多东西。 所获得的知识在当时可能没有用,但以后可能会有价值。

使用内存数据库的优势

就像我说的那样,我在这里的时间可能比我应该花的时间更多,但是我确实有希望这种解决方案起作用的原因。 H2和其他内存数据库具有两个非常理想的特征:

  • 速度: H2的启动和停止相当快,不到一秒。 因此,尽管比使用模拟慢一些,但我的测试仍会很快。
  • 可移植性: H2可以完全从导入的jar运行,因此其他开发人员可以仅提取我的代码并运行所有测试,而无需执行任何其他步骤。

另外,我最终的解决方案有两个非常重要的缺点,下面将作为解决方案的一部分进行介绍。

编写测试

有点有意义,但是到目前为止,我还没有编写任何一行生产代码。 TDD的主要原则是先编写测试,然后编写生产代码。 这种方法论以及确保高水平的测试覆盖率还鼓励开发人员仅进行必要的更改。 这回到了提高对代码正确性的信心这一目标。

以下是我用来测试PROD问题的初始测试用例:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DataSourceConfig.class, properties = {"datasource.driver=org.h2.Driver",
"datasource.url=jdbc:h2:mem:;MODE=ORACLE", "datasource.user=test", "datasource.password=test" })
public class ITDatabaseFailureAndRecovery {@Autowiredprivate DataSource dataSource;@Testpublic void test() throws SQLException {Connection conn = DataSourceUtils.getConnection(dataSource);conn.createStatement().executeQuery("SELECT 1 FROM dual");ResultSet rs = conn.createStatement().executeQuery("SELECT 1 FROM dual");assertTrue(rs.next());assertEquals(1, rs.getLong(1));conn.createStatement().execute("SHUTDOWN");DataSourceUtils.releaseConnection(conn, dataSource);conn = DataSourceUtils.getConnection(dataSource);rs = conn.createStatement().executeQuery("SELECT 1 FROM dual");assertTrue(rs.next());assertEquals(1, rs.getLong(1));}
}

最初,我觉得使用此解决方案的方向正确。 有一个问题是如何启动H2服务器备份(一次有一个问题!),但是当我运行测试时,它失败了,并给出了与我的服务在PROD中所经历的类似的错误:

org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-192]

但是,如果我修改测试用例并仅尝试第二次连接数据库:

conn = DataSourceUtils.getConnection(dataSource);

异常消失了,我的测试通过了,而无需更改生产代码。 这里不对劲…

为什么此解决方案不起作用

因此,使用H2将不起作用。 实际上,我花了很多时间尝试使H2正常工作,而不是上面建议的时间。 包括故障排除尝试; 连接到基于文件的H2服务器实例,而不只是一个内存中的远程H2服务器; 我什至偶然发现了H2 Server类 , 该类本来可以解决早先的服务器关闭/启动问题。

这些尝试显然都没有效果。 H2的基本问题(至少对于此测试用例而言)是,尝试连接到数据库(如果当前未运行)将导致该数据库启动。 正如我的初始测试用例所示,这有点延迟,但是显然这构成了一个基本问题。 在PROD中,当我的服务尝试连接到数据库时,它不会导致数据库启动(无论我尝试连接多少次)。 我的服务日志肯定可以证明这一事实。 接下来是另一种方法。

解决方案3:连接到本地数据库

模拟一切都行不通。 使用内存数据库也不会成功。 看来,我能够正确重现我的服务在PROD中遇到的方案的唯一方法是连接到更正式的数据库实现。 关闭共享开发数据库是不可能的,因此该数据库实现需要在本地运行。

该解决方案的问题

因此,在此之前的所有内容都应该很好地表明我确实希望避免走这条路。 我的沉默有一些很好的理由:

  • 降低的可移植性:如果其他开发人员想要运行此测试,则需要在本地计算机上下载并安装数据库。 她还需要确保她的配置详细信息符合测试的期望。 这是一项耗时的任务,并且至少会导致一定数量的“带外”知识。
  • 速度较慢:总体而言,我的测试仍然不太慢,但是启动,关闭和重新启动(即使是针对本地数据库)也需要花费几秒钟的时间。 虽然几秒钟听起来不算多,但可以通过足够的测试来累加时间。 这是一个主要的问题,因为允许集成测试花费更长的时间(以后要花更多的时间),但是集成测试越快,运行它们的频率就越高。
  • 组织争执:要在构建服务器上运行此测试,意味着我现在需要与已经负担过重的DevOps团队合作,在构建框中设置数据库。 即使操作团队没有负担过重,我也想尽可能避免这种情况,因为这只是又一步。
  • 许可:在我的代码示例中,我使用MySQL作为测试数据库实现。 但是,对于我的客户,我正在连接到Oracle数据库。 Oracle确实免费提供了Oracle Express Edition(XE),但确实有规定。 这些规定之一是不能同时运行两个Oracle XE实例。 除了Oracle XE的特殊情况外,在连接到特定产品时,许可可能成为一个问题,这一点要牢记。

…最后

最初,这篇文章要长很多,这也给所有 鲜血,汗水和眼泪 到现在为止的工作。 最终,这些信息对读者而言并不是特别有用,即使这是作者写信的方式。 因此,事不宜迟,一个测试可以准确地重现我的服务在PROD中遇到的情况:

@Test
public void testServiceRecoveryFromDatabaseOutage() throws SQLException, InterruptedException, IOException {Connection conn = null;conn = DataSourceUtils.getConnection(datasource);assertTrue(conn.createStatement().execute("SELECT 1"));DataSourceUtils.releaseConnection(conn, datasource);LOGGER.debug("STOPPING DB");Runtime.getRuntime().exec("/usr/local/mysql/support-files/mysql.server stop").waitFor();LOGGER.debug("DB STOPPED");try {conn = DataSourceUtils.getConnection(datasource);conn.createStatement().execute("SELECT 1");fail("Database is down at this point, call should fail");} catch (Exception e) {LOGGER.debug("EXPECTED CONNECTION FAILURE");}LOGGER.debug("STARTING DB");Runtime.getRuntime().exec("/usr/local/mysql/support-files/mysql.server start").waitFor();LOGGER.debug("DB STARTED");conn = DataSourceUtils.getConnection(datasource);assertTrue(conn.createStatement().execute("SELECT 1"));DataSourceUtils.releaseConnection(conn, datasource);
}

完整代码在这里: https : //github.com/wkorando/integration-test-example/blob/master/src/test/java/com/integration/test/example/ITDatabaseFailureAndRecovery.java

修复

所以我有我的测试用例。 现在是时候编写生产代码以使我的测试显示为绿色。 最终,我从一个朋友那里得到了答案,但是可能会在使用足够的谷歌搜索功能时偶然发现了这个答案。

最初,我在服务配置中设置的数据源实际上看起来像这样:

@Bean
public DataSource dataSource() {org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();dataSource.setDriverClassName(env.getRequiredProperty("datasource.driver"));dataSource.setUrl(env.getRequiredProperty("datasource.url"));dataSource.setUsername(env.getRequiredProperty("datasource.user"));dataSource.setPassword(env.getRequiredProperty("datasource.password"));return dataSource;
}

我的服务遇到的潜在问题是,当来自DataSource的连接池的连接未能连接到数据库时,它变得“不好”。 然后,下一个问题是我的DataSource实现不会从连接池中删除这些“不良”连接。 它只是不断尝试使用它们。

幸运的是,此修复非常简单。 当DataSource从连接池中检索连接时,我需要指示DataSource测试连接。 如果此测试失败,则连接将从池中删除,并尝试建立新的连接。 我还需要为DataSource提供一个查询,它可以用来测试连接。

最后(并非绝对必要,但对测试很有用),默认情况下,我的DataSource实现仅每30秒测试一次连接。 但是,我的测试可以在不到30秒的时间内运行。 最终,这段时间的长度并没有真正意义,因此我添加了一个由属性文件提供的验证间隔。

这是我更新后的DataSource外观:

@Bean
public DataSource dataSource() {org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();dataSource.setDriverClassName(env.getRequiredProperty("datasource.driver"));dataSource.setUrl(env.getRequiredProperty("datasource.url"));dataSource.setUsername(env.getRequiredProperty("datasource.user"));dataSource.setPassword(env.getRequiredProperty("datasource.password"));dataSource.setValidationQuery("SELECT 1");dataSource.setTestOnBorrow(true);dataSource.setValidationInterval(env.getRequiredProperty("datasource.validation.interval"));return dataSource;
}

关于编写集成测试的最后一点说明。 最初,我创建了一个测试配置文件,该文件用于配置要在测试中使用的DataSource 。 但是,这是不正确的。

问题是,如果有人要从生产配置文件中删除我的修订,但将其保留在测试配置文件中,则我的测试仍会通过,但是我的实际生产代码将再次受到我这段时间花的问题的侵害。定影! 这是一个容易想象的错误。 因此,在编写集成测试时,请确保使用实际的生产配置文件。

自动化测试

因此,即将结束。 我有一个测试用例,可以准确地重现我在PROD中遇到的情况。 我有一个修复程序,然后使我的测试从失败到通过。 但是,所有这些工作的重点不仅是让我确信我的修订适用于下一个版本,而且还适用于所有将来的版本。

Maven用户:希望您已经熟悉surefire插件 。 或者,至少希望您的DevOps团队已经设置了父pom,以便在构建服务器上构建项目时,每次提交都会花费您花时间编写的所有单元测试。

但是,本文不是关于编写单元测试的,而是关于编写集成测试的 。 集成测试套件的运行时间(有时数小时)通常比单元测试套件的时间(不超过5-10分钟)长得多。 集成测试通常也更容易波动。 虽然我在本文中编写的集成测试应该是稳定的-如果它破裂了,应该引起关注-在连接到开发数据库时,您不能总是100%确信数据库将可用或测试数据将是正确的甚至是存在的。 因此,失败的集成测试并不一定意味着代码不正确。

幸运的是,Maven背后的人们已经解决了这个问题,那就是带有故障安全插件 。 默认情况下,surefire插件将查找Test之前或之后固定的类,而failsafe插件将查找IT (集成测试)之前或之后固定的类。 像所有Maven插件一样,您可以配置插件应执行的目标。 这使您可以灵活地使每次代码提交都运行单元测试,而集成测试仅在夜间构建时运行。 这也可以防止需要部署修补程序但不存在集成测试所依赖的资源的情况。

最后的想法

编写集成测试既耗时又困难。 它需要广泛考虑您的服务将如何与其他资源交互。 当您专门测试故障场景时,此过程甚至更加困难且耗时,这通常需要对测试所连接的资源进行更深入的控制,并借鉴过去的经验和知识。

尽管花费了大量的时间和精力,但这项投资将随着时间的推移多次收回投资。 只有通过自动测试才能提高对代码正确性的信心,这对缩短开发反馈周期至关重要。

我在本文中使用的代码可以在这里找到: https : //github.com/wkorando/integration-test-example 。

翻译自: https://www.javacodegeeks.com/2016/10/blood-sweat-writing-automated-integration-tests-failure-scenarios.html

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

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

相关文章

java 8 update 11_从Java 8升级到Java 11应该注意的问题

从 Java 8迁移到Java 11比大多数升级更棘手。以下是这个过程的一些注意事项。模块在Java 9中Java引入了历史上最大的变化之一 是模块,但:不必将你自己的代码模块化以后才能升级到Java 11。在大多数情况下,放在类路径classpath上的代码能继续在…

标签树的三种遍历

一、标签树的下行遍历 属性说明.contents子节点的列表&#xff0c;将<tag>所有儿子节点存入列表中&#xff08;只能获取下一级儿子节点&#xff09;.children子节点的迭代类型&#xff0c;与.contents类似&#xff0c;用于循环遍历儿子节点.descendants子孙节点的迭代类型…

Git 使用规范流程

团队开发中&#xff0c;遵循一个合理、清晰的Git使用流程&#xff0c;是非常重要的。 否则&#xff0c;每个人都提交一堆杂乱无章的commit&#xff0c;项目很快就会变得难以协调和维护。 下面是ThoughtBot 的Git使用规范流程。我从中学到了很多&#xff0c;推荐你也这样使用Git…

oracle安装静默

安装环境&#xff1a;centos71、修改主机名 /etc/sysconfig/network#HOSTNAMEoracledb.012、修改ip和对应的主机名 /etc/hosts#10.5.1.190 oracledb.013、关闭Selinux /etc/selinux/config#SELINUXdisabled4、参考官方文档安装依赖的软件包 binutils-2.23.52.0.1-12.el7.x86_64…

[MEGA DEAL] Ultimate Java开发和认证指南(59%折扣)

通过介绍世界上最受欢迎的编程语言之一掌握Java编程概念 嘿&#xff0c;怪胎&#xff0c; 本周&#xff0c;在我们的JCG Deals商店中 &#xff0c;我们提供了一个极端的报价 。 我们提供的《 Ultimate Java Development and Certification Guide 》 仅售20美元&#xff0c;而…

python浅拷贝的说法_Python中List的复制(直接复制、浅拷贝、深拷贝)

直接赋值&#xff1a;如果用 直接赋值&#xff0c;是非拷贝方法。这两个列表是等价的&#xff0c;修改其中任何一个列表都会影响到另一个列表。old [1,[1,2,3],3]new []for i in range(len(old)):new.append(old[i])new[0] 3new[1][0] 3-----------------------Before:[1,…

AliOS-Things Visual studio code helloworld 入门

配置环境的时候别忘了下载&#xff1a;GCC工具链&#xff1a;https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads AliOS Things 完成第一个应用&#xff1a;Hello World 全局掌控 从Git上下载源码用VSCode打开源码&#xff0c;查看源码的目录结构打开Hello…

java 联网_java网络

这个图很形象的展示了OSI的五层架构之间的关系。OSI被称为开放式互联&#xff0c;是国际标准组织制定的网络模型&#xff0c;本来是七层&#xff0c;后来把表现层和会话层加到应用层里面了。那么五层模型中的每一层具体都是干什么的呢&#xff1f;在标准的网络模型中&#xff0…

[小米OJ] 4. 最长连续数列

思路&#xff1a; 时间限制为O(n)&#xff0c;即不能使用先排序后寻找的方法。 这里利用哈希表查询插入复杂度都为O(1)的特性来解&#xff0c;利用一个哈希表来保存每一个数字以及其所在数列的长度。 遍历每一个数字n&#xff1a;查询表中是否存在n-1和n1&#xff0c;若存在&am…

控制反转

控制反转[编辑] 维基百科&#xff0c;自由的百科全书控制反转&#xff08;Inversion of Control&#xff0c;缩写为IoC&#xff09;&#xff0c;是面向对象编程中的一种设计原则&#xff0c;可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入&#xff08;Depe…

使用Mutability Detector对Java数据类的不变性进行单元测试

在我们所有的项目中&#xff0c;我们使用的数据类根据定义包含数据&#xff08;字段&#xff09;&#xff0c;但不包含&#xff08;业务&#xff09;逻辑。 根据最佳编码实践&#xff0c;数据类最好应该是不可变的&#xff0c;因为不可变性意味着线程安全。 这里的主要参考是J…

三种单例模式的C++实现

简介 因为在设计或开发中&#xff0c;肯定会有这么一种情况&#xff0c;一个类只能有一个对象被创建&#xff0c;如果有多个对象的话&#xff0c;可能会导致状态的混乱和不一致。这种情况下&#xff0c;单例模式是最恰当的解决办法。它有很多种实现方式&#xff0c;各自的特性不…

centos安装Redis

vRedis的安装 ♛ 1.0 下载Rediswget http://download.redis.io/releases/redis-5.0.2.tar.gz ♛ 1.1 解压tar -zxvf redis-5.0.2.tar.gz ♛ 1.2 安装gcc依赖Redis是C实现的&#xff0c;需要gcc来进行编译&#xff0c;先安装gcc。 yum install gcc ♛ 1.3 打开Redis目录cd redis…

maven插件编写_编写Maven插件的提示

maven插件编写最近&#xff0c;我花了很多时间为Maven编写插件或在其中工作。 它们简单&#xff0c;有趣且有趣。 我以为我会分享一些技巧&#xff0c;使编写它们时的生活更轻松。 提示1&#xff1a;将任务与Mojo分开 最初&#xff0c;您将把mojo的所有代码放入mojo的类&…

e3是合法浮点数吗_下列哪些是不合法的浮点数的选项是 123 2e4.2 .e5 -e3 .234 1e3

共回答了16个问题采纳率&#xff1a;93.8%浮点数是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数.具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学记数法.由此可以看出…

VC的Win32控制台程序中使用MFC库文件

如果想要在VC中的Win32控制台程序中使用CString类&#xff0c;应注意下列情况&#xff1a; 1.必须包含头文件#include <afx.h> 2.Project ----settings... ----General---- Microsoft Foundation Classes: Use MFC in a Shared DLL

java能否调用com_java调用com

一、C#写com组件开发环境 vs20051、新建工程:ClassLibrary1 //根据自己需要取工程名2、右键点击工程->应用程序->程序集信息->使程序集com可见&#xff0c;打上勾右键点击工程->生成->为com Interop注册 打上勾3、设置强签名打开Visual Studio Command Pr…

JUnit规则–引发异常时执行附加验证

在本文中&#xff0c;我将快速向您展示如果您需要解决以下挑战&#xff0c;那么JUnit规则有多方便 方法捕获异常并必须执行一些额外的任务&#xff0c;然后再抛出或引发包装异常。 调用额外任务和引发的异常应通过单元测试进行验证。 这意味着您有一些这样的代码 public cla…

十分钟搞清字符集和字符编码

什么是字符集 什么是字符编码 UTF-8和Unicode的关系 UTF-8编码简介 为什么会出现乱码 如何识别乱码的本来想要表达的文字 常见问题处理之Emoji 本文将简述字符集&#xff0c;字符编码的概念。以及在遭遇乱码时的一些常用诊断技巧 背景&#xff1a;字符集和编码无疑是IT菜鸟甚至…

前端Ajax/JS/HTML+后端SpringMVC(二)

1. jQuery AJAX 1.1. jQuery框架中的ajax()函数 在应用了jQuery框架后&#xff0c;调用ajax()函数即可发出AJAX请求&#xff0c;并获取响应结果&#xff0c;该函数的参数必须是JSON对象&#xff0c;通常&#xff0c;在JSON对象中封装的属性有&#xff1a; url&#xff1a;处理请…