简体   繁体   English

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

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

tl;dr : Seems like the Mock of the repository I created with custom behavior regarding the save method when injected loses the custom behavior. tl;dr :似乎我使用自定义行为创建的存储库的 Mock 关于注入时保存方法的自定义行为丢失了自定义行为。


Problem Description问题描述

I've been trying to test a Service in Spring.我一直在尝试在 Spring 中测试服务。 The method of interest in particular takes some parameters and creates a User that is saved into a UserRepository through the repository method save .特别感兴趣的方法采用一些参数并创建一个用户,该用户通过存储库方法save保存到UserRepository中。

The test I am interest in making is comparing these parameters to the properties of the User passed to the save method of the repository and in this way check if it is properly adding a new user.我感兴趣的测试是将这些参数与传递给存储库的保存方法的用户属性进行比较,并以这种方式检查它是否正确添加了新用户。

For that I decided to Mock the repository and save the param passed by the service method in question to the repository save method.为此,我决定模拟存储库并将相关服务方法传递的参数保存到存储库保存方法。

I based myself on this question to save the User .我基于这个问题来保存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();

Two notes:两个注意事项:

  • I gave the name repo in case the name had to match the one in the service.我给出了名称 repo,以防名称必须与服务中的名称匹配。

  • There is no @Mock annotation since it starts failing the test, I presume that is because it will create a mock in the usual way (without the custom method I created earlier).没有@Mock注释,因为它开始未能通过测试,我认为这是因为它将以通常的方式创建一个模拟(没有我之前创建的自定义方法)。

I then created a test function to check if it had the desired behavior and all was good.然后我创建了一个测试 function 来检查它是否具有所需的行为并且一切都很好。

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

Then I tried doing what I saw recommended across multiple questions, that is, to inject the mock into the service as explained here .然后,我尝试按照我在多个问题中看到的建议进行操作,即将模拟注入到服务中,如此所述。

@InjectMocks
private UserService service = new UserService();

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

This is where the problems arise, the test I created for it throws a null exception when I try to access savedUser properties (here I simplified the users properties since that doesn't seem to be the cause).这就是问题出现的地方,当我尝试访问savedUser属性时,我为其创建的测试会引发null 异常(这里我简化了用户属性,因为这似乎不是原因)。

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

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

Upon debugging:调试时:

I decided to log the function with System.out.println for demonstrative purposes.出于演示目的,我决定使用System.out.println记录 function。

A print of my logging of the tests, demonstrating that the user test doesn't call the answer method我的测试记录打印,表明用户测试没有调用answer方法


What am I doing wrong here?我在这里做错了什么?

Thank you for the help in advance, this is my first stack exchange question any tips for improvement are highly appreciated.提前感谢您的帮助,这是我的第一个堆栈交换问题,非常感谢任何改进提示。

Instead of instanciating your service in the test class like you did, use @Autowired and make sure your UserRepository has @MockBean in the test class不要像您那样在测试 class 中实例化您的服务,而是使用 @Autowired 并确保您的 UserRepository 在测试 class 中有 @MockBean

@InjectMocks
@Autowired
private UserService service

@MockBean
private UserRepository mockUserRepo

With this, you can remove your setup method有了这个,你可以删除你的设置方法

But make sure your UserRepository is also autowired insider your Service但请确保您的 UserRepository 也在您的服务内部自动装配

You should not need Spring to test of this.你不应该需要 Spring 来测试这个。 If you are following Spring best practicies when it comes to autowiring dependencies you should be able just create the objects yourself and pass the UserRepository to the UserService如果您在自动装配依赖项时遵循 Spring 最佳实践,您应该能够自己创建对象并将UserRepository传递给UserService

Best practices being,最佳实践是,

  • Constructor injection for required beans所需bean的构造函数注入
  • Setter injection for optional beans可选 bean 的 Setter 注入
  • Field injection never unless you cannot inject to a constructor or setter, which is very very rare.除非您不能注入构造函数或设置器,否则永远不会进行字段注入,这是非常罕见的。

Note that InjectMocks is not a dependency injection framework and I discourage its use.请注意, InjectMocks不是依赖注入框架,我不鼓励使用它。 You can see in the javadoc that it can get fairly complex when it comes to constructor vs. setter vs. field.您可以在 javadoc中看到,当涉及到构造函数、setter 和字段时,它会变得相当复杂。

Note that working examples of the code here can be found in this GitHub repo.请注意,此处代码的工作示例可以在此 GitHub 存储库中找到。

A simple way to clean up your code and enable it to be more easily tested would be to correct the UserService to allow you to pass whatever implementation of a UserRepository you want, this also allows you to gaurentee immuability,清理代码并使其更易于测试的一种简单方法是更正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);
  }
}

and then your test would be made more simple,然后你的测试会变得更简单,

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

However, this doesn't add a lot of benefit in my opinion as you are interacting with the repository directly in the service unless you fully understand the complexity of a Spring Data Repository, you are after all also mocking networking I/O which is a dangerous thing to do但是,在我看来,这并没有增加很多好处,因为您直接在服务中与存储库进行交互,除非您完全了解 Spring 数据存储库的复杂性,毕竟您也是 mocking 网络 I/O,这是一个危险的事情

  • How do @Id annotations work? @Id 注释是如何工作的?
  • What about Hibernate JPA interact with my Entitiy? Hibernate JPA 与我的实体交互怎么样?
  • Do my column definitions on my Entitiy match what I would deploy against when using something like Liquibase/Flyway to manage the database migrations?我在实体上的列定义是否与我在使用 Liquibase/Flyway 之类的东西来管理数据库迁移时部署的内容相匹配?
  • How do I test against any constraints the database might have?如何针对数据库可能具有的任何约束进行测试?
  • How do I test custom transactional boundaries?如何测试自定义事务边界?

You're baking in a lot of assumptions, to that end you could use the @DataJpaTest documentation annotation that Spring Boot provides, or replicate the configuration.您做了很多假设,为此您可以使用 Spring Boot 提供的@DataJpaTest 文档注释,或复制配置。 A this point I am assuming a Spring Boot application, but the same concept applies to Spring Framework applications you just need to setup the configurations etc. yourself.在这一点上,我假设一个 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);
  }
}

In this example we've went a step further and removed any notion of mocking and are connecting to an in-memory database and verifying the user that is returned is not changed to what we saved.在此示例中,我们更进一步,删除了 mocking 的任何概念,并连接到内存数据库并验证返回的用户未更改为我们保存的用户。

Yet there are limitations with in-memory databases for testing, as we are normally deploying against something like MySQL, DB2, Postgres etc. where column definitions (for example) cannot accurately be recreated by an in-memory database for each "real" database.然而,用于测试的内存数据库存在局限性,因为我们通常针对 MySQL、DB2、Postgres 等进行部署,其中列定义(例如)无法由内存数据库为每个“真实”数据库准确地重新创建.

We could take it a step further and use Testcontainers to spin up a docker image of a database that we would connecting to at runtime and connect to it within the test我们可以更进一步,使用 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);
  }
}

Now we are accurately testing we can save, and get our user against a real MySQL database.现在我们正在准确地测试我们可以保存,并让我们的用户对照真实的 MySQL 数据库。 If we took it a step further and introduced changelogs etc. those could also be captured in these tests.如果我们更进一步并引入变更日志等,这些也可以在这些测试中捕获。

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

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