使用模拟的单元测试–测试技术5

我的最后一个博客是有关测试代码方法的一系列博客中的第四篇,演示了如何创建使用存根对象隔离测试对象的单元测试。 今天的博客探讨了有时被视为对立的技术:使用模拟对象进行单元测试。 同样,我使用了从数据库检索地址的简单方案:

…并测试AddressService类:

@Component
public class AddressService {

  private static final Logger logger = LoggerFactory.getLogger(AddressService.class);

  private AddressDao addressDao;

  /**
   * Given an id, retrieve an address. Apply phony business rules.
   * 
   * @param id
   *            The id of the address object.
   */
  public Address findAddress(int id) {

    logger.info("In Address Service with id: " + id);
    Address address = addressDao.findAddress(id);

    address = businessMethod(address);

    logger.info("Leaving Address Service with id: " + id);
    return address;
  }

  private Address businessMethod(Address address) {

    logger.info("in business method");

    // Apply the Special Case Pattern (See MartinFowler.com)
    if (isNull(address)) {
      address = Address.INVALID_ADDRESS;
    }

    // Do some jiggery-pokery here....

    return address;
  }

  private boolean isNull(Object obj) {
    return obj == null;
  }

  @Autowired
  @Qualifier("addressDao")
  void setAddressDao(AddressDao addressDao) {
    this.addressDao = addressDao;
  }
}

…通过将他的数据访问对象替换为模拟对象。

在继续之前,最好定义一个模拟对象的确切含义以及它与存根的不同之处。 如果您阅读了我的上一篇博客,您会记得我让Martin Fowler将存根对象定义为:

“存根提供对测试过程中进行的呼叫的固定答复,通常通常根本不响应测试中编程的内容。”

……摘自他的论文《 Mocks Are n't Stubs》

那么,模拟对象与存根有何不同? 当您听到人们谈论模拟对象时,他们经常提到他们在嘲笑 行为嘲笑 角色 ,但这意味着什么? 答案在于单元测试和模拟对象共同测试对象的方式。 模拟对象场景如下所示:

  1. 测试中定义了一个模拟对象。
  2. 模拟对象被注入到您的测试对象中
  3. 该测试指定将调用模拟对象上的哪些方法,以及参数和返回值。 这就是所谓的“ 设定期望 ”。
  4. 然后运行测试。
  5. 然后,测试将要求模拟程序验证步骤3中指定的所有方法调用均已正确调用。 如果是,则测试通过。 如果不是,那么测试将失败。

因此,模拟行为或模拟角色实际上意味着检查被测对象是否正确调用了模拟对象上的方法,如果没有,则使测试失败。 因此,您是在断言方法调用的正确性和通过代码的执行路径,而不是在常规单元测试的情况下断言被测试方法的返回值。

尽管有几种专业的模拟框架,但在本例中,我首先决定产生自己的AddressDao模拟,它可以满足上述要求。 毕竟,这有多难?

public class HomeMadeMockDao implements AddressDao {

  /** The return value for the findAddress method */
  private Address expectedReturn;

  /** The expected arg value for the findAddress method */
  private int expectedId;

  /** The actual arg value passed in when the test runs */
  private int actualId;

  /** used to verify that the findAddress method has been called */
  private boolean called;

  /**
   * Set and expectation: the return value for the findAddress method
   */
  public void setExpectationReturnValue(Address expectedReturn) {
    this.expectedReturn = expectedReturn;
  }

  public void setExpectationInputArg(int expectedId) {
    this.expectedId = expectedId;
  }

  /**
   * Verify that the expectations have been met
   */
  public void verify() {

    assertTrue(called);
    assertEquals("Invalid arg. Expected: " + expectedId + " actual: " + expectedId, expectedId, actualId);
  }

  /**
   * The mock method - this is what we're mocking.
   * 
   * @see com.captaindebug.address.AddressDao#findAddress(int)
   */
  @Override
  public Address findAddress(int id) {

    called = true;
    actualId = id;
    return expectedReturn;
  }
}

支持此模拟的单元测试代码为:

public class MockingAddressServiceWithHomeMadeMockTest {

  /** The object to test */
  private AddressService instance;

  /**
   * We've written a mock,,,
   */
  private HomeMadeMockDao mockDao;

  @Before
  public void setUp() throws Exception {
    /* Create the object to test and the mock */
    instance = new AddressService();
    mockDao = new HomeMadeMockDao();
    /* Inject the mock dependency */
    instance.setAddressDao(mockDao);
  }

  /**
   * Test method for
   * {@link com.captaindebug.address.AddressService#findAddress(int)}.
   */
  @Test
  public void testFindAddressWithEasyMock() {

    /* Setup the test data - stuff that's specific to this test */
    final int id = 1;
    Address expectedAddress = new Address(id, "15 My Street", "My Town", "POSTCODE", "My Country");

    /* Set the Mock Expectations */
    mockDao.setExpectationInputArg(id);
    mockDao.setExpectationReturnValue(expectedAddress);

    /* Run the test */
    instance.findAddress(id);

    /* Verify that the mock's expectations were met */
    mockDao.verify();
  }
}

好的,尽管这演示了使用模拟对象执行单元测试所需的步骤,但它相当粗糙且准备就绪,并且非常针对AddressDao / AddressService场景。 为了证明它已经做得更好,下面的示例使用easyMock作为模拟框架。 在这种更专业的情况下,单元测试代码为:

@RunWith(UnitilsJUnit4TestClassRunner.class)
public class MockingAddressServiceWithEasyMockTest {

  /** The object to test */
  private AddressService instance;

  /**
   * EasyMock creates the mock object
   */
  @Mock
  private AddressDao mockDao;

  /**
   * @throws java.lang.Exception
   */
  @Before
  public void setUp() throws Exception {
    /* Create the object to test */
    instance = new AddressService();
  }

  /**
   * Test method for
   * {@link com.captaindebug.address.AddressService#findAddress(int)}.
   */
  @Test
  public void testFindAddressWithEasyMock() {

    /* Inject the mock dependency */
    instance.setAddressDao(mockDao);
    /* Setup the test data - stuff that's specific to this test */
    final int id = 1;
    Address expectedAddress = new Address(id, "15 My Street", "My Town", "POSTCODE", "My Country");
    /* Set the expectations */
    expect(mockDao.findAddress(id)).andReturn(expectedAddress);
    replay();

    /* Run the test */
    instance.findAddress(id);

    /* Verify that the mock's expectations were met */
    verify();
  }
}

…我希望您会同意,这比我快速尝试编写模拟游戏更具进步性。

使用模拟对象的主要批评是它们将单元测试代码与生产代码的实现紧密耦合。 这是因为设置期望值的代码紧密跟踪生产代码的执行路径。 这意味着即使该类仍履行其接口协定,后续对生产代码的重构也可能破坏大量测试。 这引起了这样的断言,即模拟测试相当脆弱,并且您将花费不必要的时间修复它们,根据我的经验,尽管我使用了“非严格”模拟,但这种模拟并不关心方法的顺序,尽管我同意期望被称为,在一定程度上减轻了问题。

另一方面,一旦您知道如何使用诸如easyMock之类的框架,就可以非常快速有效地完成将您的对象隔离的单元测试。

在自我批评该示例代码时,我想指出的是,我认为在这种情况下使用模拟对象是过大的,此外,您还可以轻易地认为我将模拟作为存根使用。

几年前,当我第一次遇到easyMock时,我在各处使用了模拟,但是最近我开始更喜欢手动为应用程序边界类(例如DAO)和仅返回数据的对象编写存根。 这是因为基于存根的测试可以说比基于模拟的测试要脆弱得多,尤其是当您需要访问数据时。

为什么要使用模拟? 擅长测试使用“ 告诉不要询问 ”技术编写的应用程序,以验证是否调用了具有无效返回值的方法。

参考: Captain Debug博客上来自JCG合作伙伴 使用Mocks进行单元测试-测试技术5

相关文章 :


翻译自: https://www.javacodegeeks.com/2011/11/unit-testing-using-mocks-testing.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值