使用Byteman和JUnit进行故障注入
我們的應(yīng)用程序獨立存在的時間已經(jīng)很久了。 如今,應(yīng)用程序是一種非常復(fù)雜的野獸,它們使用無數(shù)的API和協(xié)議相互通信,將數(shù)據(jù)存儲在傳統(tǒng)或NoSQL數(shù)據(jù)庫中,通過網(wǎng)絡(luò)發(fā)送消息和事件……例如,如果考慮到數(shù)據(jù)庫,您會多久考慮會發(fā)生什么?當(dāng)您的應(yīng)用程序正在主動查詢時發(fā)生故障? 還是某個API端點突然開始拒絕連接? 將此類事故作為測試套件的一部分覆蓋不是很好嗎? 這就是故障注入和Byteman框架所要解決的問題。 例如,我們將構(gòu)建一個現(xiàn)實的,功能完善的Spring應(yīng)用程序,該應(yīng)用程序使用Hibernate / JPA訪問MySQL數(shù)據(jù)庫并管理客戶。 作為應(yīng)用程序的JUnit集成測試套件的一部分,我們將包括三種測試用例:
- 儲存/尋找顧客
- 存儲客戶并嘗試在數(shù)據(jù)庫宕機時查詢數(shù)據(jù)庫(故障模擬)
- 存儲客戶和數(shù)據(jù)庫查詢超時(故障模擬)
在本地開發(fā)箱上運行應(yīng)用程序只有兩個先決條件:
- MySQL服務(wù)器已安裝并具有客戶數(shù)據(jù)庫
- Oracle JDK已安裝,并且JAVA_HOME環(huán)境變量指向它
話雖這么說,我們已經(jīng)準(zhǔn)備好出發(fā)了。 首先,讓我們描述我們的域模型,該域模型由具有ID和單個屬性名稱的單個Customer類組成。 看起來很簡單:
package com.example.spring.domain;import java.io.Serializable;import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table;@Entity @Table( name = "customers" ) public class Customer implements Serializable{private static final long serialVersionUID = 1L;@Id@GeneratedValue@Column(name = "id", unique = true, nullable = false)private long id;@Column(name = "name", nullable = false)private String name;public Customer() {}public Customer( final String name ) {this.name = name;}public long getId() {return this.id;}protected void setId( final long id ) {this.id = id;}public String getName() {return this.name;}public void setName( final String name ) {this.name = name;} }為簡單起見,服務(wù)層與數(shù)據(jù)訪問層混合在一起并直接調(diào)用數(shù)據(jù)庫。 這是我們的CustomerService實現(xiàn):
package com.example.spring.services;import javax.persistence.EntityManager; import javax.persistence.PersistenceContext;import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import com.example.spring.domain.Customer;@Service public class CustomerService {@PersistenceContext private EntityManager entityManager;@Transactional( readOnly = true )public Customer find( long id ) {return this.entityManager.find( Customer.class, id );}@Transactional( readOnly = false )public Customer create( final String name ) {final Customer customer = new Customer( name );this.entityManager.persist(customer);return customer;}@Transactional( readOnly = false )public void deleteAll() {this.entityManager.createQuery( "delete from Customer" ).executeUpdate();} }最后, Spring應(yīng)用程序上下文定義了數(shù)據(jù)源和事務(wù)管理器。 這里需要注意的一點是:由于我們不會引入數(shù)據(jù)訪問層( @Repository )類,為了使Spring正確執(zhí)行異常轉(zhuǎn)換,我們將PersistenceExceptionTranslationPostProcessor實例定義為后處理服務(wù)類( @Service )。 其他一切都應(yīng)該非常熟悉。
package com.example.spring.config;import java.util.Properties;import javax.sql.DataSource;import org.hibernate.dialect.MySQL5InnoDBDialect; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement;import com.example.spring.services.CustomerService;@EnableTransactionManagement @Configuration @ComponentScan( basePackageClasses = CustomerService.class ) public class AppConfig {@Beanpublic PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor() {final PersistenceExceptionTranslationPostProcessor processor = new PersistenceExceptionTranslationPostProcessor();processor.setRepositoryAnnotationType( Service.class );return processor;}@Beanpublic HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {final HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();adapter.setDatabase( Database.MYSQL );adapter.setShowSql( false );return adapter;}@Beanpublic LocalContainerEntityManagerFactoryBean entityManager() throws Throwable {final LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();entityManager.setPersistenceUnitName( "customers" );entityManager.setDataSource( dataSource() );entityManager.setJpaVendorAdapter( hibernateJpaVendorAdapter() );final Properties properties = new Properties();properties.setProperty("hibernate.dialect", MySQL5InnoDBDialect.class.getName());properties.setProperty("hibernate.hbm2ddl.auto", "create-drop" );entityManager.setJpaProperties( properties );return entityManager;}@Beanpublic DataSource dataSource() {final DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setDriverClassName( com.mysql.jdbc.Driver.class.getName() );dataSource.setUrl( "jdbc:mysql://localhost/customers?enableQueryTimeouts=true" );dataSource.setUsername( "root" );dataSource.setPassword( "" );return dataSource;}@Beanpublic PlatformTransactionManager transactionManager() throws Throwable {return new JpaTransactionManager( this.entityManager().getObject() );} }現(xiàn)在,讓我們添加一個簡單的JUnit測試用例,以驗證我們的Spring應(yīng)用程序確實按預(yù)期工作。 在此之前,應(yīng)創(chuàng)建數(shù)據(jù)庫客戶 :
> mysql -u root mysql> create database customers; Query OK, 1 row affected (0.00 sec)這是一個CustomerServiceTestCase ,目前,它具有用于創(chuàng)建客戶并驗證其是否已創(chuàng)建的單個測試。
package com.example.spring;import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat;import javax.inject.Inject;import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader;import com.example.spring.config.AppConfig; import com.example.spring.domain.Customer; import com.example.spring.services.CustomerService;@RunWith( SpringJUnit4ClassRunner.class ) @ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = { AppConfig.class } ) public class CustomerServiceTestCase {@Inject private CustomerService customerService; @Afterpublic void tearDown() {customerService.deleteAll();}@Testpublic void testCreateCustomerAndVerifyItHasBeenCreated() throws Exception {Customer customer = customerService.create( "Customer A" );assertThat( customerService.find( customer.getId() ), notNullValue() );} }看起來很簡單明了。 現(xiàn)在,讓我們考慮成功創(chuàng)建客戶但由于查詢超時而導(dǎo)致查找失敗的情況。 為此,我們需要Byteman的幫助。 簡而言之, Byteman是字節(jié)碼操作框架。 這是一個Java代理實現(xiàn),可與JVM一起運行(或附加到JVM)并修改正在運行的應(yīng)用程序字節(jié)碼,從而改變其行為。 Byteman有一個很好的文檔,并且擁有豐富的規(guī)則定義集,可以執(zhí)行大多數(shù)開發(fā)人員可以想到的一切。 而且,它與JUnit框架具有很好的集成。 在該主題上,應(yīng)該使用@RunWith(BMUnitRunner.class)運行Byteman測試,但是我們已經(jīng)使用@RunWith(SpringJUnit4ClassRunner.class),并且JUnit不允許指定多個測試運行程序。 除非您熟悉JUnit @Rule機制,否則這似乎是一個問題。 事實證明,將BMUnitRunner轉(zhuǎn)換為JUnit規(guī)則非常簡單:
package com.example.spring;import org.jboss.byteman.contrib.bmunit.BMUnitRunner; import org.junit.rules.MethodRule; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement;public class BytemanRule extends BMUnitRunner implements MethodRule {public static BytemanRule create( Class< ? > klass ) {try {return new BytemanRule( klass ); } catch( InitializationError ex ) { throw new RuntimeException( ex ); }}private BytemanRule( Class klass ) throws InitializationError {super( klass );}@Overridepublic Statement apply( final Statement statement, final FrameworkMethod method, final Object target ) {Statement result = addMethodMultiRuleLoader( statement, method ); if( result == statement ) {result = addMethodSingleRuleLoader( statement, method );}return result;} }JUnit @Rule注入就這么簡單:
@Rule public BytemanRule byteman = BytemanRule.create( CustomerServiceTestCase.class );容易吧? 我們前面提到的場景可以改寫一下:當(dāng)執(zhí)行從“客戶”表中選擇的JDBC語句執(zhí)行時,我們應(yīng)該因超時異常而失敗。 這是帶有附加Byteman批注的JUnit測試用例的外觀:
@Test( expected = DataAccessException.class )@BMRule(name = "introduce timeout while accessing MySQL database",targetClass = "com.mysql.jdbc.PreparedStatement",targetMethod = "executeQuery",targetLocation = "AT ENTRY",condition = "$0.originalSql.startsWith( \"select\" ) && !flagged( \"timeout\" )",action = "flag( \"timeout\" ); throw new com.mysql.jdbc.exceptions.MySQLTimeoutException( \"Statement timed out (simulated)\" )")public void testCreateCustomerWhileDatabaseIsTimingOut() {Customer customer = customerService.create( "Customer A" );customerService.find( customer.getId() );}我們可以這樣寫:“當(dāng)有人調(diào)用PreparedStatement類的executeQuery方法并且查詢以'SELECT'開始時,將拋出MySQLTimeoutException ,并且它應(yīng)該只發(fā)生一次(由超時標(biāo)志控制)”。 運行此測試用例將在控制臺中打印stacktrace,并期望引發(fā)DataAccessException :
com.mysql.jdbc.exceptions.MySQLTimeoutException: Statement timed out (simulated)at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.7.0_21]at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) ~[na:1.7.0_21]at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.7.0_21]at java.lang.reflect.Constructor.newInstance(Constructor.java:525) ~[na:1.7.0_21]at org.jboss.byteman.rule.expression.ThrowExpression.interpret(ThrowExpression.java:231) ~[na:na]at org.jboss.byteman.rule.Action.interpret(Action.java:144) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.fire(InterpretedHelper.java:169) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.execute0(InterpretedHelper.java:137) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.execute(InterpretedHelper.java:100) ~[na:na]at org.jboss.byteman.rule.Rule.execute(Rule.java:682) ~[na:na]at org.jboss.byteman.rule.Rule.execute(Rule.java:651) ~[na:na]at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java) ~[mysql-connector-java-5.1.24.jar:na]at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:56) ~[hibernate-core-4.2.0.Final.jar:4.2.0.Final]at org.hibernate.loader.Loader.getResultSet(Loader.java:2031) [hibernate-core-4.2.0.Final.jar:4.2.0.Final]看起來不錯,還有另一種情況:創(chuàng)建客戶成功但由于數(shù)據(jù)庫關(guān)閉而導(dǎo)致查找失敗? 這一點比較復(fù)雜,但無論如何都很容易做,讓我們看一下:
@Test( expected = CannotCreateTransactionException.class ) @BMRules(rules = {@BMRule(name="create countDown for AbstractPlainSocketImpl",targetClass = "java.net.AbstractPlainSocketImpl",targetMethod = "getOutputStream",condition = "$0.port==3306",action = "createCountDown( \"connection\", 1 )"),@BMRule(name = "throw IOException when trying to execute 2nd query to MySQL",targetClass = "java.net.AbstractPlainSocketImpl",targetMethod = "getOutputStream",condition = "$0.port==3306 && countDown( \"connection\" )",action = "throw new java.io.IOException( \"Connection refused (simulated)\" )")} ) public void testCreateCustomerAndTryToFindItWhenDatabaseIsDown() {Customer customer = customerService.create( "Customer A" );customerService.find( customer.getId() ); }讓我解釋一下這是怎么回事。 我們希望坐在套接字級別,并且實際上控制通訊盡可能地接近網(wǎng)絡(luò),而不是在JDBC驅(qū)動程序級別。 這就是為什么我們要檢測AbstractPlainSocketImpl的原因。 我們也知道MySQL的默認(rèn)端口是3306,因此我們僅檢測在此端口上打開的套接字。 另一個事實,我們知道第一個創(chuàng)建的套接字與客戶創(chuàng)建相對應(yīng),我們應(yīng)該讓它通過。 但是第二個對應(yīng)于查找并且必須失敗。 名為“ connection”的createCountDown達(dá)到了此目的:第一次調(diào)用通過(閂鎖尚未計數(shù)為零),但是第二個調(diào)用觸發(fā)MySQLTimeoutException異常。 運行此測試用例將在控制臺中打印stacktrace,并期望拋出CannotCreateTransactionException :
Caused by: java.io.IOException: Connection refused (simulated)at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.7.0_21]at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) ~[na:1.7.0_21]at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.7.0_21]at java.lang.reflect.Constructor.newInstance(Constructor.java:525) ~[na:1.7.0_21]at org.jboss.byteman.rule.expression.ThrowExpression.interpret(ThrowExpression.java:231) ~[na:na]at org.jboss.byteman.rule.Action.interpret(Action.java:144) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.fire(InterpretedHelper.java:169) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.execute0(InterpretedHelper.java:137) ~[na:na]at org.jboss.byteman.rule.helper.InterpretedHelper.execute(InterpretedHelper.java:100) ~[na:na]at org.jboss.byteman.rule.Rule.execute(Rule.java:682) ~[na:na]at org.jboss.byteman.rule.Rule.execute(Rule.java:651) ~[na:na]at java.net.AbstractPlainSocketImpl.getOutputStream(AbstractPlainSocketImpl.java) ~[na:1.7.0_21]at java.net.PlainSocketImpl.getOutputStream(PlainSocketImpl.java:214) ~[na:1.7.0_21]at java.net.Socket$3.run(Socket.java:915) ~[na:1.7.0_21]at java.net.Socket$3.run(Socket.java:913) ~[na:1.7.0_21]at java.security.AccessController.doPrivileged(Native Method) ~[na:1.7.0_21]at java.net.Socket.getOutputStream(Socket.java:912) ~[na:1.7.0_21]at com.mysql.jdbc.MysqlIO.(MysqlIO.java:330) ~[mysql-connector-java-5.1.24.jar:na] 大! 字節(jié)曼為不同故障仿真提供的可能性是巨大的。 仔細(xì)添加測試套件,以驗證應(yīng)用程序如何對錯誤的條件做出反應(yīng),從而大大提高了應(yīng)用程序的健壯性和對故障的適應(yīng)能力。 多虧了Byteman伙計們! 請在GitHub上找到完整的項目。
翻譯自: https://www.javacodegeeks.com/2013/04/fault-injection-with-byteman-and-junit.html
總結(jié)
以上是生活随笔為你收集整理的使用Byteman和JUnit进行故障注入的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 房屋买卖备案登记(房屋买卖备案)
- 下一篇: Arrays.sort与Arrays.p