您的位置:首页 > Web前端

【让开发自动化】Unitils集成Feed4junit

2013-08-27 09:54 190 查看

简介

Feed4JUnit能够让编写JUnit的参数化测试变得简便,并为这些测试提供预定义或随机测试数据。它能够从业务分析人员定义好的CVS或 Excel文件读取测试用例数据并在构建/单元测试框架中报告测试成功。利用Feed4JUnit能够很方便用随机但校验过的数据执行冒烟测试来提高代码 代码覆盖率和发现由非常特殊的数据结构产生的Bug。

然而Feed4junit没有像spring-test或者unitils一样对spring集成的支持,无法通过事务控制实现方法执行完毕后对环境的回滚。如何结合Feef4junit和unitils的优势,将两者有机结合到一起,是本文需要解决的问题。

方案

UnitilsJUnit4需要在Class上定义@RunWith(UnitilsJUnit4TestClassRunner.class)而Feed4junit需要在Class上定义@RunWith(Feeder.class),默认情况两者无法并存,需要我们对其实现进行改写。改写的方式有两种:一种是用最新的@Rule改写,一种是将两者的代码合并到一个类里。从省力的角度,我决定采用后面一种方案。

改写UnitilsJUnit4TestClassRunner

UnitilsJUnit4TestClassRunner是继承于JUnit4ClassRunner的,而Feeder继承于新的BlockJUnit4ClassRunner,要支持带参数的测试方法,首先是将UnitilsJUnit4TestClassRunner改造成BlockJUnit4ClassRunner。

package org.unitils;

import java.util.List;

import org.junit.internal.runners.statements.InvokeMethod;
import org.junit.rules.MethodRule;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.unitils.core.TestListener;
import org.unitils.core.Unitils;

/**
*
* Unitils基于BlockJUnit4ClassRunner的改写.
*
* <pre><b>描述:</b>
*    升级为BlockJUnit4ClassRunner
* </pre>
*
* <pre><b>修改记录:</b>
*
* </pre>
*
* @author <a href="mailto:littcai@hotmail.com">蔡源</a>
* @since 2013-8-12
* @version 1.0
*
*/
public class UnitilsJUnitBlockRunner extends BlockJUnit4ClassRunner
{
TestListener listener;

public UnitilsJUnitBlockRunner(Class<?> aClass) throws InitializationError {
super(aClass);
listener = Unitils.getInstance().getTestListener();
}

@Override
protected Statement classBlock(RunNotifier runNotifier) {
listener.beforeTestClass(getTestClass().getClass());
return super.classBlock(runNotifier);
}

@Override
protected Object createTest() throws Exception {
Object o = super.createTest();
listener.afterCreateTestObject(o);
return o;
}

@Override
protected Statement methodInvoker(final FrameworkMethod frameworkMethod, final Object o) {
return new InvokeMethod(frameworkMethod, o) {
@Override
public void evaluate() throws Throwable {
listener.beforeTestMethod(o, frameworkMethod.getMethod());
Throwable threw = null;
try {
super.evaluate();
} catch (Throwable t) {
threw = t;
} finally {
listener.afterTestMethod(o, frameworkMethod.getMethod(), threw);
}
if(threw!=null)
throw threw;
}
};
}

@Override
protected List<MethodRule> rules(Object o) {
List<MethodRule> list = super.rules(o);
list.add(new MethodRule() {
public Statement apply(final Statement nextStatement, final FrameworkMethod frameworkMethod, final Object o) {
return new Statement() {
public void evaluate() throws Throwable {
listener.beforeTestSetUp(o, frameworkMethod.getMethod());
nextStatement.evaluate();
listener.afterTestTearDown(o, frameworkMethod.getMethod());
}
};
}
});
return list;
}
}


改造Feeder

复制Feeder的源码,直接继承UnitilsJUnitBlockRunner。

package org.databene.feed4junit;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.databene.benerator.Generator;
import org.databene.benerator.anno.AnnotationMapper;
import org.databene.benerator.anno.DefaultPathResolver;
import org.databene.benerator.anno.PathResolver;
import org.databene.benerator.anno.ThreadPoolSize;
import org.databene.benerator.engine.BeneratorContext;
import org.databene.benerator.engine.DefaultBeneratorContext;
import org.databene.benerator.factory.EquivalenceGeneratorFactory;
import org.databene.benerator.wrapper.ProductWrapper;
import org.databene.commons.ConfigurationError;
import org.databene.commons.IOUtil;
import org.databene.commons.Period;
import org.databene.commons.StringUtil;
import org.databene.commons.converter.AnyConverter;
import org.databene.feed4junit.ChildRunner;
import org.databene.feed4junit.FrameworkMethodWithParameters;
import org.databene.feed4junit.Scheduler;
import org.databene.feed4junit.scheduler.DefaultFeedScheduler;
import org.databene.model.data.DataModel;
import org.databene.platform.java.BeanDescriptorProvider;
import org.databene.platform.java.Entity2JavaConverter;
import org.databene.script.DatabeneScriptParser;
import org.databene.script.Expression;
import org.junit.Test;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerScheduler;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
import org.unitils.UnitilsJUnitBlockRunner;

/**
*
*
* Feeder整合Unitils.
*
* <pre><b>描述:</b>
*    结合Unitils对容器的管理和Feed4JUnit的冒烟测试
* </pre>
*
* <pre><b>修改记录:</b>
*
* </pre>
*
* @author <a href="mailto:littcai@hotmail.com">蔡源</a>
* @since 2013-8-12
* @version 1.0
*
*/
public class Feeder extends UnitilsJUnitBlockRunner
{
public static final String CONFIG_FILENAME_PROPERTY = "feed4junit.properties";
private static final String DEFAULT_CONFIG_FILENAME = "feed4junit.properties";
private static final String FEED4JUNIT_BASE_PATH = "feed4junit.basepath";

private static final long DEFAULT_TIMEOUT = Period.WEEK.getMillis();

static {
ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
}

private BeneratorContext context;
private PathResolver pathResolver;
private AnnotationMapper annotationMapper;
private List<FrameworkMethod> children;
private RunnerScheduler scheduler;

public Feeder(Class<?> aClass) throws InitializationError
{
super(aClass);
this.children = null;
}

@Override
protected String testName(FrameworkMethod method) {
return (method instanceof FrameworkMethodWithParameters ? method.toString() : super.testName(method));
}

@Override
public void setScheduler(RunnerScheduler scheduler) {
this.scheduler = scheduler;
super.setScheduler(scheduler);
}

/**
* Instantiates a test class and initializes attributes
* which have been marked with a @Source annotation.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
protected Object createTest() throws Exception {
Object testObject = super.createTest();
for (FrameworkField attribute : getTestClass().getAnnotatedFields(org.databene.benerator.anno.Source.class)) {
if ((attribute.getField().getModifiers() & Modifier.PUBLIC) == 0)
throw new ConfigurationError("Attribute '" + attribute.getField().getName() + "' must be public");
Generator<?> generator = getAnnotationMapper().createAndInitAttributeGenerator(attribute.getField(), getContext());
if (generator != null) {
ProductWrapper wrapper = new ProductWrapper();
wrapper = generator.generate(wrapper);
if (wrapper != null)
attribute.getField().set(testObject, wrapper.unwrap());
}
}
return testObject;
}

@Override
protected List<FrameworkMethod> computeTestMethods() {
if (children == null) {
children = new ArrayList<FrameworkMethod>();
TestClass testClass = getTestClass();
BeneratorContext context = getContext();
context.setGeneratorFactory(new EquivalenceGeneratorFactory());
getAnnotationMapper().parseClassAnnotations(testClass.getAnnotations(), context);
for (FrameworkMethod method : testClass.getAnnotatedMethods(Test.class)) {
if (method.getMethod().getParameterTypes().length == 0) {
// standard JUnit test method
children.add(method);
continue;
} else {
// parameterized Feed4JUnit test method
List<? extends FrameworkMethod> parameterizedTestMethods;
parameterizedTestMethods = computeParameterizedTestMethods(method.getMethod(), context);
children.addAll(parameterizedTestMethods);
}
}
}
return children;
}

@Override
protected void validateTestMethods(List<Throwable> errors) {
validatePublicVoidMethods(Test.class, false, errors);
}

// test execution --------------------------------------------------------------------------------------------------

protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}

private void runChildren(final RunNotifier notifier) {
RunnerScheduler scheduler = getScheduler();
for (FrameworkMethod method : getChildren())
scheduler.schedule(new ChildRunner(this, method, notifier));
scheduler.finished();
}

public RunnerScheduler getScheduler() {
if (scheduler == null)
scheduler = createDefaultScheduler();
return scheduler;
}

protected RunnerScheduler createDefaultScheduler() {
TestClass testClass = getTestClass();
Scheduler annotation = testClass.getJavaClass().getAnnotation(Scheduler.class);
if (annotation != null) {
String spec = annotation.value();
Expression<?> bean = DatabeneScriptParser.parseBeanSpec(spec);
return (RunnerScheduler) bean.evaluate(null);
} else {
return new DefaultFeedScheduler(1, DEFAULT_TIMEOUT);
}
}

@Override
public void runChild(FrameworkMethod method, RunNotifier notifier) {
super.runChild(method, notifier);
}

// helpers ---------------------------------------------------------------------------------------------------------

private PathResolver configuredPathResolver() {
if (pathResolver != null)
return pathResolver;
String configuredConfigFileName = System.getProperty(CONFIG_FILENAME_PROPERTY);
String configFileName = configuredConfigFileName;
if (StringUtil.isEmpty(configFileName))
configFileName = DEFAULT_CONFIG_FILENAME;
if (IOUtil.isURIAvailable(configFileName)) {
// load individual or configured config file
return configuredPathResolver(configFileName);
} else if (StringUtil.isEmpty(configuredConfigFileName)) {
// if no explicit config file was configured, then use defaults...
return createDefaultResolver();
} else {
// ...otherwise raise an exception
throw new ConfigurationError("Feed4JUnit configuration file not found: " + configuredConfigFileName);
}
}

private PathResolver createDefaultResolver() {
return applyBasePath(new DefaultPathResolver());
}

private PathResolver configuredPathResolver(String configFileName) {
try {
Map<String, String> properties = IOUtil.readProperties(configFileName);
String pathResolverSpec = properties.get("pathResolver");
if (pathResolverSpec != null) {
PathResolver resolver;
resolver =  (PathResolver) DatabeneScriptParser.parseBeanSpec(pathResolverSpec).evaluate(getContext());
return applyBasePath(resolver);
} else
return createDefaultResolver();
} catch (IOException e) {
throw new ConfigurationError("Error reading config file '" + configFileName + "'", e);
}
}

private PathResolver applyBasePath(PathResolver resolver) {
String confdBasePath = System.getProperty(FEED4JUNIT_BASE_PATH);
if (confdBasePath != null)
resolver.setBasePath(confdBasePath);
return resolver;
}

private void validatePublicVoidMethods(Class<? extends Annotation> annotation, boolean isStatic, List<Throwable> errors) {
List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(annotation);
for (FrameworkMethod eachTestMethod : methods)
eachTestMethod.validatePublicVoid(isStatic, errors);
}

private List<FrameworkMethodWithParameters> computeParameterizedTestMethods(Method method, BeneratorContext context) {
Integer threads = getThreadCount(method);
long timeout = getTimeout(method);
List<FrameworkMethodWithParameters> result = new ArrayList<FrameworkMethodWithParameters>();
Class<?>[] parameterTypes = method.getParameterTypes();
Generator<Object[]> paramGenerator = getAnnotationMapper().createAndInitMethodParamsGenerator(method, context);
Class<?>[] expectedTypes = parameterTypes;
ProductWrapper<Object[]> wrapper = new ProductWrapper<Object[]>();
int count = 0;
while ((wrapper = paramGenerator.generate(wrapper)) != null) {
Object[] generatedParams = wrapper.unwrap();
if (generatedParams.length > expectedTypes.length) // imported data may have more columns than the method parameters, ...
generatedParams = Arrays.copyOfRange(generatedParams, 0, expectedTypes.length); // ...so cut them
for (int i = 0; i < generatedParams.length; i++) {
generatedParams[i] = Entity2JavaConverter.convertAny(generatedParams[i]);
generatedParams[i] = AnyConverter.convert(generatedParams[i], parameterTypes[i]);
}
// generated params may be to few, e.g. if an XLS row was imported with trailing nulls,
// so create an array of appropriate size
Object[] usedParams = new Object[parameterTypes.length];
System.arraycopy(generatedParams, 0, usedParams, 0, Math.min(generatedParams.length, usedParams.length));
result.add(new FrameworkMethodWithParameters(method, usedParams, threads, timeout));
count++;
}
if (count == 0)
throw new RuntimeException("No parameter values available for method: " + method);
return result;
}

private Integer getThreadCount(Method method) {
ThreadPoolSize methodAnnotation = method.getAnnotation(ThreadPoolSize.class);
if (methodAnnotation != null)
return methodAnnotation.value();
Class<?> testClass = method.getDeclaringClass();
ThreadPoolSize classAnnotation = testClass.getAnnotation(ThreadPoolSize.class);
if (classAnnotation != null)
return classAnnotation.value();
return null;
}

private long getTimeout(Method method) {
return DEFAULT_TIMEOUT;
}

private AnnotationMapper getAnnotationMapper() {
// lazy initialization is necessary since the constructor is not executed by JUnit
if (annotationMapper == null) {
PathResolver pathResolver = configuredPathResolver();
annotationMapper = new AnnotationMapper(new EquivalenceGeneratorFactory(), getDataModel(), pathResolver);
}
return annotationMapper;
}

private BeneratorContext getContext() {
// lazy initialization is necessary since the constructor is not executed by JUnit
if (context == null) {
context = new DefaultBeneratorContext();
DataModel dataModel = context.getDataModel();
new BeanDescriptorProvider(dataModel);
}
return context;
}

private DataModel getDataModel() {
return getContext().getDataModel();
}

}


Example

现在我们可是在测试类中同时使用Feeder的注解和Unitils的注解了,下面的列子是通过feed4junit从Excel文件中读取测试案例,并对每个案例执行save方法的测试,测试类通过Spring容器管理,在测试结束后事务将自动回滚,还原测试环境。

有了带冒烟的集成测试方法,妈妈再也不用担心我的代码质量了。

package com.litt.cidp.system.service;

import java.util.Map;

import org.databene.benerator.anno.Source;
import org.databene.feed4junit.Feeder;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.jdbc.core.JdbcTemplate;
import org.unitils.spring.annotation.SpringBeanByType;

import com.litt.cidp.system.po.Role;
import com.litt.core.test.BaseServiceTester;

/**
*
* 冒烟测试.
*
* <pre><b>描述:</b>
*
* </pre>
*
* <pre><b>修改记录:</b>
*
* </pre>
*
* @author <a href="mailto:littcai@hotmail.com">蔡源</a>
* @since 2013-8-9
* @version 1.0
*
*/
@SpringApplicationContext("spring/applicationContext-*.xml")
@RunWith(Feeder.class)
public class RoleServiceSmokeTest
{
@SpringBeanByType
private IRoleService roleService;

@SpringBeanByType
private JdbcTemplate jdbcTemplate;

@Test
@Source("com/litt/cidp/system/service/Role-smoke-data.xls")
public void save(String roleName, String remark) {

Role role = new Role();
role.setRoleName(roleName);
role.setRemark(remark);

roleService.save(role);

this.validate(role);
}

private void validate(Role role)
{
Map<String, Object> rsMap = jdbcTemplate.queryForMap("SELECT * FROM ROLE WHERE ROLE_NAME=?", new Object[]{role.getRoleName()});

Assert.assertEquals(role.getRemark(), rsMap.get("REMARK"));
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Unitils Feed4junit