byteman

我们的应用程序独立存在的时间已经很久了。 如今,应用程序是一种非常复杂的野兽,它们使用无数的API和协议相互通信,将数据存储在传统或NoSQL数据库中,通过网络发送消息和事件……例如,您多久考虑一次例如数据库的情况当您的应用程序正在主动查询时发生故障? 还是某个API端点突然开始拒绝连接? 将此类事故作为测试套件的一部分覆盖不是很好吗? 这就是故障注入和Byteman框架所要解决的问题。 例如,我们将构建一个现实的,功能完善的Spring应用程序,该应用程序使用Hibernate / JPA访问MySQL数据库并管理客户。 作为应用程序的JUnit集成测试套件的一部分,我们将包括三种测试用例:

  • 储存/寻找顾客
  • 存储客户并尝试在数据库宕机时查询数据库(故障模拟)
  • 存储客户和数据库查询超时(故障模拟)

在本地开发箱上运行应用程序只有两个先决条件:

  • MySQL服务器已安装并具有客户数据库
  • 已安装Oracle JDK ,并且JAVA_HOME环境变量指向它

话虽这么说,我们已经准备好出发了。 首先,让我们描述我们的域模型,该域模型由具有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;}
}

为简单起见,服务层与数据访问层混合在一起并直接调用数据库。 这是我们的CustomerService实现:

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应用程序上下文定义了数据源和事务管理器。 这里需要注意的一点是:由于我们不会引入数据访问层( @Repository )类,为了使Spring正确执行异常转换,我们将PersistenceExceptionTranslationPostProcessor实例定义为后处理服务类( @Service )。 其他一切都应该非常熟悉。

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() );}
}

现在,让我们添加一个简单的JUnit测试用例,以验证我们的Spring应用程序确实按预期工作。 在此之前,应创建数据库客户

> mysql -u root
mysql> create database customers;
Query OK, 1 row affected (0.00 sec)

这是一个CustomerServiceTestCase ,目前,它具有单个测试以创建客户并验证其是否已创建。

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() );}
}

看起来很简单明了。 现在,让我们考虑成功创建客户但由于查询超时而导致查找失败的情况。 为此,我们需要Byteman的帮助。 简而言之, Byteman是字节码操作框架。 这是一个Java代理实现,可与JVM一起运行(或附加到JVM)并修改正在运行的应用程序字节码,从而改变其行为。 Byteman有一个很好的文档,并且拥有丰富的规则定义集,可以执行开发人员可以想到的几乎所有事情。 而且,它与JUnit框架具有很好的集成。 在该主题上,应该使用@RunWith(BMUnitRunner.class)运行Byteman测试,但是我们已经在使用@RunWith(SpringJUnit4ClassRunner.class),并且JUnit不允许指定多个测试运行程序。 除非您熟悉JUnit @Rule机制,否则这似乎是一个问题。 事实证明,将BMUnitRunner转换为JUnit规则非常容易:

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 );

容易吧? 我们前面提到的场景可以改写一下:当执行从“客户”表中选择的JDBC语句执行时,我们应该因超时异常而失败。 这是带有附加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() );}

我们可以这样写:“当有人调用PreparedStatement类的executeQuery方法,并且查询以'SELECT'开始时,将抛出MySQLTimeoutException ,并且它应该只发生一次(由超时标志控制)”。 运行此测试用例将在控制台中打印stacktrace,并期望引发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]

看起来不错,还有另一种情况:创建客户成功但由于数据库关闭而失败了吗? 这一点比较复杂,但无论如何都很容易做,让我们看一下:

@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() );
}

让我解释一下这是怎么回事。 我们希望坐在套接字级别,并且实际上控制通讯尽可能地接近网络,而不是在JDBC驱动程序级别。 这就是为什么我们要检测AbstractPlainSocketImpl的原因。 我们也知道MySQL的默认端口是3306,因此我们仅检测在此端口上打开的套接字。 另一个事实,我们知道第一个创建的套接字与客户创建相对应,我们应该让它通过。 但是第二个对应于查找并且必须失败。 名为“ connection”createCountDown可以满足以下目的:第一次调用通过(闩锁尚未计数为零),但是第二次调用触发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]

大! 字节曼为不同故障模拟提供的可能性是巨大的。 仔细添加测试套件,以验证应用程序如何对错误的条件做出React,可以大大提高应用程序的健壮性和对故障的适应能力。 多亏了Byteman伙计们! 请在GitHub上找到完整的项目。

参考: 使用Byteman和JUnit进行故障注入:通过Andriy Redko {devmind}博客上的JCG合作伙伴 Andrey Redko,可以做更多的工作来确保应用程序的健壮性 。

翻译自: https://www.javacodegeeks.com/2013/04/fault-injection-with-byteman-and-junit.html

byteman

byteman_使用Byteman和JUnit进行故障注入相关推荐

  1. 使用Byteman和JUnit进行故障注入

    我们的应用程序独立存在的时间已经很久了. 如今,应用程序是一种非常复杂的野兽,它们使用无数的API和协议相互通信,将数据存储在传统或NoSQL数据库中,通过网络发送消息和事件--例如,如果考虑到数据库 ...

  2. Byteman –用于字节码操纵的瑞士军刀

    我正在JBoss的许多社区中工作,有很多有趣的事情要谈论,以至于我自己无法将自己的每一分都缠住. 这就是为什么我非常感谢有机会不时地欢迎客座博客的主要原因. 今天是Jochen Mader,他是以代码 ...

  3. JUnit单元测试依赖包构建路径错误解决办法

    JUnit单元测试依赖包构建路径错误解决办法: 选中报错的项目文件夹→右击选择属性(ALT+Enter)→java构建路径→库→添加库→JUnit→选择合适的Junit库版本.

  4. Junit的安装与使用

    一.简介: JUnit是一个Java语言的单元测试框架.它由Kent Beck和Erich Gamma建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中最为成功的一个. JUnit有它 ...

  5. JUnit基础及第一个单元测试实例(JUnit3.8)

    JUnit基础及第一个单元测试实例(JUnit3.8) 单元测试 单元测试(unit testing) ,是指对软件中的最小可测试单元进行检查和验证. 单元测试不是为了证明您是对的,而是为了证明您没有 ...

  6. junit集成Hamcrest测试集合中某个属性是否包含特定值

    junit已经集成Hamcrest但是还是需要引用hamcrest-library,不然只有基本方法,高级的没有 <dependency> <groupId>junit< ...

  7. 【Junit】BeforeClass、Before、After、AfterClass

    在JUnit4中,添加了Annotations来标记测试. 测试方法由@Test 标记说明.使用标记的好处是你不用将所有测试方法命名为testFoo()这种形式. 执行顺序: @BeforeClass ...

  8. linux 故障注入_阿里巴巴开源故障注入工具_chaosblade

    chaosblade是阿里巴巴最近开源的一款故障注入的工具,因为我最近在做公司的虚拟化平台的可靠性测试工具,无意中发现这个工具,个人感觉比较有用,用起来也比较简单,所以拿出来分享一下,期望对大家的工作 ...

  9. Junit单元测试需要知道的一些知识点

    Junit单元测试框架-基于java语言对的主流单元测试框架 @beforeClass-位于数据准备前期或者其他前期准备(测试类调用前) --用于提取代码中的共用部分减少冗余,只能声明注解一次 --必 ...

最新文章

  1. 【物联网智能网关-15】WAV播放器(WinForm+WavPlay库实例)
  2. 独立成分分析ICA系列3:直观解释与理解
  3. substr判断最后一个是不是逗号_用java帮助你判断一个数是不是回文数
  4. 焦旭超 201771010109《面向对象程序设计课程学习进度条》
  5. jdbcTemplate快速入门
  6. 坚实原则:依赖倒置原则
  7. 计算机网络主观论述题,《计算机网络》论述题
  8. 如何使用JMeter 对Dubbo接口进行测试
  9. oracle日期的sql,Oracle中一些和日期有关的SQL查询
  10. 《Android应用开发揭秘》读者问题汇总
  11. android开发:Android 中自定义属性(attr.xml,TypedArray)的使用
  12. 使用Python抓取网页信息
  13. Unity3D基础42:AnyState大法
  14. 无线网卡802.11n、 Intel 5100 AGN
  15. 计算机网络网络层之路由算法
  16. Python画新冠肺炎国内和世界各国累计确诊数量热图!某国破百万了
  17. 双币对冲外汇ea ,单货币对冲策略, EA运行原理
  18. 小学教育专业语文方向毕业论文怎么选题?
  19. wps里ppt怎么换另一个的模板_从没想过,这个基础的PPT数据图表,原来还是排版神器!...
  20. 《C语言深度剖析》学习笔记----C语言中的符号

热门文章

  1. MySQL instr()函数
  2. Java压缩技术(七) TAR——Commons实现
  3. 常用数据库连接池 (DBCP、c3p0、Druid) 配置说明
  4. 越努力越幸运,三年了!!!
  5. ssh(Spring+Spring mvc+hibernate)——Dept.hbm.xml
  6. 给定年月日计算是一年的第几天
  7. Java开发利器:IntelliJ IDEA的安装、配置与使用
  8. HDU2612(BFS算法)
  9. jax-ws和jax-rs_JAX-RS和OpenAPI对Hypermedia API的支持:任重而道远
  10. lambda表达式java_Lambda表达式Java教程