首先,我在一个名为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();
作为参数传递给listRerecies方法的主体对象仍然是空的,即使我试图提供一个模拟对象。
有什么建议吗?
创建一个实现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(.........;
}
Spring MVC在控制器参数方面非常灵活,这使您可以将查找信息的大部分责任放在框架上,并专注于编写业务代码。在这种特殊情况下,虽然可以使用Principal
作为方法参数,但通常最好使用实际的主体类:
public String listRecipes(Model model, @AuthenticationPrincipal User user)
要实际设置用户进行测试,您需要使用 Spring 安全性,这意味着将.apply(springSecurity())
添加到您的设置中。(顺便说一下,像这样的复杂情况是我不喜欢使用的主要原因standaloneSetup
,因为它需要您记住复制确切的生产设置。我建议编写实际的单元测试和/或全栈测试。然后用@WithUserDetails
批注测试并指定测试用户的用户名。
最后,作为旁注,这个控制器模式可以用Querydsl大大简化,因为Spring能够注入一个Predicate
,它结合了你手动查找的所有过滤器属性,然后你可以将该谓词传递给Spring Data存储库。
你试过使用...吗?
@Test
@WithMockUser(username = "my_principal")
public void testListRecipes() {
...