【问题标题】:Unit testing methods using Principal from Spring Secucrity使用 Spring Security 的 Principal 的单元测试方法
【发布时间】:2017-06-28 03:28:39
【问题描述】:

谁能告诉我应该如何测试这两种方法:

public boolean deleteUser(Principal principal) {
    if (findLoggedInUser(principal) != null) {
        userRepository.delete(findLoggedInUser(principal));
        return true;
    }
    return false;
}

public User findLoggedInUser(Principal principal) {
    return findUserbyUsername(principal.getName());
}

问题是我正在使用具有基本身份验证的当前登录用户并且不知道如何以及是否可以模拟这些委托人。有没有办法做到这一点?这些方法在我的服务层中,所以也许我不能进行单元测试,而我只能进行集成测试,因为这些方法大量使用 DB?

编辑 1: 我更改的测试类:

public class UserServiceBeanTest {

@Spy
@InjectMocks
private UserServiceBean userServiceBean;

@Mock
private UserRepository userRepository;

@Mock
private Principal principal;

@Mock
private PasswordEncoder passwordEncoder;

@Mock
private User userStub;

private String defaultName = "user";
private  String defaultPassword = "password";
private String defaultEmail = "example@example.com";

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


@Test
public void shouldReturnTrue_whenUserDeleted() {
    //given
    when(principal.getName()).thenReturn(defaultName);
    when(userServiceBean.findLoggedInUser(principal)).thenReturn(userStub);

    // when
    boolean removed = userServiceBean.deleteUser(principal);

    //then
    assertTrue(removed);
    verify(userRepository, times(1));
}

@Test
public void shouldReturnFalse_whenUserNotFound() {
    //given
    when(principal.getName()).thenReturn(defaultName);
    when(userServiceBean.findLoggedInUser(principal)).thenReturn(null);

    //when
    boolean removed = userServiceBean.deleteUser(principal);

    //then
    assertFalse(removed);
    verify(userRepository, times(0));
}

}

我现在遇到了这些错误:

org.mockito.exceptions.misusing.UnfinishedVerificationException: 

失踪

g method call for verify(mock) here:
-> at com.doublemc.services.UserServiceBeanTest.shouldReturnTrue_whenUserDeleted(UserServiceBeanTest.java:63)

Example of correct verification:
    verify(mock).doSomething()

Also, this error might show up because you verify either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.


    at com.doublemc.services.UserServiceBeanTest.init(UserServiceBeanTest.java:48)

编辑 2: 这是我的 UserServiceBean 类:

package com.doublemc.services;

import com.doublemc.domain.ToDoItem;
import com.doublemc.domain.User;
import com.doublemc.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.security.Principal;

@Service
@Transactional
public class UserServiceBean {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Autowired
UserServiceBean(UserRepository userRepository, PasswordEncoder passwordEncoder) {
    this.userRepository = userRepository;
    this.passwordEncoder = passwordEncoder;
}

public User saveUser(User user) {
    User newUser = new User();
    newUser.setUsername(user.getUsername());
    newUser.setEmail(user.getEmail());
    newUser.setPassword(passwordEncoder.encode(user.getPassword()));
    return userRepository.save(newUser);
}

public boolean userExists(User user) {
    return userRepository.findByUsername(user.getUsername()) != null;
}

public Iterable<ToDoItem> getAllToDoItems(User user) {
    return user.getToDoItems();
}

public boolean deleteUser(Principal principal) {
    if (findLoggedInUser(principal) != null) {
        userRepository.delete(findLoggedInUser(principal));
        return true;
    }
    return false;
}

public User findLoggedInUser(Principal principal) {
    return userRepository.findByUsername(principal.getName());
}

}

这是我的 UserRepository:

public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}

编辑 6:我为自己创建了另外三个测试:

@Test
public void shouldReturnUser_whenPassedUser() {
    // given
    when(userRepository.save(any(User.class))).thenReturn(new User(defaultName, defaultPassword, defaultEmail));

    // when
    User savedUser = userServiceBean.saveUser(userStub);

    // then
    assertNotNull(savedUser);
    verify(userRepository, times(1)).save(any(User.class));
}

@Test
public void shouldReturnTrue_whenUserExists() {
    // given
    when(userStub.getUsername()).thenReturn(defaultName);
    when(userRepository.findByUsername(userStub.getUsername())).thenReturn(userStub);

    // when
    boolean exists = userServiceBean.userExists(userStub);

    // then
    assertTrue(exists);
    verify(userRepository, times(1)).findByUsername(defaultName);
}

@Test
public void shouldReturnFalse_whenUserNotFoundByUsername() {
    // given
    when(userStub.getUsername()).thenReturn(defaultName);
    when(userRepository.findByUsername(userStub.getUsername())).thenReturn(null);

    // when
    boolean exists = userServiceBean.userExists(userStub);

    // then
    assertFalse(exists);
    verify(userRepository, times(1)).findByUsername(defaultName);
}

以下是经过测试的方法: UserServiceBean.saveUser

public User saveUser(User user) {
    User newUser = new User(user.getUsername(), user.getEmail(), passwordEncoder.encode(user.getPassword()));
    return userRepository.save(newUser);
}

UserServiceBean.userExists

public boolean userExists(User user) {
    return userRepository.findByUsername(user.getUsername()) != null;
}

【问题讨论】:

  • 测试一下他们呢?
  • 好吧,如果例如 deleteUser 方法实际上删除了一个用户 - 这就是测试的目的,对吧?
  • 不,您不应该测试deleteUser 是否确实删除了用户:您应该检查它是否使用您期望的参数调用了userRepository.delete。是否真正删除用户取决于userRepository的实现。
  • 因此,如果我已经测试了存储库,那么我不必在我的服务层中测试与控制器和存储库通信的任何其他方法?
  • 什么是Principal?有几种类型命名。

标签: java spring unit-testing spring-security mockito


【解决方案1】:

我就是这样做的(Junit + Mockito)。

在给定的例子中有两个测试用例。

顺便说一句..我认为您可以在(我猜)两次访问数据库时进行一些重构:

public boolean deleteUser(Principal principal) {
    User loggedUser = findLoggedInUser(principal);
    if (loggedUser != null) {
        userRepository.delete(loggedUser);
        return true;
    }
    return false;
}

到测试..

import static org.mockito.Mockito.*;
import mypkg.Service;
import mypkg.User;
import mypkg.UserRepository;
import org.junit.Assert;
import org.junit.Before;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;

import java.security.Principal;

public class ServiceTest {

    @Spy
    @InjectMocks
    private Service service;

    @Mock
    private UserRepository userRepository;

    @Mock
    private Principal principal;

    @Mock
    private User userStub;

    private String defaultName = "name";

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

   @org.junit.Test
   public void shouldReturnTrue_whenUserDeleted() throws Exception{
       // Arrange
       when(principal.getName()).thenReturn(defaultName);
       when(service.findUserbyUsername(defaultName)).thenReturn(userStub);

       // Act
       boolean removed = service.deleteUser(principal);

       // Assert
       Assert.assertTrue(removed);
       verify(userRepository, times(1)).delete(userStub);
   }

    @org.junit.Test
    public void shouldReturnFalse_whenUserNotFound() throws Exception{
        // Arrange
        when(principal.getName()).thenReturn(defaultName);
        when(service.findUserbyUsername(defaultName)).thenReturn(null);

        // Act
        boolean removed = service.deleteUser(principal);

        // Assert
        Assert.assertFalse(removed);
        verify(userRepository, times(0)).delete(userStub);
    }
}

最大的好处是你模拟/存根任何外部依赖项(在本例中为 UserRepository)并只关注该服务方法中包含的逻辑。 delete 内部的内容与测试无关。您只关心是否已使用某个参数调用了该方法。仅此而已。

如果一切都清楚,请告诉我。如果需要,我会解释。

更新

@InjectMocks 是一种将依赖项注入您要测试的类的便捷方法。注入由 setter/constructor 或作为最后手段通过反射发生。

在上面的示例中,Service 类具有 UserRepository 依赖项,并且定义了一个 @Mock:

@Mock
private UserRepository userRepository;

Mockito 会将其注入Service

@Spy 类似于@Mock,只是它允许您选择性地仅模拟某些 bahvior,并且默认情况下会调用真正的实现。

在我的例子中,我用它来模拟 ServicefindUserbyUsername 方法,因为在我们的两个测试中,里面的内容并不重要:

when(service.findUserbyUsername(defaultName)).thenReturn(userStub);

【讨论】:

  • 所以我应该对我的所有服务/控制器方法使用这种测试(我的意思是使用 mockito)?所以我实际上并没有使用数据库,只是模拟存储库?
  • 是的。如果您确实想在测试中包含数据库或类的任何其他外部依赖项,那么您将进行集成测试。这也很重要,但首先你需要有一个良好的单元测试覆盖率,然后再升级并开始编写更高级别的集成测试。
  • 您能解释一下为什么要使用@Spy 和@InjectMocks 注释吗?我找到了一些关于他们的信息,但仍然很不清楚。
  • 我的帖子也添加了编辑,我不得不更改验证 - 可以吗?
  • 我已经添加了 Spy 和 IncjectMocks 信息。你可以添加 UserRepository.delete(User) 代码吗?请带进口
猜你喜欢
  • 2016-06-05
  • 2010-09-26
  • 2017-10-21
  • 1970-01-01
  • 2018-12-13
  • 2013-10-18
  • 2012-06-15
  • 2018-09-30
  • 2021-01-25
相关资源
最近更新 更多