首页 > 解决方案 > 如何在 Spring 中正确模拟 Principal 对象?

问题描述

首先,我在名为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对象作为第二个参数。运行应用程序时,参数按预期指向非空对象。它包含有关当前在应用程序中登录的用户的信息。

我为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方法的主体对象仍然为空。

有什么建议么 ?

标签: javaspringspring-bootspring-securitymockito

解决方案


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

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

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

最后,作为旁注,可以使用 Querydsl 显着简化此控制器模式,因为 Spring 能够注入一个Predicate结合了您手动查找的所有过滤器属性的 a,然后您可以将该谓词传递给 Spring Data 存储库.


推荐阅读