JUnit :: или как полюбить валидатор JavaRush

JUnit part I - 1Кратко о том, зачем этот зверь нам нужен? JUnit — это фреймворк автоматического тестирования вашего хорошего или не совсем хорошего кода. Можно сказать: — зачем мне эти качели, я и так смогу легко, и просто протестировать свой хороший Java код. Можно много писать вступительной лирики, но поэт из меня никакой, перейдём лучше к делу…

Создаем объект

И так, чтобы что-то тестировать, нам для начала нужен объект тестирования. Перед нами стоит задача.
  1. Нам нужен объект, который будет хранить информацию о Пользователе.
    1. Id — нужно считать по порядку добавления нового пользователя.
    2. Имя пользователя.
    3. Его возраст.
    4. Пол (male/female)
  2. Нужно предусмотреть хранение списка пользователей.

  3. Класс должен уметь.

    1. Формировать список всех пользователей.
    2. Формировать список пользователей по полу (MALE/FEMALE).
    3. Возвращать количество пользователей в общем списке, и посчитать количество по признаку пола пользователя.
    4. Посчитать общую сумму по возрасту пользователей, так же учесть по признаку пола.
    5. Посчитать средний возраст, как общий так и по признаку пола.
И так, приступим к созданию объекта… Создадим Java класс User он будет содержать поля:
private int id;
private String name;
private int age;
private Sex sex;
Для хранения данных о пользователе этого достаточно, посмотрим что там еще нужно по задаче. Нам нужно как-то хранить всех пользователей, сделаем в нашем классе статическое поле allUsers, думаю нормально если это будет Map<Integer, User>
private static Map<Integer, User> allUsers;
Еще нам как-то нужно присваивать порядковый номер пользователям, создадим статическое поле счетчик, который при создании нового пользователя, будет присваивать порядковый Id пользователю.
private static int countId = 0;
Так, с полями вроде разобрались, напишем конструктор для нашего объекта, и гетеры для полей id, name, age, sex. C гетерами там ничего сложного нет, попросим помощи у IDEA, она никогда не откажет, а конструктор сделаем немного с хитростью. Конструктор будет уметь. Инициализировать поля, проверять есть ли такой объект в allUsers, если такого объекта нет, то увеличиваем наш счетчик countId++, и добавляем его в список всех пользователей. А так же инициализировать поле allUsers ели оно еще не было инициализировано. Для удобства поиска одинаковых объектов, переопределим методы equals() и hashCode(), опять попросим помощи у любимой IDEA и будем сравнивать по полям name, age, sex. Плюс создадим приватный метод hasUser(), который будет проверять есть ли такой объект в списке.
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return age == user.age &&
            Objects.equals(name, user.name) &&
            sex == user.sex;
}

@Override
public int hashCode() {

    return Objects.hash(name, age, sex);
}
Конструктор в итоге у меня получился такой.
public User(String name, int age, Sex sex) {
    if (allUsers == null){
        allUsers = new HashMap<>();
    }

    this.name = name;
    this.age = age;
    this.sex = sex;

    if (!hasUser()){
        countId++;
        this.id = countId;
        allUsers.put(id, this);
    }
}
и вспомогательный приватный метод
private boolean hasUser(){
    for (User user : allUsers.values()){
        if (user.equals(this) && user.hashCode() == this.hashCode()){
            return true;
        }
    }
    return false;
}
а также переопределим toString()
@Override
public String toString() {
    return "User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
            ", sex=" + sex +
            '}';
}
Теперь пришло время, реализовать логику требуемых методов. Так как логика в основном будет работать со статическими полями, методы тоже сделаем статическими, для объектов они не нужны.
  1. Формировать список всех пользователей.
  2. Формировать список пользователей по полу(MALE/FEMALE).
  3. С пунктами a и b хорошо справится метод getAllUsers() который будет возвращать лист всех User, и перегруженный метод getAllUsers(Sex sex) он будет возвращать список, в зависимости от переданного параметра Sex.

    public static List<User> getAllUsers(){
        return new ArrayList<>(allUsers.values());
    }
    
    public static List<User> getAllUsers(Sex sex){
        List<User> listAllUsers = new ArrayList<>();
        for (User user : allUsers.values()){
            if (user.sex == sex){
                listAllUsers.add(user);
            }
        }
        return listAllUsers;
    }

  4. Возвращать количество пользователей в общем списке, и посчитать количество по признаку пола пользователя.

    public static int getHowManyUsers(){
        return allUsers.size();
    }
    
    public static int getHowManyUsers(Sex sex){
        return getAllUsers(sex).size();
    }

  5. Посчитать общую сумму по возрасту пользователей, так же учесть по признаку пола. Для этой задачи сделаем методы.

    public static int getAllAgeUsers(){
        int countAge = 0;
        for (User user : allUsers.values()){
            countAge += user.age;
        }
        return countAge;
    }
    
    public static int getAllAgeUsers(Sex sex){
        int countAge = 0;
        for (User user : getAllUsers(sex)){
            countAge += user.age;
        }
        return countAge;
    }

  6. Посчитать средний возраст, как общий так и по признаку пола.

    public static int getAverageAgeOfAllUsers(){
        return getAllAgeUsers() / getHowManyUsers();
    }
    
    public static int getAverageAgeOfAllUsers(Sex sex){
        return getAllAgeUsers(sex) / getHowManyUsers(sex);
    }

    Отлично, требуемый объект и его поведение мы описали. Теперь можно переходить к JUnit, но для начала покажу как примерно будет выглядеть простой тест если мы его будет делать в main.

    public static void main(String[] args) {
        new User("Евгений", 35, Sex.MALE);
        new User("Марина", 34, Sex.FEMALE);
        new User("Алина", 7, Sex.FEMALE);
    
    
        System.out.println("Все пользователи:");
        User.getAllUsers().forEach(System.out::println);
        System.out.println("Все пользователи: MALE");
        User.getAllUsers(Sex.MALE).forEach(System.out::println);
        System.out.println("Все пользователи: FEMALE");
        User.getAllUsers(Sex.FEMALE).forEach(System.out::println);
        System.out.println("================================================");
        System.out.println("       всех пользователей: " + User.getHowManyUsers());
        System.out.println("  всех пользователей MALE: " + User.getHowManyUsers(Sex.MALE));
        System.out.println("всех пользователей FEMALE: " + User.getHowManyUsers(Sex.FEMALE));
        System.out.println("================================================");
        System.out.println("       общий возраст всех пользователей: " + User.getAllAgeUsers());
        System.out.println("  общий возраст всех пользователей MALE: " + User.getAllAgeUsers(Sex.MALE));
        System.out.println("общий возраст всех пользователей FEMALE: " + User.getAllAgeUsers(Sex.FEMALE));
        System.out.println("================================================");
        System.out.println("       средний возраст всех пользователей: " + User.getAverageAgeOfAllUsers());
        System.out.println("  средний возраст всех пользователей MALE: " + User.getAverageAgeOfAllUsers(Sex.MALE));
        System.out.println("средний возраст всех пользователей FEMALE: " + User.getAverageAgeOfAllUsers(Sex.FEMALE));
        System.out.println("================================================");
    }

    Вывод в консоль получим примерно такой, а дальше сравниваем получили ли мы нормальную работу. Можно конечно углубиться, написать логику сравнения, и посмотреть, что скажет наше вычисление, при том что мы не будем уверены, что все смогли предусмотреть.

    //output
    Все пользователи:
    User{id=1, name='Евгений', age=35, sex=MALE}
    User{id=2, name='Марина', age=34, sex=FEMALE}
    User{id=3, name='Алина', age=7, sex=FEMALE}
    Все пользователи: MALE
    User{id=1, name='Евгений', age=35, sex=MALE}
    Все пользователи: FEMALE
    User{id=2, name='Марина', age=34, sex=FEMALE}
    User{id=3, name='Алина', age=7, sex=FEMALE}
    ================================================
           всех пользователей: 3
      всех пользователей MALE: 1
    всех пользователей FEMALE: 2
    ================================================
           общий возраст всех пользователей: 76
      общий возраст всех пользователей MALE: 35
    общий возраст всех пользователей FEMALE: 41
    ================================================
           средний возраст всех пользователей: 25
      средний возраст всех пользователей MALE: 35
    средний возраст всех пользователей FEMALE: 20
    ================================================
    
    Process finished with exit code 0

    Нас этот исход не устраивает, долой тесты main, нам нужен JUnit.

Как подключить JUnit к проекту

Возникает вопрос, как его подключить к проекту. Для знающих вариант с Mavenбрать не буду, так как это совсем другая история. ;) Открываем структуру проекта Ctrl + Alt + Shift + S -> Libraries -> жмем + (New Project Library) -> выбираем from Maven JUnit part I - 2дальше видим такое окно, в строку поиска вводим “ junit:junit:4.12 ” ждем пока найдет -> OK! -> OK! JUnit part I - 3Должен получиться такой результат JUnit part I - 4Жмем OK, поздравлю JUnit добавлен к проекту. Едем дальше. Теперь нам нужно создать тесты для нашего Java класса, ставим курсор на название класса User -> жмем Alt + Enter -> выбираем create Test. Мы должны увидеть окно, в котором нам нужно выбрать библиотеку JUnit4 -> выбрать методы которые собираемся тестировать -> OK JUnit part I - 5Идея сама создаст класс UserTest, это и есть класс, в котором мы будем покрывать наш код тестами. Приступим:

Наш первый @Test

Создадим наш первый @Test метода getAllUsers() – это метод который должен вернуть всех пользователей. Тест будет выглядеть примерно так:
@Test
public void getAllUsers() {
    //создаем тестовые данные
    User user = new User("Евгений", 35, Sex.MALE);
    User user1 = new User("Марина", 34, Sex.FEMALE);
    User user2 = new User("Алина", 7, Sex.FEMALE);

    //создаем список expected и заполняем его данными нашего метода
    List<User> expected = User.getAllUsers();

    //создаем список actual в него помещаем данные для сравнения
    //то что мы предпологиаем метод должен вернуть
    List<User> actual = new ArrayList<>();
    actual.add(user);
    actual.add(user1);
    actual.add(user2);

    //запускаем тест, в случае если список expected и actual не будут равны
    //тест будет провален, о результатах теста читаем в консоли
    Assert.assertEquals(expected, actual);
}
Тут мы создаем несколько тестовых пользователей -> создаем список expected в который поместим пользователей которых нам вернет метод getAllUsers() -> создадим список actual в который поместим пользователей которых мы предполагаем что метод getAllUsers()Assert.assertEquals(actual, expected) ему мы и передадим списки, инспектируемый и актуальный. Этот метод проверит объекты в предоставленных списках и выдаст результат теста. Метод будет сравнивать все поля объектов, даже пройдется по полям родителей, если есть наследование. Запускаем первый тест... JUnit part I - 6Тест выполнен успешно. Теперь попробуем сделать так, чтобы тест был провален, для этого нам нужно изменить один из списков теста, сделаем это путем, закомментировав добавление одного пользователя в список actual,
@Test
public void getAllUsers() {
    //создаем тестовые данные
    User user = new User("Евгений", 35, Sex.MALE);
    User user1 = new User("Марина", 34, Sex.FEMALE);
    User user2 = new User("Алина", 7, Sex.FEMALE);

    //создаем список expected и заполняем его данными нашего метода
    List<User> expected = User.getAllUsers();

    //создаем список actual в него помещаем данные для сравнения
    //то что мы предпологиаем метод должен вернуть
    List<User> actual = new ArrayList<>();
    actual.add(user);
    actual.add(user1);
    //actual.add(user2);

    //запускаем тест, в случае если список expected и actual не будут равны
    //тест будет провален, о результатах теста читаем в консоли
    Assert.assertEquals(expected, actual);
}
запускаем тест и видим следующее: JUnit part I - 7Теперь мы можем немного разобрать причину провала теста. Тут мы видим, что в инспектируемом списке больше пользователей чем в актуальном. Это и есть причина провала. А в main мы можем проверить так? JUnit : main = 1 : 0. Давайте посмотрим как будет выглядеть тест, если в нем будут полностью разные объекты, сделаем это так:
@Test
public void getAllUsers() {
    //создаем тестовые данные
    User user = new User("Евгений", 35, Sex.MALE);
    User user1 = new User("Марина", 34, Sex.FEMALE);
    User user2 = new User("Алина", 7, Sex.FEMALE);

    //создаем список expected и заполняем его данными нашего метода
    List<User> expected = User.getAllUsers();

    //создаем список actual в него помещаем данные для сравнения
    //то что мы предпологиаем метод должен вернуть
    List<User> actual = new ArrayList<>();
    actual.add(new User("User1", 1, Sex.MALE));
    actual.add(new User("User2", 2, Sex.FEMALE));
    actual.add(new User("User3", 3, Sex.MALE));

    //запускаем тест, в случае если список expected и actual не будут равны
    //тест будет провален, о результатах теста читаем в консоли
    Assert.assertEquals(expected, actual);
}
вот что будет в консоли: JUnit part I - 8тут сразу видно что в сравниваемых списках разные пользователи, еще мы можем кликнуть на &ltClick to see difference> получим такое окно, где можно посмотреть подробно с какими данными у нас проблема. IDEA подсветит все поля в которых есть различия. JUnit part I - 9main такое может? — нет. JUnit : main = 2 : 0 Ну что, пойдем дальше у нас еще куча методов, которые нужно покрыть тестами ), но подождите, а ведь будет не плохо, проверить, а не будет ли нам метод getAllUsers() возвращать null, ведь примерно так нас на задачах JavaRush ловит валидатор ). Сделаем это, делов то на три копейки …
@Test
public void getAllUsers_NO_NULL() {
    //добавим проверку на null
    List<User> expected = User.getAllUsers();
    Assert.assertNotNull(expected);
}
Да, да примерно так валидатор ловит наш говно код на null ;) Теперь запустим этот тест, и посмотрим, что он нам покажет. А покажет он ошибку, как ???? как же тут можно было допустить ошибку теста))) JUnit part I - 10И тут мы можем пожинать первые плоды покрытия своего кода тестами. Как вы помните, поле allUsers мы инициализировали в конструкторе, и значит при вызове метода getAllUsers(), мы обратимся к объекту, который еще не был инициализирован. Будем править, уберем инициализацию из конструктора, и сделаем ее при объявлении поля.
private static Map<Integer, User> allUsers = new HashMap<>();

    public User(String name, int age, Sex sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;

        if (!hasUser()) {
            countId++;
            this.id = countId;
            allUsers.put(id, this);
        }
    }
Запустим тест, теперь все хорошо. JUnit part I - 11не думаю что в main легко будет отловить NPE, думаю вы согласитесь что счет JUnit : main = 3 : 0 Дальше я все методы покрою тестами, и дам вам посмотреть, как это будет выглядеть... Теперь класс тестов у нас выглядит так:
package user;

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;

public class UserTest {

    @Test
    public void getAllUsers() {
        //создаем тестовые данные
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        //создаем список expected и заполняем его данными нашего метода
        List<User> expected = User.getAllUsers();

        //создаем список actual в него помещаем данные для сравнения
        //то что мы предпологиаем метод должен вернуть
        List<User> actual = new ArrayList<>();
        actual.add(user);
        actual.add(user1);
        actual.add(user2);

        //запускаем тест, в случае если список expected и actual не будут равны
        //тест будет провален, о результатах теста читаем в консоли
        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_NO_NULL() {
        //добавим проверку на null
        List<User> expected = User.getAllUsers();
        Assert.assertNotNull(expected);
    }

    @Test
    public void getAllUsers_MALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        List<User> expected = User.getAllUsers(Sex.MALE);

        List<User> actual = new ArrayList<>();
        actual.add(user);

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_MALE_NO_NULL() {
        //добавим проверку на null
        List<User> expected = User.getAllUsers(Sex.MALE);
        Assert.assertNotNull(expected);
    }

    @Test
    public void getAllUsers_FEMALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        List<User> expected = User.getAllUsers(Sex.FEMALE);

        List<User> actual = new ArrayList<>();
        actual.add(user1);
        actual.add(user2);

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_FEMALE_NO_NULL() {
        //добавим проверку на null
        List<User> expected = User.getAllUsers(Sex.FEMALE);
        Assert.assertNotNull(expected);
    }

    @Test
    public void getHowManyUsers() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getHowManyUsers();

        int actual = 3;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getHowManyUsers_MALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getHowManyUsers(Sex.MALE);

        int actual = 1;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getHowManyUsers_FEMALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getHowManyUsers(Sex.FEMALE);

        int actual = 2;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getAllAgeUsers();

        int actual = 35 + 34 + 7;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers_MALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getAllAgeUsers(Sex.MALE);

        int actual = 35;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers_FEMALE() {
        User user = new User("Евгений", 35, Sex.MALE);
        User user1 = new User("Марина", 34, Sex.FEMALE);
        User user2 = new User("Алина", 7, Sex.FEMALE);

        int expected = User.getAllAgeUsers(Sex.FEMALE);

        int actual = 34 + 7;

        Assert.assertEquals(expected, actual);
    }
}
Да не маленький получился, а что же будет при работе с большими проектами. Что же тут можно сократить, оценив все можно заметить, что тестовые данные мы создаем в каждом тесте, и тут нам на помощь приходят аннотации. Возьмем @Before — Аннотация @Before указывает на то, что метод будет выполнятся перед каждым тестируемым методом @Test. Вот так теперь будет выглядеть наш класс тестов с аннотацией @Before:
package user;

import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;

public class UserTest {
    private User user;
    private User user1;
    private User user2;

    @Before
    public void setUp() throws Exception {
        user = new User("Евгений", 35, Sex.MALE);
        user1 = new User("Марина", 34, Sex.FEMALE);
        user2 = new User("Алина", 7, Sex.FEMALE);
    }

    @Test
    public void getAllUsers() {
        List<User> expected = User.getAllUsers();

        List<User> actual = new ArrayList<>();
        actual.add(user);
        actual.add(user1);
        actual.add(user2);

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_NO_NULL() {
        List<User> expected = User.getAllUsers();
        Assert.assertNotNull(expected);
    }

    @Test
    public void getAllUsers_MALE() {
        List<User> expected = User.getAllUsers(Sex.MALE);

        List<User> actual = new ArrayList<>();
        actual.add(user);

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_MALE_NO_NULL() {
        //добавим проверку на null
        List<User> expected = User.getAllUsers(Sex.MALE);
        Assert.assertNotNull(expected);
    }

    @Test
    public void getAllUsers_FEMALE() {
        List<User> expected = User.getAllUsers(Sex.FEMALE);

        List<User> actual = new ArrayList<>();
        actual.add(user1);
        actual.add(user2);

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllUsers_FEMALE_NO_NULL() {
        //добавим проверку на null
        List<User> expected = User.getAllUsers(Sex.FEMALE);
        Assert.assertNotNull(expected);
    }

    @Test
    public void getHowManyUsers() {
        int expected = User.getHowManyUsers();

        int actual = 3;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getHowManyUsers_MALE() {
        int expected = User.getHowManyUsers(Sex.MALE);

        int actual = 1;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getHowManyUsers_FEMALE() {
        int expected = User.getHowManyUsers(Sex.FEMALE);

        int actual = 2;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers() {
        int expected = User.getAllAgeUsers();

        int actual = 35 + 34 + 7;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers_MALE() {
        int expected = User.getAllAgeUsers(Sex.MALE);

        int actual = 35;

        Assert.assertEquals(expected, actual);
    }

    @Test
    public void getAllAgeUsers_FEMALE() {
        int expected = User.getAllAgeUsers(Sex.FEMALE);

        int actual = 34 + 7;

        Assert.assertEquals(expected, actual);
    }
}
Ну как вам, уже веселее и легче читать ;) Вот список аннотаций для JUnit с ними однозначно жить проще.
@Test – определяет что метод method() является тестовым.
@Before – указывает на то, что метод будет выполнятся перед каждым тестируемым методом @Test.
@After – указывает на то что метод будет выполнятся после каждого тестируемого метода @Test
@BeforeClass – указывает на то, что метод будет выполнятся в начале всех тестов,
а точней в момент запуска тестов(перед всеми тестами @Test).
@AfterClass – указывает на то, что метод будет выполнятся после всех тестов.
@Ignore – говорит, что метод будет проигнорирован в момент проведения тестирования.
(expected = Exception.class) – указывает на то, что в данном тестовом методе
вы преднамеренно ожидаете Exception.
(timeout = 100) – указывает, что тестируемый метод не должен занимать больше чем 100 миллисекунд.
Основные методы класса Assert для проверки:
fail(String) – указывает на то что бы тестовый метод завалился при этом выводя текстовое сообщение.
assertTrue([message], boolean condition) – проверяет, что логическое условие истинно.
assertsEquals([String message], expected, actual) – проверяет, что два значения совпадают.
Примечание: для массивов проверяются ссылки, а не содержание массивов.
assertNull([message], object) – проверяет, что объект является пустым null.
assertNotNull([message], object) – проверяет, что объект не является пустым null.
assertSame([String], expected, actual) – проверяет, что обе переменные относятся к одному объекту.
assertNotSame([String], expected, actual) – проверяет, что обе переменные относятся к разным объектам.
Вот так мы можем добавить зависимость JUnit 4.12 в Maven
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>
продолжение тут -> JUnit part II