【问题标题】:Injecting Mock of Repository into Service doesn't inject the proper mocked method (Spring, JUnit and Mockito)将存储库的模拟注入服务不会注入正确的模拟方法(Spring、JUnit 和 Mockito)
【发布时间】:2020-08-04 18:10:42
【问题描述】:

tl;博士: 似乎我使用关于 save 方法的自定义行为创建的存储库的 Mock 在注入时会丢失自定义行为。


问题描述

我一直在尝试在 Spring 中测试服务。特别感兴趣的方法采用一些参数并创建一个 User,该 User 通过存储库方法 save 保存到 UserRepository 中。

我感兴趣的测试是将这些参数与传递给存储库的 save 方法的 User 的属性进行比较,然后检查它是否是正确添加新用户。

为此,我决定模拟存储库并将相关服务方法传递的参数保存到存储库 save 方法。

我基于myself on this question 来保存用户

private static User savedUser;

public UserRepository createMockRepo() {
   UserRepository mockRepo = mock(UserRepository.class);
   try {
      doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                savedUser= (User) invocation.getArguments(0);
                return null;
            }
        }).when(mockRepo).save(any(User.class));
   } catch( Exception e) {}
   return mockRepo;
}

private UserRepository repo = createMockRepo();

两个音符:

  • 我提供了名称 repo,以防名称必须与服务中的名称匹配。

  • 没有 @Mock 注释,因为它开始无法通过测试,我认为这是因为它将以通常的方式创建一个模拟(没有我之前创建的自定义方法)。

然后我创建了一个测试函数来检查它是否具有所需的行为并且一切都很好。

@Test 
void testRepo() {
   User u = new User();
   repo.save(u);
   assertSame(u, savedUser);
}

然后我尝试按照我在多个问题中看到的建议进行操作,即将模拟注入服务,如here 所述。

@InjectMocks
private UserService service = new UserService();

@Before
public void setup() {
   MockitoAnnotations.initMocks(this);
}

这就是问题出现的地方,当我尝试访问 savedUser 属性时,我为其创建的测试会引发 null 异常(这里我简化了用户属性,因为似乎不是原因)。

@Test 
void testUser() {
   String name = "Steve";
   String food = "Apple";
   
   service.newUser(name, food);

   assertEquals(savedUser.getName(), name);
   assertEquals(savedUser.getFood(), food);
}

调试时:

出于演示目的,我决定使用 System.out.println 记录该函数。

A print of my logging of the tests, demonstrating that the user test doesn't call the answer method


我在这里做错了什么?

提前感谢您的帮助,这是我的第一个堆栈交换问题,非常感谢任何改进提示。

【问题讨论】:

    标签: java spring unit-testing junit mockito


    【解决方案1】:

    不要像您那样在测试类中实例化您的服务,而是使用 @Autowired 并确保您的 UserRepository 在测试类中有 @MockBean

    @InjectMocks
    @Autowired
    private UserService service
    
    @MockBean
    private UserRepository mockUserRepo
    

    有了这个,你可以删除你的设置方法

    但请确保您的 UserRepository 也在您的服务内部自动装配

    【讨论】:

    • 不确定我是否遗漏了什么,但这样做会使服务为空。
    【解决方案2】:

    你不应该需要 Spring 来测试这个。如果您在自动装配依赖项时遵循 Spring 最佳实践,您应该能够自己创建对象并将UserRepository 传递给UserService

    最佳实践是,

    • 所需 bean 的构造函数注入
    • 可选 bean 的 Setter 注入
    • 除非您不能注入构造函数或设置器,否则永远不会进行字段注入,这非常罕见。

    请注意,InjectMocks 不是依赖注入框架,我不鼓励使用它。你可以看到in the javadoc,当涉及到构造函数、setter 和字段时,它会变得相当复杂。

    请注意,此处代码的工作示例可以在 in this GitHub repo. 找到

    清理代码并使其更易于测试的一种简单方法是更正 UserService 以允许您传递所需的 UserRepository 的任何实现,这也允许您保证不变性,

    public class UserService {
    
      public UserService(final UserRepository userRepository) {
        this.userRepository = userRepository;
      }
    
      public final UserRepository userRepository;
    
      public User newUser(String name, String food) {
        var user = new User();
        user.setName(name);
        user.setFood(food);
        return userRepository.save(user);
      }
    }
    

    然后你的测试就会变得更简单,

    class UserServiceTest {
    
      private UserService userService;
      private UserRepository userRepository;
    
      private static User savedUser;
    
      @BeforeEach
      void setup() {
        userRepository = createMockRepo();
        userService = new UserService(userRepository);
      }
    
      @Test
      void testSaveUser(){
        String name = "Steve";
        String food = "Apple";
    
        userService.newUser(name, food);
    
        assertEquals(savedUser.getName(), name);
        assertEquals(savedUser.getFood(), food);
      }
    
      public UserRepository createMockRepo() {
        UserRepository mockRepo = mock(UserRepository.class);
        try {
          doAnswer(
                  (Answer<Void>) invocation -> {
                    savedUser = (User) invocation.getArguments()[0];
                    return null;
                  })
              .when(mockRepo)
              .save(any(User.class));
        } catch (Exception e) {
        }
        return mockRepo;
      }
    }
    

    但是,在我看来,这并没有增加很多好处,因为您直接在服务中与存储库进行交互,除非您完全了解 Spring Data Repository 的复杂性,毕竟您还在模拟网络 I/O这是一件危险的事情

    • @Id 注释如何工作?
    • Hibernate JPA 与我的实体交互怎么样?
    • 我的实体上的列定义是否与我将部署的内容相匹配 使用 Liquibase/Flyway 之类的东西来管理数据库 迁移?
    • 如何针对数据库可能存在的任何约束进行测试?
    • 如何测试自定义事务边界?

    您做了很多假设,为此您可以使用 Spring Boot 提供的 @DataJpaTest documentation 注释,或复制配置。在这一点上,我假设一个 Spring Boot 应用程序,但同样的概念也适用于 Spring Framework 应用程序,您只需要自己设置配置等​​。

    @DataJpaTest
    class BetterUserServiceTest {
    
      private UserService userService;
    
      @BeforeEach
      void setup(@Autowired UserRepository userRepository) {
        userService = new UserService(userRepository);
      }
    
      @Test
      void saveUser() {
        String name = "Steve";
        String food = "Apple";
    
        User savedUser = userService.newUser(name, food);
    
        assertEquals(savedUser.getName(), name);
        assertEquals(savedUser.getFood(), food);
      }
    }
    

    在此示例中,我们更进一步,删除了任何模拟概念,并连接到内存数据库并验证返回的用户未更改为我们保存的内容。

    然而,用于测试的内存数据库存在限制,因为我们通常针对 MySQL、DB2、Postgres 等进行部署,其中列定义(例如)无法由内存数据库准确地重新创建每个“真正的”数据库。

    我们可以更进一步,使用 Testcontainers 启动我们将在运行时连接到的数据库的 docker 映像,并在测试中连接到它

    @DataJpaTest
    @Testcontainers(disabledWithoutDocker = true)
    class BestUserServiceTest {
    
      private UserService userService;
    
      @BeforeEach
      void setup(@Autowired UserRepository userRepository) {
        userService = new UserService(userRepository);
      }
    
      @Container private static final MySQLContainer<?> MY_SQL_CONTAINER = new MySQLContainer<>();
    
      @DynamicPropertySource
      static void setMySqlProperties(DynamicPropertyRegistry properties) {
        properties.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername);
        properties.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword);
        properties.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl);
      }
    
      @Test
      void saveUser() {
        String name = "Steve";
        String food = "Apple";
    
        User savedUser = userService.newUser(name, food);
    
        assertEquals(savedUser.getName(), name);
        assertEquals(savedUser.getFood(), food);
      }
    }
    

    现在我们正在准确地测试我们可以保存并让我们的用户使用真正的 MySQL 数据库。如果我们更进一步并引入变更日志等,这些也可以在这些测试中捕获。

    【讨论】:

    • 尝试让 UserService 的构造函数获取存储库并像第一个示例一样带走注释,结果是一样的,savedUser 为空。第二个示例(将存储库作为设置参数的示例)我不明白之后如何访问存储库以检查匹配项。
    • 第二个例子中你不需要。它由服务返回,save 调用返回通过 JPA 保存的实体,这就是为什么我建议 模拟存储库并使用某种集成测试
    • 还与您的第一个示例为空有关,而不是在测试方法之外设置答案,您可以再次重构要设置的答案并将其设置在测试方法内的局部变量中,这意味着不使用静态变量来保存断言测试所需的局部变量,这不是一个好习惯
    猜你喜欢
    • 1970-01-01
    • 2018-10-15
    • 1970-01-01
    • 1970-01-01
    • 2021-05-28
    • 1970-01-01
    • 1970-01-01
    • 2013-01-24
    • 1970-01-01
    相关资源
    最近更新 更多