针对故障场景的血液,汗液和书写自动集成测试
去年冬天,我為仍在工作的客戶(hù)編寫(xiě)并發(fā)布了一項(xiàng)服務(wù)。 總體而言,該服務(wù)滿足了業(yè)務(wù)需求和性能要求,但是使用該服務(wù)的一個(gè)團(tuán)隊(duì)告訴我,他們定期遇到一個(gè)問(wèn)題,該問(wèn)題是該服務(wù)將返回500個(gè)錯(cuò)誤,并且在重新啟動(dòng)該服務(wù)之前不會(huì)恢復(fù)正常。 我問(wèn)這是什么時(shí)候發(fā)生的, 戴上了偵探的帽子。
在此博客中,我將介紹診斷錯(cuò)誤并確定正確的集成測(cè)試解決方案以正確方式進(jìn)行修復(fù)的過(guò)程。 為此,我必須創(chuàng)建一個(gè)測(cè)試,以準(zhǔn)確再現(xiàn)服務(wù)在PROD中遇到的情況。 我必須創(chuàng)建一個(gè)修復(fù)程序,使測(cè)試從失敗到通過(guò)。 最后,我努力提高對(duì)所有未來(lái)發(fā)行版代碼正確性的信心,這只有通過(guò)自動(dòng)測(cè)試才能實(shí)現(xiàn)。
診斷錯(cuò)誤
在500個(gè)錯(cuò)誤開(kāi)始發(fā)生時(shí),我會(huì)仔細(xì)閱讀服務(wù)的日志文件。 他們很快發(fā)現(xiàn)了一個(gè)非常嚴(yán)重的問(wèn)題:在星期六的午夜之前,我的服務(wù)將開(kāi)始引發(fā)錯(cuò)誤。 最初,所有SQLException都發(fā)生了各種各樣的錯(cuò)誤,但最終根本原因是相同的:
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)此過(guò)程持續(xù)了幾個(gè)小時(shí),直到次日凌晨重新啟動(dòng)服務(wù),服務(wù)恢復(fù)正常為止。
與檢查 洞穴巨魔 DBA,我發(fā)現(xiàn)要連接的數(shù)據(jù)庫(kù)已關(guān)閉以進(jìn)行維護(hù)。 確切的細(xì)節(jié)使我無(wú)所適從,但我認(rèn)為這是數(shù)據(jù)庫(kù)關(guān)閉的大約30分鐘的窗口。 因此,很明顯,一旦數(shù)據(jù)庫(kù)從中斷中恢復(fù),我的服務(wù)就無(wú)法重新連接到數(shù)據(jù)庫(kù)。
修復(fù)此錯(cuò)誤(和我過(guò)去經(jīng)常去過(guò)的錯(cuò)誤)的最直接方法是使用Google“從數(shù)據(jù)庫(kù)中斷中恢復(fù)”,這很可能導(dǎo)致我遇到一個(gè)Stack Overflow線程,該線程可以回答我的問(wèn)題。 然后,我將在提供的答案中“復(fù)制并粘貼”并推送要測(cè)試的代碼。
如果生產(chǎn)受到錯(cuò)誤的嚴(yán)重影響,則在短期內(nèi)可能需要使用此方法。 就是說(shuō),應(yīng)該在不久的將來(lái)留出時(shí)間來(lái)用自動(dòng)測(cè)試來(lái)覆蓋更改。
因此,通常情況下,“正確的方式”做事通常意味著大量的字體加載時(shí)間投資,這句話在這里肯定是正確的。
但是,投資回報(bào)是花費(fèi)在修復(fù)錯(cuò)誤上的時(shí)間減少了,對(duì)代碼正確性的信心增加了,此外,測(cè)試可以作為文檔在給定場(chǎng)景下的行為的重要形式。
盡管這個(gè)特定的測(cè)試用例有些深?yuàn)W,但在設(shè)計(jì)和編寫(xiě)測(cè)試(無(wú)論是單元測(cè)試還是集成測(cè)試)時(shí)要牢記這一重要因素:給測(cè)試起好名字,確保測(cè)試代碼可讀性,等等。
解決方案1:模擬一切
我為該問(wèn)題編寫(xiě)測(cè)試的第一個(gè)步驟是嘗試“模擬一切”。 盡管Mockito和其他模擬框架非常強(qiáng)大,并且變得越來(lái)越容易使用,但在考慮了此解決方案之后,我很快得出結(jié)論,就是我永遠(yuǎn)不會(huì)有信心,除了模擬之外,我不會(huì)進(jìn)行任何測(cè)試已經(jīng)寫(xiě)了。
獲得“綠色”結(jié)果并不會(huì)增加我對(duì)代碼正確性的信心,而這首先是編寫(xiě)自動(dòng)化測(cè)試的全部要點(diǎn)! 轉(zhuǎn)到另一種方法。
解決方案2:使用內(nèi)存數(shù)據(jù)庫(kù)
我編寫(xiě)測(cè)試的下一個(gè)嘗試是使用內(nèi)存數(shù)據(jù)庫(kù)。 我是H2的忠實(shí)擁護(hù)者,過(guò)去我廣泛使用H2,希望它可以再次滿足我的需求。 我在這里的時(shí)間可能比我應(yīng)該花費(fèi)的時(shí)間多。
雖然最終這種方法沒(méi)有成功,但花費(fèi)的時(shí)間并沒(méi)有完全浪費(fèi),我確實(shí)學(xué)到了更多有關(guān)H2的知識(shí)。 以“正確的方式”做事的好處之一(盡管此刻通常很痛苦)是您可以學(xué)到很多東西。 所獲得的知識(shí)在當(dāng)時(shí)可能沒(méi)有用,但以后可能會(huì)有價(jià)值。
使用內(nèi)存數(shù)據(jù)庫(kù)的優(yōu)勢(shì)
就像我說(shuō)的那樣,我在這里的時(shí)間可能比我應(yīng)該花的時(shí)間更多,但是我確實(shí)有希望這種解決方案起作用的原因。 H2和其他內(nèi)存數(shù)據(jù)庫(kù)具有兩個(gè)非常理想的特征:
- 速度: H2的啟動(dòng)和停止相當(dāng)快,不到一秒。 因此,盡管比使用模擬慢一些,但我的測(cè)試仍會(huì)很快。
- 可移植性: H2可以完全從導(dǎo)入的jar運(yùn)行,因此其他開(kāi)發(fā)人員可以?xún)H提取我的代碼并運(yùn)行所有測(cè)試,而無(wú)需執(zhí)行任何其他步驟。
另外,我最終的解決方案有兩個(gè)非常重要的缺點(diǎn),下面將作為解決方案的一部分進(jìn)行介紹。
編寫(xiě)測(cè)試
有點(diǎn)有意義,但是到目前為止,我還沒(méi)有編寫(xiě)任何一行生產(chǎn)代碼。 TDD的主要原則是先編寫(xiě)測(cè)試,然后編寫(xiě)生產(chǎn)代碼。 這種方法論以及確保高水平的測(cè)試覆蓋率還鼓勵(lì)開(kāi)發(fā)人員僅進(jìn)行必要的更改。 這回到了提高對(duì)代碼正確性的信心這一目標(biāo)。
以下是我用來(lái)測(cè)試PROD問(wèn)題的初始測(cè)試用例:
@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));} }最初,我覺(jué)得使用此解決方案的方向正確。 有一個(gè)問(wèn)題是如何啟動(dòng)H2服務(wù)器備份(一次有一個(gè)問(wèn)題!),但是當(dāng)我運(yùn)行測(cè)試時(shí),它失敗了,并給出了與我的服務(wù)在PROD中所經(jīng)歷的類(lèi)似的錯(cuò)誤:
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]但是,如果我修改測(cè)試用例并僅嘗試第二次連接數(shù)據(jù)庫(kù):
conn = DataSourceUtils.getConnection(dataSource);異常消失了,我的測(cè)試通過(guò)了,而無(wú)需更改生產(chǎn)代碼。 這里不對(duì)勁…
為什么此解決方案不起作用
因此,使用H2將不起作用。 實(shí)際上,我花了很多時(shí)間嘗試使H2正常工作,而不是上面建議的時(shí)間。 包括故障排除嘗試; 連接到基于文件的H2服務(wù)器實(shí)例,而不只是一個(gè)內(nèi)存中的遠(yuǎn)程H2服務(wù)器; 我什至偶然發(fā)現(xiàn)了H2 Server類(lèi) , 該類(lèi)本來(lái)可以解決早先的服務(wù)器關(guān)閉/啟動(dòng)問(wèn)題。
這些嘗試顯然都沒(méi)有效果。 H2的基本問(wèn)題(至少對(duì)于此測(cè)試用例而言)是,嘗試連接到數(shù)據(jù)庫(kù)(如果當(dāng)前未運(yùn)行)將導(dǎo)致該數(shù)據(jù)庫(kù)啟動(dòng)。 正如我的初始測(cè)試用例所示,這有點(diǎn)延遲,但是顯然這構(gòu)成了一個(gè)基本問(wèn)題。 在PROD中,當(dāng)我的服務(wù)嘗試連接到數(shù)據(jù)庫(kù)時(shí),它不會(huì)導(dǎo)致數(shù)據(jù)庫(kù)啟動(dòng)(無(wú)論我嘗試連接多少次)。 我的服務(wù)日志肯定可以證明這一事實(shí)。 接下來(lái)是另一種方法。
解決方案3:連接到本地?cái)?shù)據(jù)庫(kù)
模擬一切都行不通。 使用內(nèi)存數(shù)據(jù)庫(kù)也不會(huì)成功。 看來(lái),我能夠正確重現(xiàn)我的服務(wù)在PROD中遇到的方案的唯一方法是連接到更正式的數(shù)據(jù)庫(kù)實(shí)現(xiàn)。 關(guān)閉共享開(kāi)發(fā)數(shù)據(jù)庫(kù)是不可能的,因此該數(shù)據(jù)庫(kù)實(shí)現(xiàn)需要在本地運(yùn)行。
該解決方案的問(wèn)題
因此,在此之前的所有內(nèi)容都應(yīng)該很好地表明我確實(shí)希望避免走這條路。 我的沉默有一些很好的理由:
- 降低的可移植性:如果其他開(kāi)發(fā)人員想要運(yùn)行此測(cè)試,則需要在本地計(jì)算機(jī)上下載并安裝數(shù)據(jù)庫(kù)。 她還需要確保她的配置詳細(xì)信息符合測(cè)試的期望。 這是一項(xiàng)耗時(shí)的任務(wù),并且至少會(huì)導(dǎo)致一定數(shù)量的“帶外”知識(shí)。
- 速度較慢:總體而言,我的測(cè)試仍然不太慢,但是啟動(dòng),關(guān)閉和重新啟動(dòng)(即使是針對(duì)本地?cái)?shù)據(jù)庫(kù))也需要花費(fèi)幾秒鐘的時(shí)間。 雖然幾秒鐘聽(tīng)起來(lái)不算多,但可以通過(guò)足夠的測(cè)試來(lái)累加時(shí)間。 這是一個(gè)主要的問(wèn)題,因?yàn)樵试S集成測(cè)試花費(fèi)更長(zhǎng)的時(shí)間(以后要花更多的時(shí)間),但是集成測(cè)試越快,運(yùn)行它們的頻率就越高。
- 組織爭(zhēng)執(zhí):要在構(gòu)建服務(wù)器上運(yùn)行此測(cè)試,意味著我現(xiàn)在需要與已經(jīng)負(fù)擔(dān)過(guò)重的DevOps團(tuán)隊(duì)合作,在構(gòu)建框中設(shè)置數(shù)據(jù)庫(kù)。 即使操作團(tuán)隊(duì)沒(méi)有負(fù)擔(dān)過(guò)重,我也想盡可能避免這種情況,因?yàn)檫@只是又一步。
- 許可:在我的代碼示例中,我使用MySQL作為測(cè)試數(shù)據(jù)庫(kù)實(shí)現(xiàn)。 但是,對(duì)于我的客戶(hù),我正在連接到Oracle數(shù)據(jù)庫(kù)。 Oracle確實(shí)免費(fèi)提供了Oracle Express Edition(XE),但確實(shí)有規(guī)定。 這些規(guī)定之一是不能同時(shí)運(yùn)行兩個(gè)Oracle XE實(shí)例。 除了Oracle XE的特殊情況外,在連接到特定產(chǎn)品時(shí),許可可能成為一個(gè)問(wèn)題,這一點(diǎn)要牢記。
…最后
最初,這篇文章要長(zhǎng)很多,這也給所有 鮮血,汗水和眼淚 到現(xiàn)在為止的工作。 最終,這些信息對(duì)讀者而言并不是特別有用,即使這是作者寫(xiě)信的方式。 因此,事不宜遲,一個(gè)測(cè)試可以準(zhǔn)確地重現(xiàn)我的服務(wù)在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
修復(fù)
所以我有我的測(cè)試用例。 現(xiàn)在是時(shí)候編寫(xiě)生產(chǎn)代碼以使我的測(cè)試顯示為綠色。 最終,我從一個(gè)朋友那里得到了答案,但是可能會(huì)在使用足夠的谷歌搜索功能時(shí)偶然發(fā)現(xiàn)了這個(gè)答案。
最初,我在服務(wù)配置中設(shè)置的數(shù)據(jù)源實(shí)際上看起來(lái)像這樣:
@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; }我的服務(wù)遇到的潛在問(wèn)題是,當(dāng)來(lái)自DataSource的連接池的連接未能連接到數(shù)據(jù)庫(kù)時(shí),它變得“不好”。 然后,下一個(gè)問(wèn)題是我的DataSource實(shí)現(xiàn)不會(huì)從連接池中刪除這些“不良”連接。 它只是不斷嘗試使用它們。
幸運(yùn)的是,此修復(fù)非常簡(jiǎn)單。 當(dāng)DataSource從連接池中檢索連接時(shí),我需要指示DataSource測(cè)試連接。 如果此測(cè)試失敗,則連接將從池中刪除,并嘗試建立新的連接。 我還需要為DataSource提供一個(gè)查詢(xún),它可以用來(lái)測(cè)試連接。
最后(并非絕對(duì)必要,但對(duì)測(cè)試很有用),默認(rèn)情況下,我的DataSource實(shí)現(xiàn)僅每30秒測(cè)試一次連接。 但是,我的測(cè)試可以在不到30秒的時(shí)間內(nèi)運(yùn)行。 最終,這段時(shí)間的長(zhǎng)度并沒(méi)有真正意義,因此我添加了一個(gè)由屬性文件提供的驗(yàn)證間隔。
這是我更新后的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; }關(guān)于編寫(xiě)集成測(cè)試的最后一點(diǎn)說(shuō)明。 最初,我創(chuàng)建了一個(gè)測(cè)試配置文件,該文件用于配置要在測(cè)試中使用的DataSource 。 但是,這是不正確的。
問(wèn)題是,如果有人要從生產(chǎn)配置文件中刪除我的修訂,但將其保留在測(cè)試配置文件中,則我的測(cè)試仍會(huì)通過(guò),但是我的實(shí)際生產(chǎn)代碼將再次受到我這段時(shí)間花的問(wèn)題的侵害。定影! 這是一個(gè)容易想象的錯(cuò)誤。 因此,在編寫(xiě)集成測(cè)試時(shí),請(qǐng)確保使用實(shí)際的生產(chǎn)配置文件。
自動(dòng)化測(cè)試
因此,即將結(jié)束。 我有一個(gè)測(cè)試用例,可以準(zhǔn)確地重現(xiàn)我在PROD中遇到的情況。 我有一個(gè)修復(fù)程序,然后使我的測(cè)試從失敗到通過(guò)。 但是,所有這些工作的重點(diǎn)不僅是讓我確信我的修訂適用于下一個(gè)版本,而且還適用于所有將來(lái)的版本。
Maven用戶(hù):希望您已經(jīng)熟悉surefire插件 。 或者,至少希望您的DevOps團(tuán)隊(duì)已經(jīng)設(shè)置了父pom,以便在構(gòu)建服務(wù)器上構(gòu)建項(xiàng)目時(shí),每次提交都會(huì)花費(fèi)您花時(shí)間編寫(xiě)的所有單元測(cè)試。
但是,本文不是關(guān)于編寫(xiě)單元測(cè)試的,而是關(guān)于編寫(xiě)集成測(cè)試的 。 集成測(cè)試套件的運(yùn)行時(shí)間(有時(shí)數(shù)小時(shí))通常比單元測(cè)試套件的時(shí)間(不超過(guò)5-10分鐘)長(zhǎng)得多。 集成測(cè)試通常也更容易波動(dòng)。 雖然我在本文中編寫(xiě)的集成測(cè)試應(yīng)該是穩(wěn)定的-如果它破裂了,應(yīng)該引起關(guān)注-在連接到開(kāi)發(fā)數(shù)據(jù)庫(kù)時(shí),您不能總是100%確信數(shù)據(jù)庫(kù)將可用或測(cè)試數(shù)據(jù)將是正確的甚至是存在的。 因此,失敗的集成測(cè)試并不一定意味著代碼不正確。
幸運(yùn)的是,Maven背后的人們已經(jīng)解決了這個(gè)問(wèn)題,那就是帶有故障安全插件 。 默認(rèn)情況下,surefire插件將查找Test之前或之后固定的類(lèi),而failsafe插件將查找IT (集成測(cè)試)之前或之后固定的類(lèi)。 像所有Maven插件一樣,您可以配置插件應(yīng)執(zhí)行的目標(biāo)。 這使您可以靈活地使每次代碼提交都運(yùn)行單元測(cè)試,而集成測(cè)試僅在夜間構(gòu)建時(shí)運(yùn)行。 這也可以防止需要部署修補(bǔ)程序但不存在集成測(cè)試所依賴(lài)的資源的情況。
最后的想法
編寫(xiě)集成測(cè)試既耗時(shí)又困難。 它需要廣泛考慮您的服務(wù)將如何與其他資源交互。 當(dāng)您專(zhuān)門(mén)測(cè)試故障場(chǎng)景時(shí),此過(guò)程甚至更加困難且耗時(shí),這通常需要對(duì)測(cè)試所連接的資源進(jìn)行更深入的控制,并借鑒過(guò)去的經(jīng)驗(yàn)和知識(shí)。
盡管花費(fèi)了大量的時(shí)間和精力,但這項(xiàng)投資將隨著時(shí)間的推移多次收回投資。 只有通過(guò)自動(dòng)測(cè)試才能提高對(duì)代碼正確性的信心,這對(duì)縮短開(kāi)發(fā)反饋周期至關(guān)重要。
我在本文中使用的代碼可以在這里找到: https : //github.com/wkorando/integration-test-example 。
翻譯自: https://www.javacodegeeks.com/2016/10/blood-sweat-writing-automated-integration-tests-failure-scenarios.html
總結(jié)
以上是生活随笔為你收集整理的针对故障场景的血液,汗液和书写自动集成测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 平板电脑怎么安装软件(平板电脑如何下载软
- 下一篇: maven插件编写_编写Maven插件的