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是继承于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:[email protected]">蔡源</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的源码,直接继承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:[email protected]">蔡源</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(); } }
现在我们可是在测试类中同时使用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:[email protected]">蔡源</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")); } }