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 {
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor() {
final PersistenceExceptionTranslationPostProcessor processor = new PersistenceExceptionTranslationPostProcessor();
processor.setRepositoryAnnotationType( Service.class );
return processor;
}
@Bean
public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
final HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabase( Database.MYSQL );
adapter.setShowSql( false );
return adapter;
}
@Bean
public 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;
}
@Bean
public 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;
}
@Bean
public 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;
@After
public void tearDown() {
customerService.deleteAll();
}
@Test
public 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 );
}
@Override
public 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上找到完整的项目。
翻译自: https://www.javacodegeeks.com/2013/04/fault-injection-with-byteman-and-junit.html
byteman