Daniel
Daniel

Reputation: 125

Mocking a void method in the Spring Framework (Mockito)

I'm writing integration tests for a spring web app and I have reached a step where I need to mock service methods calls which have a void return type. I've done some research on some ways to do this but none seem to be the correct way.

What I want to do is:

Below I'll provide the code and also the two main ways I've tried already. If anyone can help that would be great!

The method that needs mocking

@RequestMapping(path = "/recipes/add", method = RequestMethod.POST)
public String persistRecipe(@Valid Recipe recipe, BindingResult result, @RequestParam("image") MultipartFile photo, RedirectAttributes redirectAttributes) {
    if (result.hasErrors()) {
        redirectAttributes.addFlashAttribute("recipe", recipe);
        redirectAttributes.addFlashAttribute("flash",
                new FlashMessage("I think you missed something. Try again!", FlashMessage.Status.FAILURE));
        return "redirect:/recipes/add";
    }

    User user = getUser();
    recipe.setOwner(user);
    user.addFavorite(recipe);
    recipeService.save(recipe, photo);
    userService.save(user);
    redirectAttributes.addFlashAttribute("flash", new FlashMessage("The recipe has successfully been created", FlashMessage.Status.SUCCESS));

    return "redirect:/recipes";
}

The service that needs calling (save method)

@Service
public class RecipeServiceImpl implements RecipeService {

private final RecipeRepository recipes;

@Autowired
public RecipeServiceImpl(RecipeRepository recipes) {
    this.recipes = recipes;
}

@Override
public void save(Recipe recipe, byte[] photo) {
    recipe.setPhoto(photo);
    recipes.save(recipe);
}

@Override
public void save(Recipe recipe, MultipartFile photo) {
    try {
        recipe.setPhoto(photo.getBytes());
        recipes.save(recipe);
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

@Override
public Recipe findById(Long id) {
    Optional<Recipe> recipe = recipes.findById(id);
    if (recipe.isPresent()) {
        return recipe.get();
    }

    // TODO:drt - Create new exception to handle this
    throw new RuntimeException();
}

@Override
public Recipe findByName(String name) {
    return null;
}

@Override
public List<Recipe> findAll() {
    return (List<Recipe>) recipes.findAll();
}

@Override
public void deleteById(Long id) {
    recipes.deleteById(id);
}

}

Attempt 1

@Test
@WithMockUser(value = "daniel")
public void createNewRecipeRedirects() throws Exception {
    User user = userBuilder();
    Recipe recipe = recipeBuilder(1L);
    recipe.setOwner(user);
    user.addFavorite(recipe);
    MockMultipartFile photo = new MockMultipartFile("image", "food.jpeg",
                    "image/png", "test image".getBytes());

    when(userService.findByUsername("daniel")).thenReturn(user);

    doAnswer(new Answer<Void>() {

        @Override
        public Void answer(InvocationOnMock invocation) throws Throwable {
            Object[] arguments = invocation.getArguments();
            if (arguments != null && arguments.length > 1 && arguments[0] != null && arguments[1] != null) {

                Recipe recipe1 = (Recipe) arguments[0];
                MultipartFile file = (MultipartFile) arguments[1];
                recipe1.setPhoto(file.getBytes());

            }
            return null;
        }
    }).when(recipeService).save(any(Recipe.class), any(MultipartFile.class));

    mockMvc.perform(post("/recipes/add"))
            .andExpect(redirectedUrl("/recipes"))
            .andExpect(flash().attributeExists("flash"));


}

Attempt 2

@Test
@WithMockUser(value = "daniel")
public void createNewRecipeRedirects() throws Exception {
    List<Recipe> recipes = recipeListBuilder();
    List<User> users = new ArrayList<>();
    User user = userBuilder();
    Recipe recipe = recipeBuilder(1L);
    recipe.setOwner(user);
    user.addFavorite(recipe);
    MockMultipartFile photo = new MockMultipartFile("image", "food.jpeg",
                    "image/png", "test image".getBytes());

    when(userService.findByUsername("daniel")).thenReturn(user);

    doAnswer(answer -> {
        recipe.setPhoto(photo.getBytes());
        recipes.add(recipe);
        return true;
    }).when(recipeService).save(any(Recipe.class), any(MultipartFile.class));

    doAnswer(answer -> {
        users.add(user);
        return true;
    }).when(userService).save(any(User.class));

    mockMvc.perform(post("/recipes/add"))
            .andExpect(redirectedUrl("/recipes"))
            .andExpect(flash().attributeExists("flash"));

    assertEquals(3, recipes.size());
    assertEquals(1, users.size());
}

Complete test code so far

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@WebAppConfiguration
public class RecipeControllerTests {

private MockMvc mockMvc;

@Mock
private RecipeService recipeService;

@Mock
private UserService userService;

@Mock
private IngredientService ingredientService;

@Autowired
WebApplicationContext wac;

@InjectMocks
private RecipeController recipeController;

@Before
public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
}

/**
 * Tests for index pages / & /recipes
 */
@Test
@WithUserDetails(value = "daniel")
public void indexPageLoads() throws Exception {
    List<Recipe> recipes = recipeListBuilder();
    List<Ingredient> ingredients = ingredientsListBuilder();

    when(recipeService.findAll()).thenReturn(recipes);
    when(ingredientService.findAll()).thenReturn(ingredients);
    when(userService.findByUsername("daniel")).thenReturn(userBuilder());

    mockMvc.perform(get("/recipes"))
            .andExpect(model().attributeExists("recipes", "ingredients", "favs"))
            .andExpect(status().isOk());
}

/**
 * Tests for page /recipes/add
 */
@Test
@WithMockUser
public void addRecipePageLoads() throws Exception {
    mockMvc.perform(get("/recipes/add"))
            .andExpect(model().attributeExists("task", "buttonAction", "action", "photo", "recipe"))
            .andExpect(status().isOk());
}

@Test
@WithUserDetails("daniel")
public void createNewRecipeRedirects() throws Exception {
    User user = userBuilder();
    Recipe recipe = recipeBuilder(1L);
    recipe.setOwner(user);
    user.addFavorite(recipe);
    MultipartFile photo = new MockMultipartFile("image", "food.jpeg",
            "image/jpeg", "dummy content file".getBytes());

    when(userService.findByUsername("daniel")).thenReturn(user);
    verify(recipeService, times(1)).save(recipe, photo);
    verify(userService, times(1)).save(user);


    mockMvc.perform(post("/recipes/add"))
            .andExpect(redirectedUrl("/recipes"))
            .andExpect(flash().attributeExists("flash"));


}


private User userBuilder() {
    User user = new User();
    user.setFavorites(recipeListBuilder());
    user.setId(1L);
    user.setRoles(new String[]{"ROLE_USER", "ROLE_ADMIN"});
    user.setUsername("daniel");
    user.setPassword("password");

    return user;
}

private List<Recipe> recipeListBuilder() {
    List<Recipe> recipes =  new ArrayList<>();
    recipes.add(recipeBuilder(1L));
    recipes.add(recipeBuilder(2L));

    return recipes;
}

private List<Ingredient> ingredientsListBuilder() {
    List<Ingredient> ingredients = new ArrayList<>();
    ingredients.add(ingredientBuilder());

    return ingredients;
}

private Ingredient ingredientBuilder() {
    Ingredient ingredient = new Ingredient();
    ingredient.setCondition("good");
    ingredient.setName("test ing");
    ingredient.setQuantity(1);
    ingredient.setId(1L);

    return ingredient;
}

private Recipe recipeBuilder(Long id) {
    Recipe recipe = new Recipe();
    recipe.setName("Test recipe");
    recipe.setDescription("Test Description");
    recipe.setId(id);
    recipe.setCategory(Category.ALL_CATEGORIES);
    recipe.setCookTime(10);
    recipe.setPrepTime(10);
    recipe.addIngredient(ingredientBuilder());

    return recipe;
}
}

Upvotes: 0

Views: 13965

Answers (3)

Michał Krzywański
Michał Krzywański

Reputation: 16910

If you have some logic that you want to unit test and this logic invokes methods of other component that you want to mock and some of those methods return void - the typical way of testing your logic is to verify that your logic actually invoked the void methods of mocked object. You can achieve this by using Mockito::verify :

 Mockito.verify(recipeService, Mockito.times(1)).save(any(Recipe.class), any(MultipartFile.class));

This way you test that logic of persistRecipe() method actually invoked the desired method on your mock object.

Upvotes: 0

Willem
Willem

Reputation: 1060

try Mockito.doNothing(): it basically tells Mockito to do nothing when a method in a mock object is called:

Mockito.doNothing().when(recipeService).save(any(Recipe.class), any(MultipartFile.class));

Upvotes: 2

Gavin
Gavin

Reputation: 1767

If you are mocking the save method I would use one of the ‘do...‘ Docs Of course this suggests your method has a side effect somewhere.

If you want to ensure a method is called the you can use ‘verify‘ as mentioned in other answers.

Generally speaking mocking allows you to replace some collaboration/functionality with a version which is under the tests control where as verifying allows for checking something occurred (or didn’t)

Upvotes: 0

Related Questions