9

在我们最近的项目中,Sonar 抱怨测试覆盖率低。我们注意到它默认不考虑集成测试。除了您可以配置 Sonar,因此它会考虑它们(JaCoCo 插件)之外,当您使用集成测试覆盖所有服务和数据库层时,我们在团队中讨论了是否真的需要编写单元测试的问题反正。

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

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

那么,为什么我要为我的 CRUD 操作编写单元测试,而这些单元测试的可靠性不如我可以编写的集成测试呢?

我看到的唯一一点是集成测试将花费更多时间来运行,项目越大。因此,您不想在签入前全部运行它们。但我不太确定这是否如此糟糕,如果你有一个带有 Jenkins/Hudson 的 CI 环境可以完成这项工作。

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

4

3 回答 3

11

如果您的大多数服务只是简单地传递到您的 daos,并且您的 daos 只调用 Spring 上的方法,HibernateTemplate那么JdbcTemplate您是正确的,单元测试并不能真正证明您的集成测试已经证明的任何事情。然而,由于所有常见的原因,进行单元测试是有价值的。

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

  • 服务单元测试模拟 daos。
  • Dao 单元测试模拟数据库驱动程序(或 spring 模板)或使用嵌入式数据库(在 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 调用之外还有业务逻辑,你肯定必须测试这些。

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

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 风格编写,那么无论如何你都会拥有它们。

于 2012-09-18T00:39:00.620 回答
1

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

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

这种方法将为您提供:

  • 良好的覆盖范围
  • 现实测试
  • 最小的努力
  • 最少的反思努力

但是,隔离测试层确实允许您的团队更多地同时工作,因此一个开发人员可以做存储库,另一个可以为一个功能提供服务,并产生独立测试的工作。

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

于 2012-09-18T01:09:46.937 回答
0

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

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

2)假设您有一个调用外部服务的进程。由于一个或另一个原因(错误),原始异常得到消费者,并且不携带有关错误技术性质的信息的异常到达集成测试。集成测试将捕获错误,但您将不知道错误是什么或它来自哪里。进行更细粒度的测试可以增加指出正确方向的机会是什么和哪里失败了。

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

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

于 2019-11-27T19:28:22.490 回答