【问题标题】:unit test a interface implementation with mock in Spring Boot在 Spring Boot 中使用 mock 对接口实现进行单元测试
【发布时间】:2018-01-11 03:56:53
【问题描述】:

我正在尝试为 Spring Boot 中的服务编写一个简单的单元测试。

该服务调用存储库上的一个方法,该方法返回一个用户实例。 我正在尝试模拟存储库,因为我只想测试服务。

所以,Repository的代码:

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

服务接口

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

服务实现

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

单元测试

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

当我运行这个测试时,我得到了一个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.

我没有使用接口,而是尝试使用真正的实现;像这样改变测试:

@InjectMocks
UserServiceImpl userService;

现在,测试成功通过,但这似乎不对(至少对我而言)。 我喜欢测试 Spring Boot 正在使用的 UserService(假设在我的系统的新版本中,我实现了一个新的 UserServicePostgreSQLImpl - 现在我正在使用 MongoDB)。 (编辑:见问题底部的编辑)

我将单元测试更改如下:

@Autowired
@InjectMocks
UserService userService;

但现在我测试失败了:

Expected :model.User@383caf89
Actual   :null

由于某种原因,当我使用 @Autowired 时,UserRepository 模拟不起作用。 如果我将emailTest 更改为在我的数据库中使用真实的电子邮件, 测试通过。 当我使用@Autowired时, 测试使用的是真正的UserRepository,而不是UserRepositoryMock 版本。

有什么帮助吗?

编辑:查看@msfoster 和@dunni 的答案,思考得更好,我相信正确的方法是测试每个实现(在我的示例中,使用UserServiceImpl userService)。

【问题讨论】:

  • 你不是在测试实际的实现,是你说的吗?重点是测试你的接口实现。
  • 这正是我要说的!如果在测试中我使用UserServiceImpl userService,我可以测试真正的实现,但是如果我创建另一个 userService 实现,我将需要编写一个新的单元测试(好吧,这也是正确的),但我喜欢只测试Spring 正在使用的实现
  • 请问您为什么要这样做?似乎测试的目的不是测试服务,而是您的应用程序的一个方面。
  • 您想对实现进行单元测试。如果您稍后添加UserServicePostgreSQLImpl,您也会为该类添加另一个单元测试。如果你想编写一个集成测试,在运行时进行依赖注入,不要使用@InjectMocks,而只使用@Inject@Autowired,将实现注入你的测试类。然后,您可以使用 @MockBean 将您的存储库的模拟添加到 Spring 上下文中。
  • @msfoster 和 dunni ,是的,两者都是正确的。现在想得更好,我必须测试所有实现。关于单元测试和集成测试的这件事对我来说是新的!谢谢! (对我来说,这个问题可以结束了!)

标签: java spring unit-testing


【解决方案1】:

为了让您的 UserServiceImpl 在使用 @InjectMocks 对其进行注释时自动装配,那么它需要注册为 Spring bean 本身。您可以通过使用 @Service 注释 UserServiceImpl 类来最简单地做到这一点。

这将确保它被 Spring 引导配置中的组件扫描拾取。 (只要扫描包含您的服务类所在的包!)

【讨论】:

  • 抱歉,我更新了我的问题。 UserServiceImpl 类已经有 @Service 注释。
【解决方案2】:

您正在使用 SpringRunner 运行您的测试,但对于模拟,您并不需要 spring 上下文。试试下面的代码

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

【讨论】:

  • 也不行...Cannot instantiate @InjectMocks field named 'userService'. You haven't provided the instance at field declaration so I tried to construct the instance. However, I failed because: the type 'UserService' is an interface.
【解决方案3】:

这只是@Yogesh Badke 答案的一个变体。

虽然你在运行时使用的是 spring, 在单元测试期间不需要使用spring。 反而, 您可以模拟所有依赖项并在测试设置期间将它们设置为模拟 (使用反射或设置器,如果你有的话)。

下面是一些示例代码:

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

【讨论】:

    【解决方案4】:

    如果接口由多个类实现,则使用 Junit beans.xml 文件中的限定符名称(以下示例)运行相应的 Junit 测试用例。

    例子:

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

    在 Junit beans.xml 中

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

    其中 Lion 是接口 Animal 的实现类。

    【讨论】:

      【解决方案5】:

      实现类需要@InjectMocks。不是接口类。

      例子:

      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class UserServiceTest {
      
        @Mock
        UserRepository mockUserRepository;
      
        @InjectMocks
        UserServiceImpl userServiceImpl; ------> This is important
      }
      

      【讨论】: