简体   繁体   English

在 Spring Boot 中使用模拟单元测试接口实现

[英]unit test a interface implementation with mock in Spring Boot

I'm trying to write a simple unit test for a service in Spring Boot.我正在尝试为 Spring Boot 中的服务编写一个简单的单元测试。

The service calls a method on a repository which returns an instance of User.该服务调用存储库上的方法,该方法返回用户的实例。 I'm trying to mock the repository, because I want to test only the service.我正在尝试模拟存储库,因为我只想测试服务。

So, the code for Repository :因此, Repository的代码:

public interface UserRepository extends MongoRepository<User, String> {
  User findByEmail(String email);
}

Service interface :服务接口

public interface UserService {
  @Async
  CompletableFuture<User> findByEmail(String email) throws InterruptedException;
}

Service implementation :服务实施

@Service
public class UserServiceImpl implements UserService {
  private UserRepository userRepository;

  // dependency injection
  // don't need Autowire here
  // https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-spring-beans-and-dependency-injection.html
  public UserServiceImpl(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Async
  public CompletableFuture<User> findByEmail(String email) throws InterruptedException {
    User user = userRepository.findByEmail(email);
    return CompletableFuture.completedFuture(user);
  }
}

Unit Test :单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

  @InjectMocks
  UserService userService;

  @Mock
  UserRepository mockUserRepository;

  @Before
  public void setUp() {
    MockitoAnnotations.initMock(this);
  }

  @Test
  public void mustReturnUser() throws InterruptedException {
    String emailTest = "foo@bar.com";

    User fakeUser = new User();
    fakeUser.setEmail(emailTest);

    when(mockUserRepository.findByEmail(emailTest)).thenReturn(fakeUser);

    User user = userService.findByEmail(emailTest).join();
    assertThat(user).isEqualTo(fakeUser);

    verify(mockUserRepository).findByEmail(emailTest);
  }
}

When I run this test, I got a MockitoException :当我运行这个测试时,我得到了一个MockitoException

org.mockito.exceptions.base.MockitoException: 
Cannot instantiate @InjectMocks field named 'userService'.
...
Caused by: org.mockito.exceptions.base.MockitoException: the type 'UserService' is an interface.

Instead of using the interface, I tried to use the real implementation;我没有使用接口,而是尝试使用真正的实现; changing the test like this:像这样改变测试:

@InjectMocks
UserServiceImpl userService;

Now, the test passes with success, but this don't appear be right (at least for me).现在,测试成功通过,但这似乎不正确(至少对我而言)。 I like to test the UserService that Spring Boot is using (suppose that in a new version of my system, I implement a new UserServicePostgreSQLImpl - now I'm using MongoDB).我喜欢测试 Spring Boot 正在使用的 UserService(假设在我的系统的新版本中,我实现了一个新的 UserServicePostgreSQLImpl - 现在我正在使用 MongoDB)。 (edit: see the bottom edit in the question) (编辑:请参阅问题中的底部编辑)

I changed the Unit Test as follows:我改变了单元测试如下:

@Autowired
@InjectMocks
UserService userService;

but now I got a test failure:但现在我测试失败了:

Expected :model.User@383caf89
Actual   :null

For some reason, when I use @Autowired , the UserRepository mock doesn't work.出于某种原因,当我使用@AutowiredUserRepository模拟不起作用。 If I change the emailTest to use a real email in my database, the test passes.如果我将emailTest更改为在我的数据库中使用真实的电子邮件,则测试通过。 When I use @Autowired , the test is using the real UserRepository and not a Mock version of UserRepository .当我使用@Autowired ,测试使用真实UserRepository而不是Mock版本的UserRepository

Any help?有什么帮助吗?

Edit: looking at the answers from @msfoster and @dunni, and thinking better, I believe that the correct way is to test every implementation (in my example, use UserServiceImpl userService ).编辑:查看@msfoster 和@dunni 的答案,并更好地思考,我相信正确的方法是测试每个实现(在我的示例中,使用UserServiceImpl userService )。

In order for your UserServiceImpl to be autowired when annotating it with @InjectMocks then it needs to registered as a Spring bean itself.为了在使用 @InjectMocks 注释时自动装配 UserServiceImpl,它需要注册为 Spring bean 本身。 You can do this most simply by annotating your UserServiceImpl class with @Service.您可以通过使用 @Service 注释 UserServiceImpl 类来最简单地做到这一点。

This will ensure it is picked up by the component scan in your Spring boot configuration.这将确保它被 Spring boot 配置中的组件扫描选中。 (As long as the scan includes the package your service class is in!) (只要扫描包含您的服务类所在的包!)

You are running your tests with SpringRunner but for mocks you don't really need spring context.您正在使用SpringRunner运行测试,但对于SpringRunner ,您实际上并不需要 spring 上下文。 Try following code试试下面的代码

// Using mockito runner
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

  @Mock
  UserRepository mockUserRepository;

  // Mockito will auto inject mockUserRepository mock to userService via constructor injection
  @InjectMocks
  UserService userService;

  @Test
  public void mustReturnUser() throws InterruptedException {
    String emailTest = "foo@bar.com";

    User fakeUser = new User();
    fakeUser.setEmail(emailTest);

    when(mockUserRepository.findByEmail(emailTest)).thenReturn(fakeUser);

    User user = userService.findByEmail(emailTest).join();
    assertThat(user).isEqualTo(fakeUser);

    verify(mockUserRepository).findByEmail(emailTest);
  }
}

This is just a variation on the @Yogesh Badke answer.这只是@Yogesh Badke 答案的一个变体。

Although you are using spring at runtime, there is no need to use spring during the unit test.虽然你在运行时使用的是spring,但是单元测试时没有必要使用spring。 Instead, you can mock all the dependencies and set them to the mocks during test setup (using reflection or setters, if you have them).相反,您可以在测试设置期间模拟所有依赖项并将它们设置为模拟(使用反射或设置器,如果有的话)。

Here is some example code:下面是一些示例代码:

import org.springframework.test.util.ReflectionTestUtils;

public class TestUserService
{
    private static final String VALUE_EMAIL = "test email value";

    private UserService classToTest;

    @Mock
    private User mockUser;

    @Mock
    private UserRepository mockUserRepository;

    @Before
    public void beforeTest()
    {
        MockitoAnnotations.initMock(this);

        classToTest = new UserService();

        doReturn(mockUser).when(mockUserRepository).findByEmail(VALUE_EMAIL);

        ReflectionTestUtils.setField(
            classToTest,
            "userRepository",
            mockUserRepository);
    }

    @Test
    public void findByEmail_goodEmailInput_returnsCorrectUser()
    {
        final User actualResult;


        actualResult = classToTest.findByEmail(VALUE_EMAIL);


        assertSame(
            mockUser,
            actualResult);
    }
}

If interface is implemented by more than one class, then use the qualifier name (example below) in Junit beans.xml file to run the respective Junit test case.如果接口由多个类实现,则使用 Junit beans.xml 文件中的限定符名称(下面的示例)来运行相应的 Junit 测试用例。

Example:示例:

 @Autowired
    @Qualifier("animal")
    private Animal animals;

In Junit beans.xml在 Junit beans.xml 中

  <bean id="animal" class="com.example.abc.Lion"/>

where Lion is the implementation class for the Interface Animal.其中 Lion 是 Interface Animal 的实现类。

You need to @InjectMocks for the implementation class.您需要为实现类使用@InjectMocks Not the interface class.不是接口类。

Example:示例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

  @Mock
  UserRepository mockUserRepository;

  @InjectMocks
  UserServiceImpl userServiceImpl; ------> This is important
}

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

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