【问题标题】:How can I properly mock a Principal object in Spring?如何在 Spring 中正确模拟 Principal 对象?
【发布时间】:2019-02-10 12:04:52
【问题描述】:

首先,我在一个名为 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();
    }
}

正如您在第二个 sn-p 中看到的那样,我尝试使用 UsernamePasswordAuthenticationToken 实现来模拟测试类中的 Principal 对象。

当我运行测试时,我得到一个 NullPointerException,并且堆栈跟踪将我从代码的第一个 sn-p 指向以下行:

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

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

有什么建议吗?

【问题讨论】:

  • 首先,使用@AuthenticationPrincipal 并且不要在控制器中进行多次不安全的强制转换。然后只需构造一个真实的、实际的UserDetails 实例并将其传递给控制器​​。使用 MockMvc 时,您需要阅读有关 Spring Security 集成的文档(需要将其添加到 standaloneSetup),并且您应该在测试方法上使用类似 @WithUserDetails 的内容。
  • @chrylis 使用@AuthenticationPrincipal User user 作为第二个参数而不是Principal principal 并在控制器方法中注释掉user 变量对我来说是诀窍。 User 类实现了UserDetails 接口,正如您在上面指定的那样。不再抛出异常。请在您的评论中创建一个答案,以便我将其标记为解决方案。

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


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

    创建一个实现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(.........;
    
    }
    

    【讨论】:

      【解决方案3】:

      您是否尝试使用...?

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

      【讨论】:

      • 我尝试使用该注释。但在这种情况下并没有真正帮助我。
      猜你喜欢
      • 2020-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-07
      • 2021-06-17
      相关资源
      最近更新 更多