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

简介

    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:[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

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


}

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:[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"));
	}

}


你可能感兴趣的:(unitils,Feed4Junit)