繁体   English   中英

将存储库的模拟注入服务不会注入正确的模拟方法(Spring、JUnit 和 Mockito)

[英]Injecting Mock of Repository into Service doesn't inject the proper mocked method (Spring, JUnit and Mockito)

tl;dr :似乎我使用自定义行为创建的存储库的 Mock 关于注入时保存方法的自定义行为丢失了自定义行为。


问题描述

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

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

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

我基于这个问题来保存User

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注释,因为它开始未能通过测试,我认为这是因为它将以通常的方式创建一个模拟(没有我之前创建的自定义方法)。

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

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

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

@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记录 function。

我的测试记录打印,表明用户测试没有调用answer方法


我在这里做错了什么?

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

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

@InjectMocks
@Autowired
private UserService service

@MockBean
private UserRepository mockUserRepo

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

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

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

最佳实践是,

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

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

请注意,此处代码的工作示例可以在此 GitHub 存储库中找到。

清理代码并使其更易于测试的一种简单方法是更正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 数据存储库的复杂性,毕竟您也是 mocking 网络 I/O,这是一个危险的事情

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

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

@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);
  }
}

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

然而,用于测试的内存数据库存在局限性,因为我们通常针对 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 数据库。 如果我们更进一步并引入变更日志等,这些也可以在这些测试中捕获。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM