Использование mockito для тестирования больших сервисов в Spring - PullRequest
0 голосов
/ 13 декабря 2018

Я начал изучать мокито для проверки своих классов.Я знаю, как это сделать с маленькими классами с одним, может быть, с двумя издевательствами, но у меня есть проблема, когда мой сервис намного больше.Например, у меня есть сервис

public class ShoppingListService {

    Map<Ingredient, Long> shoppingList = new HashMap<>();
    List<MealInfo> meals = new ArrayList<>();
    UserInfoService userInfoService;
    DietMealsService dietMealsService;
    UserRepository userRepository;
    User user;

    @Autowired
    public ShoppingListService(UserInfoService userInfoService, DietMealsService dietMealsService,UserRepository userRepository) {
        this.userInfoService = userInfoService;
        this.dietMealsService = dietMealsService;
        this.userRepository = userRepository;
    }

    public Map<Ingredient,Long> createShoppingList(){
        user = userRepository.findByLoginAndPassword(userInfoService.getUser().getLogin(),userInfoService.getUser().getPassword()).get();
        shoppingList.clear();
        meals.clear();
        meals = user.getDiet().getMeals();
        meals=dietMealsService.adjustIngredients(meals);
        for (MealInfo meal : meals) {
            meal.getMeal().getIngredients().forEach(s -> {
                if(shoppingList.containsKey(s.getIngredient()))
                    shoppingList.put(s.getIngredient(), s.getWeight()+shoppingList.get(s.getIngredient()));
                else
                shoppingList.put(s.getIngredient(),s.getWeight());
            });
        }
        return shoppingList;
    }
}

, и я хочу проверить метод createShoppingList.

Должен ли я создать несколько экземпляров и высмеивать каждое поле, кроме shoppingList и питание, а затем создать 1 или 2случаи ингредиентов, еды и после использования, когда-> затем как это?

@Test
public void createShoppingList() {

    //GIVEN
    Ingredient pineapple = new Ingredient().builder().name("Pineapple").caloriesPer100g(54F).carbohydratePer100g(13.6F).fatPer100g(0.2F).proteinPer100g(0.8F).build();
    Ingredient watermelon = new Ingredient().builder().name("Watermelon").caloriesPer100g(36F).carbohydratePer100g(8.4F).fatPer100g(0.1F).proteinPer100g(0.6F).build();

    IngredientWeight pineappleWithWeight...

    //after this create Meal, MealInfo, Diet...

}

Ниже других классов:

public class MealInfo implements Comparable<MealInfo>{

    @Id
    @GeneratedValue
    private Long id;
    private LocalDate date;
    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "meal_id")
    private Meal meal;
    private String name;
    @ManyToMany(cascade = CascadeType.REMOVE)
    @JoinTable(name = "diet_meal_info", joinColumns = @JoinColumn(name = "meal_info_id"),
            inverseJoinColumns = @JoinColumn(name = "diet_id"))
    private List<Diet> diet;

    public MealInfo(LocalDate date, String description, Meal meal) {
        this.date = date;
        this.name = description;
        this.meal = meal;
    }

    @Override
    public int compareTo(MealInfo o) {
        return getName().compareTo(o.getName());
    }
}


public class Meal {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "meal_ingredient", joinColumns = @JoinColumn(name = "meal_id"),
            inverseJoinColumns = @JoinColumn(name = "ingredient_id"))
    private List<IngredientWeight> ingredients;
    @Column(length = 1000)
    private String description;
    private String imageUrl;
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "meal_category", joinColumns = @JoinColumn(name = "meal_id"),
    inverseJoinColumns = @JoinColumn(name = "category_id"))
    private Set<Category> category;
    @OneToMany(mappedBy = "meal", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MealInfo> mealInfo;
    private Integer calories;

    public Meal(MealForm mealForm) {
        this.name = mealForm.getName();
        this.description = mealForm.getDescription();
        this.imageUrl = mealForm.getImageUrl();
        this.category = mealForm.getCategory();
    }
}

public class IngredientWeight {

    @Id
    @GeneratedValue
    private Long id;
    @ManyToOne
    @JoinColumn(name = "ingredient_weight_id")
    private Ingredient ingredient;
    private Long weight;

    @ManyToMany
    @JoinTable(name = "meal_ingredient", joinColumns = @JoinColumn(name = "ingredient_id"),
            inverseJoinColumns = @JoinColumn(name = "meal_id"))
    private Set<Meal> meals;

}

public class Ingredient {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Column(name = "calories")
    private Float caloriesPer100g;
    @Column(name = "proteins")
    private Float proteinPer100g;
    @Column(name = "carbohydrates")
    private Float carbohydratePer100g;
    @Column(name = "fat")
    private Float fatPer100g;
    @OneToMany(mappedBy = "ingredient", cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.MERGE},
    fetch = FetchType.EAGER)
    private List<IngredientWeight> ingredientWeights;

}

Не могли бы вы написать, как проверить этот метод или проверить реализацию?Или, может быть, у вас есть публичные репозитории с такими большими тестовыми методами?

Ответы [ 2 ]

0 голосов
/ 17 декабря 2018

Я исправил свой дизайн, как сказал Адриан, и создал тест для этого метода.Что касается моего кода ниже, у меня есть несколько вопросов:

  1. Что вы думаете о моих тестах?Необходим первый раздел в методе setUp и выше, или я могу заменить его как-нибудь лучше?Может быть, я могу создать пример сущности в базе данных только для теста?

  2. Какие еще случаи я должен тестировать?

  3. Можно лиизвлечь user.getDiet() для разделения метода checkDiet() и использования try-catch внутри?

  4. Почему я получаю ShoppingServiceException («Пользователь не найден»), когда я удаляю login и passwordполя из моей переменной user, хотя здесь я высмеиваю поведение методов when(userInfoService.getUser()).thenReturn(user); when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user));

Мой рефакторинг ShoppingServiceClass:

@Service
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Data
@NoArgsConstructor

public class ShoppingListService {

    UserInfoService userInfoService;
    DietMealsService dietMealsService;
    UserRepository userRepository;

    @Autowired
    public ShoppingListService(UserInfoService userInfoService, DietMealsService dietMealsService,UserRepository userRepository) {
        this.userInfoService = userInfoService;
        this.dietMealsService = dietMealsService;
        this.userRepository = userRepository;
    }


    public Map<Ingredient,Long> createShoppingList() throws ShoppingServiceException {
        User user = findUser(userInfoService.getUser()).orElseThrow(() -> new ShoppingServiceException("User not found"));
        List<MealInfo> meals = checkDiet(user).getMeals();
        dietMealsService.adjustMealsIngredients(meals);
        Map<Ingredient, Long> shoppingList = new HashMap<>();
        processMeals(meals, shoppingList);
        return shoppingList;
    }

    private Optional<User> findUser(User user) {
        if (user != null) {
            return userRepository.findByLoginAndPassword(user.getLogin(), user.getPassword());
        }
        else {
            return Optional.empty();
        }
    }

    private Diet checkDiet(User user){
        try{
            user.getDiet().getMeals();
        } catch(NullPointerException e){
            throw new ShoppingServiceException("User doesn't have diet");
        }
        return user.getDiet();
    }

    private void processMeals(List<MealInfo> meals, Map<Ingredient, Long> shoppingList) {
        for (MealInfo mealInfo : meals) {
            processIngredientWeights(mealInfo.getMeal().getIngredientWeights(), shoppingList);
        }
    }

    private void processIngredientWeights(List<IngredientWeight> ingredientWeights, Map<Ingredient, Long> shoppingList) {
        for (IngredientWeight ingredientWeight: ingredientWeights) {
            processIngredientWeight(ingredientWeight, shoppingList);
        }
    }

    private void processIngredientWeight(IngredientWeight ingredientWeight, Map<Ingredient, Long> shoppingList) {
        Ingredient ingredient = ingredientWeight.getIngredient();
        Long weight = shoppingList.getOrDefault(ingredient, 0L);
        weight += ingredientWeight.getWeight();
        shoppingList.put(ingredient, weight);
    }

}

И класс ShoppingServiceTest:

@RunWith(MockitoJUnitRunner.class)
public class ShoppingListServiceTest {

    @InjectMocks
    ShoppingListService shoppingListService;

    @Mock
    UserInfoService userInfoService;

    @Mock
    DietMealsService dietMealsService;

    @Mock
    UserRepository userRepository;

    private Ingredient pineapple;
    private Ingredient bread;
    private Ingredient butter;
    private IngredientWeight pineappleWeight;
    private IngredientWeight bread1Weight;
    private IngredientWeight bread2Weight;
    private IngredientWeight butterWeight;
    private Meal meal1;
    private Meal meal2;
    private Meal meal3;
    private MealInfo mealInfo1;
    private MealInfo mealInfo2;
    private MealInfo mealInfo3;
    private Diet diet;
    private User user;
    private User user2;

    @Before
    public void setUp() {

        //Ingredient
        pineapple = new Ingredient();
        pineapple.setName("Pineapple");
        bread = new Ingredient();
        bread.setName("Bread");
        butter = new Ingredient();
        butter.setName("Butter");

        //IngredientWeight
        pineappleWeight = new IngredientWeight();
        pineappleWeight.setIngredient(pineapple);
        pineappleWeight.setWeight(200L);
        bread1Weight = new IngredientWeight();
        bread1Weight.setIngredient(bread);
        bread1Weight.setWeight(300L);
        bread2Weight = new IngredientWeight();
        bread2Weight.setIngredient(bread);
        bread2Weight.setWeight(200L);
        butterWeight = new IngredientWeight();
        butterWeight.setIngredient(butter);
        butterWeight.setWeight(50L);

        //Meal
        meal1 = new Meal();
        meal1.setIngredientWeights(Arrays.asList(bread1Weight,butterWeight));
        meal2 = new Meal();
        meal2.setIngredientWeights(Arrays.asList(pineappleWeight,bread2Weight));
        meal3 = new Meal();
        meal3.setIngredientWeights(Arrays.asList(butterWeight,bread2Weight));

        //MealInfo
        mealInfo1 = new MealInfo();
        mealInfo1.setMeal(meal1);
        mealInfo1.setName("Posiłek 1"); //Meal 1
        mealInfo2 = new MealInfo();
        mealInfo2.setMeal(meal2);
        mealInfo2.setName("Posiłek 2"); //Meal 2
        mealInfo3 = new MealInfo();
        mealInfo3.setMeal(meal3);
        mealInfo3.setName("Posiłek 3"); //Meal 3

        //Diet
        diet = new Diet();
        diet.setMeals(Arrays.asList(mealInfo1,mealInfo2,mealInfo3));

        //User
        user = new User();
        user.setDiet(diet);
        user.setLogin("123");
        user.setPassword("123");

        //User
        user2 = new User();
        user2.setLogin("123");
        user2.setPassword("123");

    }

    @Test(expected = ShoppingServiceException.class)
    public void shouldThrownShoppingServiceExceptionWhenUserNotFound() throws ShoppingServiceException {

        shoppingListService.createShoppingList();
    }

    @Test
    public void shouldReturnShoppingListWhenUserHasDiet(){

        when(userInfoService.getUser()).thenReturn(user);
        when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user));
        doNothing().when(dietMealsService).adjustMealsIngredients(anyList());
        Map<Ingredient,Long> expectedResult = new HashMap<>();
        expectedResult.put(pineapple, 200L);
        expectedResult.put(bread, 700L);
        expectedResult.put(butter,100L);
        Map<Ingredient,Long> actualResult = shoppingListService.createShoppingList();
        assertEquals(actualResult,expectedResult);

    }

    @Test(expected = ShoppingServiceException.class)
    public void shouldReturnShoppingServiceExceptionWhenUserDoesntHaveDiet(){

        when(userInfoService.getUser()).thenReturn(user2);
        when(userRepository.findByLoginAndPassword(anyString(),anyString())).thenReturn(Optional.of(user2));
        doNothing().when(dietMealsService).adjustMealsIngredients(anyList());
        Map<Ingredient,Long> expectedResult = new HashMap<>();
        Map<Ingredient,Long> actualResult = shoppingListService.createShoppingList();
        assertEquals(actualResult,expectedResult);

    }
}
0 голосов
/ 13 декабря 2018

Как уже упоминалось, вам, вероятно, не нужны поля user, shoppingList и meals в вашем сервисе.Эти поля делают службу небезопасной для использования в многопоточной среде, такой как веб-приложение или веб-служба (к которой могут обращаться несколько клиентов, то есть несколько потоков одновременно).Например, shoppingList, над которым вы работаете, может быть очищено в середине процесса, если другой поток введет createShoppingList.Вместо этого сделайте эти поля локальными переменными внутри метода createShoppingList.Если логика становится слишком сложной, а ваша служба слишком большой, вы можете извлечь ее в отдельный сервис или вспомогательный класс, который создается в начале вызова метода и отбрасывается в конце.

Я всегданаписать модульные тесты как тесты белого ящика для одного класса.Я стараюсь охватить каждую ветку в коде, если смогу.Вы можете проверить это, запустив тесты с покрытием в IntelliJ.Обратите внимание, что тесты черного ящика также очень полезны, они сосредоточены на «контракте» компонента.По моему мнению, модульные тесты обычно не подходят для этого, поскольку контракт одного класса обычно не очень интересен для функциональности компонента в целом и может легко измениться, если код подвергается рефакторингу.Я пишу интеграционные (или сквозные) тесты как тесты черного ящика.Это требует настройки среды приложения-заглушки, например, с базой данных в памяти и, возможно, с некоторыми внешними службами через WireMock.Если вы заинтересованы в этом, изучите условия контрактного тестирования Google или среду RestAssured.

Некоторые замечания по поводу вашего кода:

public Map<Ingredient,Long> createShoppingList() {

// if any of the chained methods below return null, a NullPointerException occurs
// You could extract a method which takes the userInfoService user as an argument, see `findUser` below.
    user = userRepository.findByLoginAndPassword(userInfoService.getUser().getLogin(),userInfoService.getUser().getPassword()).get();

// the above would then  become:
    User user = findUser(userInfoService.getUser()).orElseThrow(new ShoppingServiceException("User not found");

// instead of clearing these field, just initialize them as local variables:       
    shoppingList.clear();
    meals.clear();

    meals = user.getDiet().getMeals();

// I would change adjustIngredients so it doesn't return the meals but void
// it's expected that such a method modifies the meals without making a copy
    meals = dietMealsService.adjustIngredients(meals);

// I would extract the below iteration into a separate method for clarity
    for (MealInfo meal : meals) {

// I would also extract the processing of a single meal into a separate method
// the `meal.getIngredients` actually doesn't return Ingredients but IngredientWeights
// this is very confusing, I would rename the field to `ingredientWeights`
        meal.getMeal().getIngredients().forEach(s -> {
// I would replace the four calls to s.getIngredient() with one call and a local variable
// and probably extract another method here
// You are using Ingredient as the key of a Map so you must implement
// `equals` and // `hashCode`. Otherwise you will be in for nasty 
// surprises later when Java doesn't see your identical ingredients as 
// equal. The simplest would be to use the database ID to determine equality.
            if(shoppingList.containsKey(s.getIngredient()))
                shoppingList.put(s.getIngredient(), s.getWeight()+shoppingList.get(s.getIngredient()));
            else
            shoppingList.put(s.getIngredient(),s.getWeight());
        });
    }
    return shoppingList;
}


private Optional<User> findUser(my.service.User user) {
    if (user != null) {
        return userRepository.findByLoginAndPassword(user.getLogin(), user.getPassword());
    }
    else {
        return Optional.empty();
    }
}

private void processMeals(List<MealInfo> meals, Map<Ingredient, Long> shoppingList) {
    for (MealInfo mealInfo : meals) {
        processIngredientWeights(mealInfo.getMeal().getIngredients(), shoppingList);
    }
}

private void processIngredientWeights(List<IngredientWeight> ingredientWeights, Map<Ingredient, Long> shoppingList) {
    for (IngredientWeight ingredientWeight: ingredientWeights) {            
        processIngredientWeight(ingredientWeight, shoppingList);
    }
}

private void processIngredientWeight(IngredientWeight ingredientWeight, Map<Ingredient, Long> shoppingList) {          
    Ingredient ingredient = ingredientWeight.getIngredient();
    Long weight = shoppingList.getOrDefault(ingredient, 0L);
    weight += ingredientWeight.getWeight();
    shoppingList.put(ingredient, weight);
}

РЕДАКТИРОВАТЬ: я снова посмотрел на ваш код и домен и сделал несколькоизменения, см. мой пример кода здесь: https://github.com/akoster/x-converter/blob/master/src/main/java/xcon/stackoverflow/shopping

Модель предметной области была немного запутанной из-за классов 'Info'.Я переименовал их следующим образом:

MealInfo -> Meal
Meal -> Recipe (with a list of Ingredients)
IngredientInfo -> Ingredient (represents a certain amount of a FoodItem)
Ingredient -> FoodItem (e.g. 'broccoli')

Я понял, что служба не принимает аргументов!Это немного странно.Имеет смысл получить пользователя отдельно (например, в зависимости от текущего / выбранного пользователя) и передать его в службу, как вы видите выше.Теперь ShoppingListService выглядит следующим образом:

public class ShoppingListService {

    private DietMealsService dietMealsService;

    public ShoppingListService(DietMealsService dietMealsService) {
        this.dietMealsService = dietMealsService;
    }

    public ShoppingList createShoppingList(User user) {
        List<Meal> meals = getMeals(user);
        dietMealsService.adjustIngredients(meals);
        return createShoppingList(meals);
    }

    private List<Meal> getMeals(User user) {
        Diet diet = user.getDiet();
        if (diet == null || diet.getMeals() == null || diet.getMeals().isEmpty()) {
            throw new ShoppingServiceException("User doesn't have diet");
        }
        return diet.getMeals();
    }

    private ShoppingList createShoppingList(List<Meal> meals) {
        ShoppingList shoppingList = new ShoppingList();
        for (Meal meal : meals) {
            processIngredientWeights(meal.getRecipe().getIngredients(), shoppingList);
        }
        return shoppingList;
    }

    private void processIngredientWeights(List<Ingredient> ingredients, ShoppingList shoppingList) {
        for (Ingredient ingredient : ingredients) {
            shoppingList.addWeight(ingredient);
        }
    }

}

Я также ввел класс ShoppingList, потому что передача Map вокруг - это запах кода, и теперь я могу переместить логику, чтобы добавить ингредиент в список покупок.класс.

import lombok.Data;

@Data
public class ShoppingList {

    private final Map<FoodItem, Long> ingredientWeights = new HashMap<>();

    public void addWeight(Ingredient ingredient) {
        FoodItem foodItem = ingredient.getFoodItem();
        Long weight = ingredientWeights.getOrDefault(foodItem, 0L);
        weight += ingredient.getWeight();
        ingredientWeights.put(foodItem, weight);
    }
}

Модульный тест для этой службы теперь выглядит следующим образом:

@RunWith(MockitoJUnitRunner.class)
public class ShoppingListServiceTest {

    @InjectMocks
    private ShoppingListService instanceUnderTest;

    @Mock
    private DietMealsService dietMealsService;
    @Mock
    private User user;
    @Mock
    private Diet diet;
    @Mock
    private Meal meal;

    @Test(expected = ShoppingServiceException.class)
    public void testCreateShoppingListUserDietNull() {
        // SETUP
        User user = mock(User.class);
        when(user.getDiet()).thenReturn(null);

        // CALL
        instanceUnderTest.createShoppingList(user);
    }

    @Test(expected = ShoppingServiceException.class)
    public void testCreateShoppingListUserDietMealsNull() {
        // SETUP
        when(user.getDiet()).thenReturn(diet);
        when(diet.getMeals()).thenReturn(null);

        // CALL
        instanceUnderTest.createShoppingList(user);
    }

    @Test(expected = ShoppingServiceException.class)
    public void testCreateShoppingListUserDietMealsEmpty() {
        // SETUP
        when(user.getDiet()).thenReturn(diet);
        List<Meal> meals = new ArrayList<>();
        when(diet.getMeals()).thenReturn(meals);

        // CALL
        instanceUnderTest.createShoppingList(user);
    }


    @Test
    public void testCreateShoppingListAdjustsIngredients() {
        // SETUP
        when(user.getDiet()).thenReturn(diet);
        List<Meal> meals = Collections.singletonList(meal);
        when(diet.getMeals()).thenReturn(meals);

        // CALL
        instanceUnderTest.createShoppingList(user);

        // VERIFY
        verify(dietMealsService).adjustIngredients(meals);
    }

    @Test
    public void testCreateShoppingListAddsWeights() {
        // SETUP
        when(user.getDiet()).thenReturn(diet);
        when(diet.getMeals()).thenReturn(Collections.singletonList(meal));
        Recipe recipe = mock(Recipe.class);
        when(meal.getRecipe()).thenReturn(recipe);
        Ingredient ingredient1 = mock(Ingredient.class);
        Ingredient ingredient2 = mock(Ingredient.class);
        when(recipe.getIngredients()).thenReturn(Arrays.asList(ingredient1, ingredient2));
        FoodItem foodItem = mock(FoodItem.class);
        when(ingredient1.getFoodItem()).thenReturn(foodItem);
        when(ingredient2.getFoodItem()).thenReturn(foodItem);
        Long weight1 = 42L;
        Long weight2 = 1337L;
        when(ingredient1.getWeight()).thenReturn(weight1);
        when(ingredient2.getWeight()).thenReturn(weight2);

        // CALL
        ShoppingList shoppingList = instanceUnderTest.createShoppingList(user);

        // VERIFY
        Long expectedWeight = weight1 + weight2;
        Long actualWeight = shoppingList.getIngredientWeights().get(foodItem);
        assertEquals(expectedWeight, actualWeight);
    }
}

Надеюсь, это довольно очевидно.

Кстати, помните, что юнитТест должен проверять только тестируемый класс.Постарайтесь свести к минимуму любые предположения о поведении других классов и сделать это явным, высмеивая их, как показано выше.По той же причине я всегда стараюсь избегать использования «реалистичных» тестовых данных в модульных тестах, потому что это говорит о том, что значения имеют значение для теста - они не имеют.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...