byteman_使用Byteman和JUnit进行故障注入
byteman
我們的應(yīng)用程序獨(dú)立存在的時(shí)間已經(jīng)很久了。 如今,應(yīng)用程序是一種非常復(fù)雜的野獸,它們使用無(wú)數(shù)的API和協(xié)議相互通信,將數(shù)據(jù)存儲(chǔ)在傳統(tǒng)或NoSQL數(shù)據(jù)庫(kù)中,通過(guò)網(wǎng)絡(luò)發(fā)送消息和事件……例如,您多久考慮一次例如數(shù)據(jù)庫(kù)的情況當(dāng)您的應(yīng)用程序正在主動(dòng)查詢時(shí)發(fā)生故障? 還是某個(gè)API端點(diǎn)突然開(kāi)始拒絕連接? 將此類事故作為測(cè)試套件的一部分覆蓋不是很好嗎? 這就是故障注入和Byteman框架所要解決的問(wèn)題。 例如,我們將構(gòu)建一個(gè)現(xiàn)實(shí)的,功能完善的Spring應(yīng)用程序,該應(yīng)用程序使用Hibernate / JPA訪問(wèn)MySQL數(shù)據(jù)庫(kù)并管理客戶。 作為應(yīng)用程序的JUnit集成測(cè)試套件的一部分,我們將包括三種測(cè)試用例:
- 儲(chǔ)存/尋找顧客
- 存儲(chǔ)客戶并嘗試在數(shù)據(jù)庫(kù)宕機(jī)時(shí)查詢數(shù)據(jù)庫(kù)(故障模擬)
- 存儲(chǔ)客戶和數(shù)據(jù)庫(kù)查詢超時(shí)(故障模擬)
在本地開(kāi)發(fā)箱上運(yùn)行應(yīng)用程序只有兩個(gè)先決條件:
- MySQL服務(wù)器已安裝并具有客戶數(shù)據(jù)庫(kù)
- 已安裝Oracle JDK ,并且JAVA_HOME環(huán)境變量指向它
話雖這么說(shuō),我們已經(jīng)準(zhǔn)備好出發(fā)了。 首先,讓我們描述我們的域模型,該域模型由具有ID和單個(gè)屬性名的單個(gè)Customer類組成。 看起來(lái)很簡(jiǎn)單:
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;} }為簡(jiǎn)單起見(jiàn),服務(wù)層與數(shù)據(jù)訪問(wèn)層混合在一起并直接調(diào)用數(shù)據(jù)庫(kù)。 這是我們的CustomerService實(shí)現(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ù)管理器。 這里需要注意的一點(diǎn)是:由于我們不會(huì)引入數(shù)據(jù)訪問(wèn)層( @Repository )類,為了使Spring正確執(zhí)行異常轉(zhuǎn)換,我們將PersistenceExceptionTranslationPostProcessor實(shí)例定義為后處理服務(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)在,讓我們添加一個(gè)簡(jiǎn)單的JUnit測(cè)試用例,以驗(yàn)證我們的Spring應(yīng)用程序確實(shí)按預(yù)期工作。 在此之前,應(yīng)創(chuàng)建數(shù)據(jù)庫(kù)客戶 :
> mysql -u root mysql> create database customers; Query OK, 1 row affected (0.00 sec)這是一個(gè)CustomerServiceTestCase ,目前,它具有單個(gè)測(cè)試以創(chuàng)建客戶并驗(yàn)證其是否已創(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() );} }看起來(lái)很簡(jiǎn)單明了。 現(xiàn)在,讓我們考慮成功創(chuàng)建客戶但由于查詢超時(shí)而導(dǎo)致查找失敗的情況。 為此,我們需要Byteman的幫助。 簡(jiǎn)而言之, Byteman是字節(jié)碼操作框架。 這是一個(gè)Java代理實(shí)現(xiàn),可與JVM一起運(yùn)行(或附加到JVM)并修改正在運(yùn)行的應(yīng)用程序字節(jié)碼,從而改變其行為。 Byteman有一個(gè)很好的文檔,并且擁有豐富的規(guī)則定義集,可以執(zhí)行開(kāi)發(fā)人員可以想到的幾乎所有事情。 而且,它與JUnit框架具有很好的集成。 在該主題上,應(yīng)該使用@RunWith(BMUnitRunner.class)運(yùn)行Byteman測(cè)試,但是我們已經(jīng)在使用@RunWith(SpringJUnit4ClassRunner.class),并且JUnit不允許指定多個(gè)測(cè)試運(yùn)行程序。 除非您熟悉JUnit @Rule機(jī)制,否則這似乎是一個(gè)問(wèn)題。 事實(shí)證明,將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注入就這么簡(jiǎn)單:
@Rule public BytemanRule byteman = BytemanRule.create( CustomerServiceTestCase.class );容易吧? 我們前面提到的場(chǎng)景可以改寫(xiě)一下:當(dāng)執(zhí)行從“客戶”表中選擇的JDBC語(yǔ)句執(zhí)行時(shí),我們應(yīng)該因超時(shí)異常而失敗。 這是帶有附加Byteman批注的JUnit測(cè)試用例的外觀:
@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() );}我們可以這樣寫(xiě):“當(dāng)有人調(diào)用PreparedStatement類的executeQuery方法,并且查詢以'SELECT'開(kāi)始時(shí),將拋出MySQLTimeoutException ,并且它應(yīng)該只發(fā)生一次(由超時(shí)標(biāo)志控制)”。 運(yùn)行此測(cè)試用例將在控制臺(tái)中打印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]看起來(lái)不錯(cuò),還有另一種情況:創(chuàng)建客戶成功但由于數(shù)據(jù)庫(kù)關(guān)閉而失敗了嗎? 這一點(diǎn)比較復(fù)雜,但無(wú)論如何都很容易做,讓我們看一下:
@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() ); }讓我解釋一下這是怎么回事。 我們希望坐在套接字級(jí)別,并且實(shí)際上控制通訊盡可能地接近網(wǎng)絡(luò),而不是在JDBC驅(qū)動(dòng)程序級(jí)別。 這就是為什么我們要檢測(cè)AbstractPlainSocketImpl的原因。 我們也知道MySQL的默認(rèn)端口是3306,因此我們僅檢測(cè)在此端口上打開(kāi)的套接字。 另一個(gè)事實(shí),我們知道第一個(gè)創(chuàng)建的套接字與客戶創(chuàng)建相對(duì)應(yīng),我們應(yīng)該讓它通過(guò)。 但是第二個(gè)對(duì)應(yīng)于查找并且必須失敗。 名為“ connection”的createCountDown可以滿足以下目的:第一次調(diào)用通過(guò)(閂鎖尚未計(jì)數(shù)為零),但是第二次調(diào)用觸發(fā)MySQLTimeoutException異常。 運(yùn)行此測(cè)試用例將在控制臺(tái)中打印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ì)添加測(cè)試套件,以驗(yàn)證應(yīng)用程序如何對(duì)錯(cuò)誤的條件做出React,可以大大提高應(yīng)用程序的健壯性和對(duì)故障的適應(yīng)能力。 多虧了Byteman伙計(jì)們! 請(qǐng)?jiān)贕itHub上找到完整的項(xiàng)目。
翻譯自: https://www.javacodegeeks.com/2013/04/fault-injection-with-byteman-and-junit.html
byteman
總結(jié)
以上是生活随笔為你收集整理的byteman_使用Byteman和JUnit进行故障注入的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 登录样式:log4j 2,上下文,自动清
- 下一篇: 国产沃尔沃 EX30 现身工信部:提供双