【问题标题】:Spring Data: Service layer unit testingSpring Data:服务层单元测试
【发布时间】:2013-03-04 10:58:09
【问题描述】:

在我的项目中,我在进行单元测试时遇到了麻烦。一个问题是,仅进行集成测试编写起来要快得多,并且还可以测试组件是否实际协同工作。单元测试新颖的“算法”左右似乎要容易得多。单元测试服务类它只是感觉错误和无用。

我正在使用 mockito 来模拟 spring 数据存储库(因此是数据库访问)。问题是,如果我告诉模拟存储库在方法调用 getById 上返回实体 A,它显然会返回它,并且服务也会返回它。是的,该服务做了一些额外的事情,但非常小的事情,比如加载惰性集合(来自休眠)。显然我在单元测试中没有任何惰性集合(代理)。

例子:

@Test
public void testGetById() {
    System.out.println("getById");
    TestCompound expResult = new TestCompound(id, "Test Compound", "9999-99-9", null, null, null);

    TestCompoundRepository mockedRepository = mock(TestCompoundRepository.class);
    when(mockedRepository.findOne(id)).thenReturn(expResult);

    ReflectionTestUtils.setField(testCompoundService, "testCompoundRepository",
            mockedRepository, TestCompoundRepository.class);

    TestCompound result = testCompoundService.getById(id);
    assertEquals(expResult, result);
}

万岁,其余的都成功了。多么惊喜!不是真的没有。

有人可以向我解释我做错了什么吗?否则这种测试的意义何在?我的意思是我告诉返回 expResult 然后它被返回。哇。多么惊喜!感觉就像我在测试 mockito 是否有效,而不是我的 Service。

编辑:

如果出现一些愚蠢的错误,我看到的唯一好处就是在此处留下不需要的行,将返回值设置为 null 或类似的愚蠢。这种情况将被单元测试捕获。 “回报-努力”比似乎仍然很糟糕?

【问题讨论】:

  • 您的思路正确,如果您对我的回答有任何其他问题或疑问,请告诉我。
  • 如果您的服务具有不只是访问数据库的功能,那么您应该进行测试。一个简单的 CRUD 操作只对在 DB 集成层进行测试才有意义。如果您的findById 服务方法正在调用数据库findById 方法,则使用同一数据库/表/存储库中不存在的一些信息来增强结果 - 用户记录的示例可能包括通过街道地址查找他们的 GPS 坐标或反向 IP 查找 - 那么在服务级别进行测试是有意义的。但是模拟 everything 只是对您的测试和模拟框架进行单元测试。

标签: java spring unit-testing spring-data


【解决方案1】:

我喜欢测试我的 Spring Data 存储库的原因之一是测试我是否正确定义了我的 JPA 映射。我没有为这些测试使用模拟框架,我使用 Spring Test 框架,它实际上引导容器,允许我将实际存储库自动连接到 Junit 测试中,以便我可以针对它执行测试。

我同意你的想法,即模拟存储库毫无用处。由于您使用 Spring,我建议您利用 Spring Test 框架对您的存储库执行真实测试,这些测试可以以更基于单元测试的方式对嵌入式数据库(如 H2)或您的实际数据库实现(如 Oracle 或 MySql)执行,以进行更多的集成测试。 (针对开发数据库的副本执行这些测试)这些测试将揭示您的 JPA 映射和其他项目中的谬误,例如数据库中不正确的级联设置。

这是我在 GitHub 上的一项测试的example。注意框架实际上是如何将存储库自动装配到测试中的。该存储库还包含一个如何配置 Spring Test 框架的示例,我也在此 blog post 中进行了演示。

总之,我不相信你会从我所讨论的使用存储库的模拟测试存储库中获得任何好处。

我想补充一点,mock 并不是真正打算在实际的测试类中使用。它们的用途是为被测类提供所需的依赖项。

【讨论】:

  • 是的,我有一整套使用 SpringJUnitTestRunner 和真实存储库的集成测试。在这里,我正在尝试对内部使用存储库的服务进行单元测试。我的项目需要 postgresql,因此内存数据库无法工作。但据我所知,您同意上述示例单元测试或多或少毫无价值?
  • 再次阅读此答案后,我认为您误解了我。我正在测试使用存储库而不是存储库本身的服务,因此对其进行了模拟。
  • 您的博客链接已损坏 :-( 我想了解更多有关您的自动接线的信息。
  • 我想我们仍然想模拟存储库的原因之一是单元测试不需要为存储库设置数据库,因此无论环境如何它都可用。而且我认为单元测试应该快速并不断调用以进行持续集成。如果服务层简单,也许可以和控制器一起测试,但可以使用mock for repository。如果服务层本身比较复杂,需要独立测试。
  • 一旦你停止模拟要测试的类的环境,你就不再进行单元测试了。单元测试的概念是孤立地测试一个单元。如果你想一起测试其他部分,那么你需要编写集成测试
【解决方案2】:

你完全正确。这是明确的单元测试。而且它永远不会失败(所以,它没用)我认为您需要在集成测试中使用 real 数据库测试 real JPA 存储库(例如内存中的H2) (就像我一直做的那样)。

最好测试您的服务(他们的接口)。如果一段时间后您将更改您的存储(例如更改为 Mongo) - 您将能够使用您的服务测试来确保一切正常。

一段时间后,您会惊讶地发现有多少与 DB\JPA 相关的问题(约束、乐观锁、延迟加载、重复 id、一些休眠问题等等)。

另外,尝试通过测试进行开发——而不仅仅是在实现后编写测试。而不是在服务中创建新方法之前 - 为它创建测试,实现服务方法,并且只有在实际应用程序中重新检查它之后。至少启动测试比服务器快得多。

因此,不要创建包含大量测试的测试。了解他们如何为您提供帮助。

对存储库使用模拟不是一个好主意。测试您的服务如何与 Hibernate\JPA\Database 一起工作。大部分问题位于两层

【讨论】:

  • 感谢您的评论,我同意。正如在其他 cmets 中提到的,我已经有一堆集成测试和配置问题,并且组件间的交互很好。我是测试新手,并试图确定我是否需要在这里进行单元测试,或者只是进行需要 postgresql + 和附加组件才能安装的直接集成测试。示例测试所做的唯一事情是确定是否有人不小心弄乱了返回语句,例如 return null; 或类似的愚蠢或不可能的东西。
  • 但有时单元测试非常有用。例如,如果您修复了一个错误 - 在将其推送到存储库之前 - 为它创建单元(如果可能)或集成测试。在大约 30% 的情况下,您会感到惊讶 :)
【解决方案3】:

问题可能有点老,但我会回答以防有人偶然发现。

  • 我正在使用 Mockito 和 JUnit。
  • AccountRepository 是一个扩展 JPARepository 的普通 Spring 数据存储库。
  • Account 是一个普通的 JPA 实体。

要测试您的服务和模拟 Spring Data 存储库,您需要以下内容。

package foo.bar.service.impl;

import foo.bar.data.entity.Account;
import foo.bar.data.repository.AccountRepository;
import foo.bar.service.AccountService;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class AccountServiceImplTest {

    @Mock
    private static AccountRepository accountRepository;

    @InjectMocks
    private static AccountService accountService = new AccountServiceImpl();

    private Account account;

    @Test
    public void testFindAccount() {

        Integer accountId = new Integer(1);

        account = new Account();
        account.setId(accountId);
        account.setName("Account name");
        account.setCode("Accont code");
        account.setDescription("Account description");

        Mockito.when(accountRepository.findOne(accountId)).thenReturn(account);

        Account retrivedAccount = accountService.findAccount(accountId);

        Assert.assertEquals(account, retrivedAccount);

    }

}

【讨论】:

【解决方案4】:

你可以使用这个库:https://github.com/agileapes/spring-data-mock

这将为您模拟您的存储库,同时允许您为任何方法以及您的本机查询方法实现自定义功能。

【讨论】:

    【解决方案5】:

    您可以模拟存储库并将其注入服务,这就是方式;但是,如果您只是使用存储库的@Mock 来实例化服务,最好将存储库定义为服务中的private final 字段并使用所有存储库的构造函数。这样一来,如果您向服务添加另一个存储库,测试将失败,您必须更改它,这就是目的。

    想象一下这项服务:

    class OrderService {
        private final UserRepository userRepos;
    
        public OrderService(UserRepository userRepos) {
            this.userRepos = userRepos;
        }
    
        ...
    
    }
    

    还有这个测试:

    class OrderServiceTests {
        @Mock
        private UserRepository userRepos;
    
        private OrderService service;
    
        private OrderServiceTests() {
            this.service = new OrderService(this.userRepos);
        }
    }
    

    现在,如果我们向服务添加另一个依赖项:

    class OrderService {
        private final UserRepository userRepos;
        private final AddressRepository addRepos;
    
        public OrderService(UserRepository userRepos, AddressRepository addRepos) {
            this.userRepos = userRepos;
            this.addRepos = addRepos;
    
        ...
    
    }
    

    之前的测试将失败,因为构造函数已更改。如果您使用@InjectMocks,则不会发生这种情况;注射发生在幕后,我们不清楚会发生什么;这可能是不可取的。

    另一件事是,我不同意集成测试将涵盖单元测试将涵盖的所有情况;可能但并非总是如此。甚至控制器也可以使用 mock 进行单元测试;毕竟所有的测试都是为了覆盖我们编写的所有代码,所以它们必须是细粒度的;想象一下,当我们遵循 TTD 并且只完成控制器和服务级别时:如果没有控制器单元测试,我们将如何进行?

    【讨论】:

      【解决方案6】:

      假设我们有下面的Service

      @Service 
      public class EmployeeServiceImpl implements EmployeeService {
      
          @Autowired
          private EmployeeRepository employeeRepository;
      
          @Override
          public Employee getEmployeeByName(String name) {
              return employeeRepository.findByName(name);
          }
      }
      

      测试类:

      @RunWith(SpringRunner.class)
      public class EmployeeServiceImplIntegrationTest {
      
          @TestConfiguration
          static class EmployeeServiceImplTestContextConfiguration {
      
             @Bean
             public EmployeeService employeeService() {
                 return new EmployeeServiceImpl();
              }
          }
      
          @Autowired
          private EmployeeService employeeService;
      
          @MockBean
          private EmployeeRepository employeeRepository;
      
      // write test cases here
      }
      

      要检查 Service 类,我们需要创建一个 Service 类的实例并作为 @Bean 使用,以便我们可以在测试类中 @Autowire 它.此配置是通过使用 @TestConfiguration 注释来实现的。

      在组件扫描过程中,我们可能会发现仅为特定测试创建的组件或配置意外地随处可见。为了帮助防止这种情况,Spring Boot 提供了 @TestConfiguration 注释,可以在 src/test/java 中的类上使用,以指示它们不应被扫描拾取。

      这里另一个有趣的地方是 @MockBean 的使用。它为 EmployeeRepository 创建了一个 Mock,可用于绕过对实际 EmployeeRepository 的调用:

      @Before
      public void setUp() {
          Employee alex = new Employee("alex");
      
          Mockito.when(employeeRepository.findByName(alex.getName()))
            .thenReturn(alex);
      }
      

      设置完成后,我们可以轻松地测试我们的服务,例如:

      @Test
      public void whenValidName_thenEmployeeShouldBeFound() {
          String name = "alex";
          Employee found = employeeService.getEmployeeByName(name);
      
          assertThat(found.getName())isEqualTo(name);
      }
      

      如需更深入的知识检查: https://www.baeldung.com/spring-boot-testing

      【讨论】:

        猜你喜欢
        • 2020-02-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多