我已经进行了Integration-Tests,是否应该为CRUD操作编写单元测试?

时间:2012-09-17 22:30:07

标签: spring hibernate unit-testing integration-testing crud

在我们最近的项目中,Sonar抱怨测试覆盖率很低。我们注意到它默认不考虑集成测试。除了你可以配置Sonar,它会考虑它们(JaCoCo插件),我们正在讨论我们团队中的问题,如果确实需要编写单元测试,当你用集成测试覆盖所有服务和数据库层时无论如何。

我对集成测试的意思是,我们所有的测试都是针对我们在生产中使用的相同类型的专用Oracle实例运行的。我们不会嘲笑任何东西。如果服务依赖于其他服务,我们将使用真实服务。在运行测试之前我们需要的数据,我们通过一些使用我们的服务/存储库(DAO)的工厂类来构建。

因此,从我的角度来看 - 为简单的CRUD操作编写集成测试,尤其是在使用Spring Data / Hibernate等框架时,这并不是一件大事。它有时甚至更容易,因为你没有想到什么以及如何模仿。

那么为什么我应该为我的CRUD操作编写单元测试,因为我可以编写的集成测试不太可靠?

我看到的唯一一点是集成测试需要更多时间才能运行,项目得到的越大。因此,您不希望在办理登机手续之前全部运行它们。但是我不确定这是不是很糟糕,如果你有一个CI环境,Jenkins / Hudson会做这个工作。

所以 - 任何意见或建议都非常感谢!

3 个答案:

答案 0 :(得分:10)

如果你的大部分服务都只是传递给你的daos,你的daos做的很少,只能在Spring的HibernateTemplateJdbcTemplate上调用方法,那么你是正确的,单元测试不是&#39 ; t真正证明你的集成测试已经证明了什么。但是,进行单元测试对于所有常见原因都是有价值的。

由于单元测试只测试单个类,在没有磁盘或网络访问的内存中运行,并且从不真正测试多个类一起工作,它们通常是这样的:

  • 服务单元测试模拟道具。
  • Dao单元测试模拟数据库驱动程序(或弹簧模板)或使用嵌入式数据库(在Spring 3中非常简单)。

要对刚刚传递给dao的服务进行单元测试,您可以这样进行模拟:

@Before
public void setUp() {
    service = new EventServiceImpl();
    dao = mock(EventDao.class);
    service.EventDao = dao;
}

@Test
public void creationDelegatesToDao() {
    service.createEvent(sampleEvent);
    verify(dao).createEvent(sampleEvent);
}

@Test(expected=EventExistsException.class)
public void creationPropagatesExistExceptions() {
    doThrow(new EventExistsException()).when(dao).createEvent(sampleEvent);
    service.createEvent(sampleEvent);
}

@Test
public void updatesDelegateToDao() {
    service.updateEvent(sampleEvent);
    verify(dao).updateEvent(sampleEvent);
}

@Test
public void findingDelgatesToDao() {
    when(dao.findEventById(7)).thenReturn(sampleEvent);
    assertThat(service.findEventById(7), equalTo(sampleEvent));

    service.findEvents("Alice", 1, 5);
    verify(dao).findEventsByName("Alice", 1, 5);

    service.findEvents(null, 10, 50);
    verify(dao).findAllEvents(10, 50);
}

@Test
public void deletionDelegatesToDao() {
    service.deleteEvent(sampleEvent);
    verify(dao).deleteEvent(sampleEvent);
}

但这真的是个好主意吗?这些Mockito断言声称dao方法被调用,而不是它做了预期的!您将获得您的覆盖率数字,但您或多或少地将测试绑定到dao的实现。哎哟。

现在这个例子假设服务没有真正的业务逻辑。通常,服务将具有与dao调用相关的业务逻辑,并且您肯定必须测试它们。

现在,对于单元测试,我喜欢使用嵌入式数据库。

private EmbeddedDatabase database;
private EventDaoJdbcImpl eventDao = new EventDaoJdbcImpl();

@Before
public void setUp() {
    database = new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("schema.sql")
            .addScript("init.sql")
            .build();
    eventDao.jdbcTemplate = new JdbcTemplate(database);
}

@Test
public void creatingIncrementsSize() {
    Event e = new Event(9, "Company Softball Game");

    int initialCount = eventDao.findNumberOfEvents();
    eventDao.createEvent(e);
    assertThat(eventDao.findNumberOfEvents(), is(initialCount + 1));
}

@Test
public void deletingDecrementsSize() {
    Event e = new Event(1, "Poker Night");

    int initialCount = eventDao.findNumberOfEvents();
    eventDao.deleteEvent(e);
    assertThat(eventDao.findNumberOfEvents(), is(initialCount - 1));
}

@Test
public void createdEventCanBeFound() {
    eventDao.createEvent(new Event(9, "Company Softball Game"));
    Event e = eventDao.findEventById(9);
    assertThat(e.getId(), is(9));
    assertThat(e.getName(), is("Company Softball Game"));
}

@Test
public void updatesToCreatedEventCanBeRead() {
    eventDao.createEvent(new Event(9, "Company Softball Game"));
    Event e = eventDao.findEventById(9);
    e.setName("Cricket Game");
    eventDao.updateEvent(e);
    e = eventDao.findEventById(9);
    assertThat(e.getId(), is(9));
    assertThat(e.getName(), is("Cricket Game"));
}

@Test(expected=EventExistsException.class)
public void creatingDuplicateEventThrowsException() {
    eventDao.createEvent(new Event(1, "Id1WasAlreadyUsed"));
}

@Test(expected=NoSuchEventException.class)
public void updatingNonExistentEventThrowsException() {
    eventDao.updateEvent(new Event(1000, "Unknown"));
}

@Test(expected=NoSuchEventException.class)
public void deletingNonExistentEventThrowsException() {
    eventDao.deleteEvent(new Event(1000, "Unknown"));
}

@Test(expected=NoSuchEventException.class)
public void findingNonExistentEventThrowsException() {
    eventDao.findEventById(1000);
}

@Test
public void countOfInitialDataSetIsAsExpected() {
    assertThat(eventDao.findNumberOfEvents(), is(8));
}

我仍称这是一个单元测试,尽管大多数人可能称之为集成测试。嵌入式数据库驻留在内存中,并在测试运行时启动并关闭。但这取决于嵌入式数据库看起来与生产数据库相同的事实。情况会是这样吗?如果没有,那么所有的工作都是无用的。如果是这样,那么,正如您所说,这些测试正在做与集成测试不同的任何事情。但我可以按需mvn test运行它们,我有信心重构。

因此,无论如何,我都会编写这些单元测试并达到我的覆盖率目标。当我编写集成测试时,我断言HTTP请求返回预期的HTTP响应。是的,它包含了单元测试,但是,嘿,当你练习TDD时,无论如何你都要在实际的dao实现之前编写单元测试。

如果你在dao之后编写单元测试,那么当然它们写起来并不好玩。 TDD文献中充满了关于如何在代码感觉像工作之后编写测试并且没有人愿意这样做的警告。

TL; DR:您的集成测试将包含您的单元测试,从这个意义上说,单元测试不会增加真正的测试值。但是,当您拥有高覆盖率的单元测试套件时,您就有信心进行重构。但是当然如果dao简单地调用了Spring的数据访问模板,那么你可能不会重构。但你永远不知道。最后,如果单元测试首先以TDD风格编写,那么无论如何你都会拥有它们。

答案 1 :(得分:1)

如果您计划将图层暴露给项目中的其他组件,则您只需要单独对每个图层进行单元测试。对于Web应用程序,可以调用存储库层的唯一方法是通过服务层,并且可以通过控制器层调用服务层的唯一方法。因此,测试可以在控制器层开始和结束。对于后台任务,这些在服务层中调用,因此需要在此进行测试。

如今,使用真实数据库进行测试的速度非常快,因此,如果您设置好设置/拆卸,请不要太慢地减慢测试速度。但是,如果有任何其他依赖可能很慢或有问题,那么这些应该被模拟/存根。

这种方法会给你:

  • 良好的报道
  • 现实主义测试
  • 最小努力量
  • 最低限度的反思努力

但是,单独测试图层确实允许您的团队更多地同时工作,因此一个开发人员可以执行存储库,另一个开发人员可以为一个功能部门提供服务,并生成经过独立测试的工作。

当合并硒/功能测试时,总会有双重覆盖,因为它们不能单独依赖这些测试,因为它们太慢而无法运行。但是,功能测试不一定需要覆盖所有代码,只要核心功能就足够了,只要代码已被单元/集成测试所覆盖。

答案 2 :(得分:0)

除了高端集成测试,我认为具有更细粒度的测试(这里我将不故意使用单词单元测试)有两个优点。

1)冗余,将层覆盖在一个以上的位置就像一个开关。如果一组测试(例如集成测试)未能找到错误,则第二层可能会捕获该错误。在这里,我将与必须冗余的电气开关进行比较。您有一个主开关和一个专用开关。

2)让我们假设您有一个调用外部服务的进程。由于一个或另一个原因(错误),原始异常将成为使用者,而一个不携带有关错误的技术性质的信息的异常将到达集成测试。集成测试将捕获该错误,但是您将不知道该错误是什么或从何而来。进行更细粒度的测试会增加朝正确方向指出确切失败的原因和位置的机会。

我个人认为测试中一定程度的冗余并不是一件坏事。

在特定情况下,如果您在内存数据库中编写CRUD测试,您将有机会测试您的Hibernate映射层,如果您使用诸如Cascading或fetching之类的东西,这可能会非常复杂... < / p>