`
hippoom
  • 浏览: 8371 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

在集成测试中使用Mock和Stub的几种方法

阅读更多

       由于“集成测试”这个术语被许多不同角色的人使用,可能对不同人代表了不同的意思,这里说的集成测试是指挑选出几个程序单元(通常包括外部系统)将它们装配起来并对它们进行测试。就我个人而言,经常使用集成测试的方式来测试持久化逻辑(比如调用Dao,验证其实现是否按照预想地操作了数据库)或是一些对象是否正确地被spring framework装配起来(比如一些添加在对象上的AOP advices是否起作用了)。

       一般来说,既然是集成测试,那么所有相关的程序都会在测试中执行,但某些特定情况下,需要在测试中使用测试替身(这可能是由于依赖的外部系统没有测试环境等原因)或是运行过程中,某个功能的执行需要花费很大力气去准备数据和环境,但其只是一个程序执行的必经点并不是要测试的核心点。这里,介绍几种使用spring framework启动applicationContext后,替换其中被管理的bean为测试替身的方法。

 

一、手工替换

       这里使用一些Toy code来做演示。整个测试使用spring-test框架搭建,它会在测试前自动启动一个按照classpath:config.xml配置的ApplicationContext,接下来可以通过(3)这样的代码从ApplicationContext中获得你需要测试的Bean进行测试。如果你熟悉JMock2的话,从(5)中你应该可以看出SomeApp.returnHelloWorld()依赖SomeInterface.isAvailable(),我们假设这其实现在测试中无法调用(我遇到过一个真实的例子就是一个在线付款的应用,每次调用其中一个供应商提供的接口,意味着需要使用银行卡支付一笔钱,而该供应商并不提供测试环境,我们也不希望每次运行该测试兜里就少掉1块钱)。于是我们在(4)手工的替换掉了该实现。

 

 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:config.xml")
public class SomeAppIntegrationTestsUsingManualReplacing {

	private Mockery context = new JUnit4Mockery();     (1)

	private SomeInterface mock = context.mock(SomeInterface.class);   (2)

	@Resource(name = "someApp")
	private SomeApp someApp;                  (3)

	@Before
	public void replaceDependenceWithMock() {
		someApp.setDependence(mock);          (4)
	}

	@Test
	public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {

		context.checking(new Expectations() {
			{
				allowing(mock).isAvailable();        
				will(returnValue(true));         (5)
			}
		});

		String actual = someApp.returnHelloWorld();
		assertEquals("helloWorld", actual);
		context.assertIsSatisfied(); (6)
	}
}

 这里要注意(6)这步非常重要,因为我们指定了@RunWith(SpringJUnit4ClassRunner.class),而不是@RunWith(JMock.class),所以一些原本由JMock自动处理的工作需要我们手工来完成,其中就包括调用assertIsSatisfied()来验证是否所有的预期都被调用了且没有未预期调用出现。

 

二、预先准备替换过的ApplicationContext

方案一已经基本达到了我们的目标,但还有些瑕疵:

1.如果需要替换多个对象,有时你可能不得不从ApplicationContext中取出多个对象,再依次通过setter()注入测试替身。这样比较麻烦,而且必须通过硬编码来完成注入(本来应该是由spring来完成的事)。

2.严格来说,这不能叫做替换,因为并不能保证,原来的对象已经被正确地注入到目标中去,比如你忘记编写<property name="beanName" ref="bean" />来完成注入(不知为什么,这件事情经常发生在我身上)。

那怎么能在已经按照生产环境配置装配ApplicationContext的情况下,替换测试替身呢?Spring为我们提供了一个解决方案BeanPostProcessor,根据参考手册中的说法,ApplicationContext会自动检测xml文件中定义的BeanPostProcessor实现,并使用它们来增强其他Bean。

 

public class PredefinedBeanPostProcessor implements BeanPostProcessor {

	public Mockery context = new JUnit4Mockery();    (1)

	public SomeInterface mock = context.mock(SomeInterface.class);   (2)

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName)
			throws BeansException {
		if ("dependence".equals(beanName)) {
			return mock;
		} else {
			return bean;
		}
	}
}

在这个实现中,我们定义了测试替身(1)(2),并在postProcessAfterInitialization()中根据beanName做了替换。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:config.xml",
		"classpath:predefined.xml" })   (1)
public class SomeAppIntegrationTestsUsingPredefinedReplacing {

	@Resource(name = "someApp")
	private SomeApp someApp;

	@Resource(name = "predefined")
	private PredefinedBeanPostProcessor fixture;

	@Test
	public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {

		fixture.context.checking(new Expectations() {
			{
				allowing(fixture.mock).isAvailable();
				will(returnValue(true));
			}
		});

		String actual = someApp.returnHelloWorld();
		assertEquals("helloWorld", actual);
		fixture.context.assertIsSatisfied();
	}
}

请注意这个版本的测试,我们在(1)中额外定义了一个xml文件,在其中定义了PredefinedBeanPostProcessor,这样我们可以在生产环境的二进制包中比较简单的去除掉该文件其PredefinedBeanPostProcessor(比如将predefined.xml放在src/test/resources/目录下)。

 

三、动态替换

    方案二比起方案一更“安全”一些,因为没有手工注入的代码,我们只要使用了正确地beanName,基本可以确保config.xml中的Bean的装配是符合预期的。不过这样就是比较麻烦,每当有这样的问题时,都要定义一个Java文件,一个xml文件。如果可以像方案一那样,可以在具体的测试用例中指定要替换哪个测试替身,但又可以像方案二一样,利用spring来实现替换就好了。

 

public class TestDoubleInjector implements BeanPostProcessor {

	private static Map<String, Object> MOCKS = new HashMap<String, Object>(); (1)

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName)
			throws BeansException {
		if (MOCKS.containsKey(beanName)) {
			return MOCKS.get(beanName);
		}
		return bean;
	}

	public void addMock(String beanName, Object mock) {
		MOCKS.put(beanName, mock);
	}

	public void clear() {
		MOCKS.clear();
	}

}

这里我想到了将测试替身保存在一个Map中,在测试开始前,填充该Map,但是测试开始前,ApplicationContext已经启动了,替换的时机已经错过了。这里我在(1)中将这个Map定义为静态变量,然后通过延迟启动ApplicationContext的方式初步完成了目标。

@RunWith(JMock.class)
public class SomeAppIntegrationTestsUsingDynamicReplacing {

	private Mockery context = new JUnit4Mockery();

	private SomeInterface mock = context.mock(SomeInterface.class);

	private SomeApp someApp;

	private ConfigurableApplicationContext applicationContext;

	private TestDoubleInjector fixture = new TestDoubleInjector(); (1)

	@Before
	public void replaceDependenceWithMock() {

		fixture.addMock("dependence", mock);  (2)

		applicationContext = new ClassPathXmlApplicationContext(new String[] {
				"classpath:config.xml", "classpath:dynamic.xml" });  (3)
		someApp = (SomeApp) applicationContext.getBean("someApp");
	}

	@Test
	public void returnsHelloWorldIfDependenceIsAvailable() throws Exception {

		context.checking(new Expectations() {
			{
				allowing(mock).isAvailable();
				will(returnValue(true));
			}
		});

		String actual = someApp.returnHelloWorld();
		assertEquals("helloWorld", actual);
	}

	@After
	public void clean() {
		applicationContext.close();
		fixture.clear();
	}
}

在这个版本的测试中,利用类似Monostate模式的方法(1)(3),我们在启动ApplicationContext前(3),完成刚才提高的Map的填充,虽然spring实例化的TestDoubleInjector和我们在(1)中实例化的TestDoubleInjector并不是同一个实例,但由于它们共享一个Map,所以在随后启动ApplicationContext的过程中就可以取到我们指定的测试替身了。这样,如果TestDoubleInjector及其xml配置可以被若干个测试共享,不过要注意的是,由于static Map的关系,记得要在测试前/后清空一下,否则可能混入上一个测试中定义的测试替身。

    这个方案也有缺点,就是ApplicationContext每次测试都要重建,如果你的测试集中包含了很多这样的测试,而ApplicationContext的规模比较大(其中定义的Bean较多),那么花费的时间就会比较多。当然,方案一和方案二也可能有这个问题,因为它们也都“污染”了ApplicationContext,如果在其他测试中你并不需要替换测试替身,你需要在测试方法中声明@DirtiesContext,这样在测试后,spring会重建ApplicationContext(默认情况下,在一个测试套件中,spring只会创建一次ApplicationContext以节约时间)。

    好了,就介绍到这里,虽然我都是以Mock作为例子,但如果你要替换的是Stub,方法也是一样的。如果你有更好的方法,请务必跟帖让我知道,谢谢

 

参考资料:

http://www.jmock.org 在这里你可以找到Mock的详细信息

http://www.oracle.com/technetwork/articles/entarch/spring-aop-with-ejb5-093994.html 在这里我第一次了解到BeanPostProcessor及其在测试中的用处

 

 

 

0
0
分享到:
评论

相关推荐

    单元测试

    4. **mock和stub**:在单元测试中,mock和stub常用于模拟外部依赖,使测试环境独立于实际运行环境。Mock对象可以模拟真实对象的行为,而stub则用于提供预定义的返回值或行为。 5. **测试覆盖率**:测试覆盖率是衡量...

    cpp-CMock一个C的mockstub生成器

    这些代码可以被包含到测试框架中,如Google Test(gtest)、Unity等,以便在测试用例中使用。使用CMock,开发者可以专注于编写测试逻辑,而无需手动编写大量mock和存根代码。 使用CMock的步骤大致如下: 1. **配置...

    java 中UNIT测试学习

    JUnit可以与其他库(如Mockito)结合,模拟(mock)和存根(stub)外部依赖,以便孤立地测试目标代码。 10. **运行和分析测试结果** 测试可以通过IDE、命令行或者构建工具运行。JUnit的测试结果会显示成功、失败...

    测试驱动开发with Junit(三)

    3. **Mock对象和Stub**:在TDD中,为了隔离被测试代码,经常需要使用Mock对象或Stub。Mock对象模拟了真实对象的行为,以便于控制测试环境,而Stub则提供了预定义的返回值,帮助我们专注于测试目标代码的功能。 4. *...

    全面详尽的JavaScript和Node.js测试最佳实践(2023年7月).zip

    在JavaScript中,可以使用Karma与Jasmine配合,或者使用AVA进行集成测试。 3. 浏览器兼容性测试:由于JavaScript在不同浏览器中的行为可能不同,所以需要确保代码在主流浏览器上都能正常工作。工具如BrowserStack和...

    spring-boot-test_springboot学习笔记

    2. **H2数据库**:在集成测试中,经常使用内存数据库如H2,这样不会污染生产数据库。通过`spring.datasource.url=jdbc:h2:mem:testdb`来配置。 四、Spring Boot测试实战 在“558.spring-boot-test__zsl131”这个...

    python 自动化测试安装包

    7. **Mock 和 Stub**:在测试中,有时需要隔离被测试代码,避免外部依赖的影响。Python 的 `unittest.mock` 模块提供了 Mock 对象和 Stub 功能,可以模拟函数、类的行为,使测试更加可控。 8. **测试驱动开发 (TDD)...

    C#测试项目

    在C#中,我们可以使用相同的测试框架进行集成测试,但通常需要更高的环境配置,比如数据库连接或网络服务。 5. **代码覆盖率**:为了确保测试的全面性,我们通常会关注代码覆盖率,即测试执行了多少源代码。工具如...

    单元集成系统验收

    单元集成测试系统验收测试是指在单元测试的基础上,进一步验证各个模块或组件之间的交互是否正确无误的过程。它不仅关注单个模块的功能实现,还强调不同模块间的协同工作能力。 #### 测试目标 - **确保接口兼容性**...

    java 单元测试

    在复杂的依赖关系中,Mock和Stub的使用可以更精确地控制测试流程。 10. **测试驱动开发(TDD)**:一种开发模式,先编写测试,再编写实现代码,确保代码一开始就符合需求。TDD可以促进更好的设计,因为测试迫使开发者...

    基于python的软件测试练习项目(代码为python实现)

    例如,在Software_testing_01-code中可能包含了使用unittest编写的测试脚本,你可以看到如何创建测试类,定义测试方法,并使用assert系列函数进行断言。 三、pytest框架 pytest相比unittest更加强大且易用,它自动...

    中文版:java测试驱动开发的编程技术

    Java测试驱动开发(Test-Driven Development,TDD)是一种软件开发方法,它强调在编写实际功能代码之前,先编写测试代码。这种做法有助于确保代码的质量,降低错误率,并且能够及时发现和修复问题。以下是对Java TDD...

    单元测试(C#,VB,C++)

    在C#、VB(Visual Basic)和C++这三种编程语言中,单元测试可以帮助开发者确保他们的代码质量,减少错误,并提高代码的可维护性。下面我们将详细探讨这三个语言中的单元测试实践。 在C#中,最常用的单元测试框架是...

    HttpMock:一个用于在测试和响应中即时创建Http服务器的库

    这对于集成和验收测试特别有用。 HttpMock在运行时返回罐装响应。用法。 首先,在要测试的应用程序中,使用HttpMock的URL更改要模拟的HTTP服务的URL。 告诉HttpMock监听您提供的端口。 这始终是本地主机,例如: _...

    pragmatic unit testing in java with junit

    7. **集成测试**:虽然本书主要关注单元测试,但也会涉及集成测试的概念,讨论如何在项目中适当地结合单元测试和集成测试。 8. **错误处理和日志记录**:了解如何在测试中有效地处理异常,并使用日志记录工具(如...

    软件测试课件

    我们可以创建模拟对象(mock objects)和存根对象(stub objects)来隔离被测试代码,以便更好地控制测试环境。此外,C#还支持特性(attributes),如 `[Test]` 和 `[ExpectedException]`,可以用于标记测试方法和...

    Java的测试文件说明

    使用`@Test`注解标识测试方法,并在方法内部设置断言,比如`assertEquals()`,`assertTrue()`等。 4. **使用注解**:JUnit提供了多种注解,如`@Before`和`@After`,它们分别表示在所有测试方法之前和之后执行的代码...

    函数测试_函数测试_

    - 使用mock和stub技术:模拟函数依赖的外部资源,以隔离测试环境。 - 遵循TDD(测试驱动开发)或BDD(行为驱动开发)原则:先编写测试用例,再编写实现代码。 - 定期重构测试代码:保持测试代码的整洁和可维护性。 ...

    java单元测试demo

    它的目的是确保每个独立的部分都能正常工作,以便在集成测试中确保整体系统的稳定。 2. JUnit框架: 在Java领域,JUnit是最常用的单元测试框架。它提供了一套注解(如@Test)和断言库,使得编写和执行单元测试变得...

Global site tag (gtag.js) - Google Analytics