如何在Spring中正确模拟Principal对象?

时间:2019-02-10 12:04:53

标签: java spring spring-boot spring-security mockito

首先,我在名为 RecipeController 的类中提供了以下终结点方法:

@RequestMapping(value = {"/", "/recipes"})
    public String listRecipes(Model model, Principal principal){
        List<Recipe> recipes;
        User user = (User)((UsernamePasswordAuthenticationToken)principal).getPrincipal();
        User actualUser = userService.findByUsername(user.getUsername());
        if(!model.containsAttribute("recipes")){
            recipes = recipeService.findAll();
            model.addAttribute("nullAndNonNullUserFavoriteRecipeList",
                    UtilityMethods.nullAndNonNullUserFavoriteRecipeList(recipes, actualUser.getFavoritedRecipes()));

            model.addAttribute("recipes", recipes);
        }

        if(!model.containsAttribute("recipe")){
            model.addAttribute("recipe", new Recipe());
        }

        model.addAttribute("categories", Category.values());
        model.addAttribute("username", user.getUsername());
        return "recipe/index";
    }

如上所示,该方法将 Principal 对象作为第二个参数。运行应用程序时,参数按预期指向非null对象。它包含有关应用程序中当前登录用户的信息。

我为 RecipeController 创建了一个测试类,称为 RecipeControllerTest 。此类包含一个名为 testListRecipes 的方法。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class RecipeControllerTest{

    @Mock
    private RecipeService recipeService;

    @Mock
    private IngredientService ingredientService;

    @Mock
    private StepService stepService;

    @Mock
    private UserService userService;

    @Mock
    private UsernamePasswordAuthenticationToken principal;

    private RecipeController recipeController;

    private MockMvc mockMvc;

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

        recipeController = new RecipeController(recipeService,
                ingredientService, stepService, userService);

        mockMvc = MockMvcBuilders.standaloneSetup(recipeController).build();
    }

    @Test
    public void testListRecipes() throws Exception {
        User user = new User();

        List<Recipe> recipes = new ArrayList<>();
        Recipe recipe = new Recipe();
        recipes.add(recipe);

        when(principal.getPrincipal()).thenReturn(user);
        when(userService.findByUsername(anyString()))
                .thenReturn(user);
        when(recipeService.findAll()).thenReturn(recipes);

        mockMvc.perform(get("/recipes"))
                .andExpect(status().isOk())
                .andExpect(view().name("recipe/index"))
                .andExpect(model().attributeExists("recipes"))
                .andExpect(model().attributeExists("recipe"))
                .andExpect(model().attributeExists("categories"))
                .andExpect(model().attributeExists("username"));

        verify(userService, times(1)).findByUsername(anyString());
        verify(recipeService, times(1)).findAll();
    }
}

在第二个片段中可以看到,我尝试使用 UsernamePasswordAuthenticationToken 实现在测试类中模拟 Principal 对象。

运行测试时,我得到一个 NullPointerException ,然后堆栈跟踪将我从代码的第一行指向以下行:

User user = (User)((UsernamePasswordAuthenticationToken)principal).getPrincipal();

即使我试图提供一个模拟对象,作为参数传递给 listRecipes 方法的主体对象仍然为空。

有什么建议吗?

3 个答案:

答案 0 :(得分:1)

Spring MVC的控制器参数非常灵活,这使您可以将查找信息的大部分责任放在框架上,并专注于编写业务代码。在这种情况下,虽然您可以 使用Principal作为方法参数,但通常最好使用实际的主体类:

public String listRecipes(Model model, @AuthenticationPrincipal User user)

要实际设置用户进行测试,您需要使用Spring Security,这意味着将.apply(springSecurity())添加到您的设置中。 (顺便说一下,这样的复杂性是我不喜欢使用standaloneSetup的主要原因,因为它要求您记住复制精确的生产设置。我建议编写实际的单元测试和/或全栈测试。)然后用@WithUserDetails注释测试,并指定测试用户的用户名。

最后,作为一个侧面说明,使用Querydsl可以大大简化此控制器模式,因为Spring可以注入一个Predicate来合并您要手动查找的所有过滤器属性,然后您就可以将该谓词传递给Spring Data存储库。

答案 1 :(得分:0)

您尝试使用...吗?

@Test
@WithMockUser(username = "my_principal")
public void testListRecipes() {
...

答案 2 :(得分:0)

创建一个实现Principal的类:

class PrincipalImpl implements Principal {

    @Override
    public String getName() {

        return "XXXXXXX";
    }

}

样本测试:

@Test
public void login() throws Exception {
    Principal principal = new PrincipalImpl();

    mockMvc.perform(get("/login").principal(principal)).andExpect(.........;

}