spring - 当我已经有集成测试时,我应该为 CRUD 操作编写单元测试吗?

标签 spring hibernate unit-testing integration-testing crud

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

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

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

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

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

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

最佳答案

如果您的大部分服务只是通过您的 daos,而您的 daos 只调用 Spring 的 HibernateTemplateJdbcTemplate那么您是正确的,单元测试并不能真正证明您的集成测试已经证明的任何事情。然而,由于所有常见的原因,进行单元测试是有值(value)的。

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

  • 服务单元测试模拟 daos。
  • Dao 单元测试模拟数据库驱动程序(或 spring 模板)或使用嵌入式数据库(在 Spring 3 中 super 简单)。

  • 要对刚刚通过 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 方法,而不是它做了预期的事情!您将获得您的覆盖率数字,但您或多或少地将您的测试绑定(bind)到 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:您的集成测试将包含您的单元测试,从这个意义上说,单元测试并没有增加真正的测试值(value)。但是,当您拥有高覆盖率的单元测试套件时,您就有信心进行重构。但是当然,如​​果 dao 只是简单地调用 Spring 的数据访问模板,那么您可能不会进行重构。但你永远不知道。最后,尽管如此,如果单元测试首先以 TDD 风格编写,那么无论如何你都会拥有它们。

    关于spring - 当我已经有集成测试时,我应该为 CRUD 操作编写单元测试吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12467966/

    相关文章:

    java - 如何在没有完全限定名称的情况下在另一个生成的类中定义 java codeModel 生成的类

    java - 如何将 oracle 时间戳映射到 hibernate 中适当的 java 类型?

    unit-testing - XUnit 和 AutoFixture Exception No Data found for (test name)

    Spring + Wicket 口 + Tomcat : Program fails to start because of IllegalArgumentException

    java - 获取表格:options value in AJAX

    java - Spring MVC 中@SessionAttributes 对象的生命周期和注入(inject)

    unit-testing - 什么是模拟?

    spring - 在Spring Boot应用程序中将Gradle用作Nexus As Dependency Resolver

    Oracle组合键删除速度很慢

    Java `final` 类和模拟