Как автоматизировать тестирование программы

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

Однако тут есть небольшая проблема: те программы, которые мы писали до этого, строго говоря, никуда ничего не возвращали, а просто выводили информацию на экран.

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

Рассмотрим пару примеров.

Пример 1

Задача: Студенты Иванов и Петров за время практики заработали определенную сумму. Кто из них заработал большую сумму? Определить средний заработок.

Возьмем код из предыдущих разделов и определим какая часть кода отвечает за взаимодействие с пользователем, а какая часть за логику:

var scanner = new Scanner(System.in);

var ivanovSum = scanner.nextInt(); // взаимодействие с пользователем
var petrovSum = scanner.nextInt(); // взаимодействие с пользователем

if (ivanovSum > petrovSum) { // логика
    System.out.println("Иванов заработал больше"); // взаимодействие с пользователем
} else if (petrovSum > ivanovSum) { // логика
    System.out.println("Петров заработал больше"); // взаимодействие с пользователем
} else {
    System.out.println("Студенты заработали одинаковое количество денег"); // взаимодействие с пользователем
}

var averageSum = (petrovSum + ivanovSum) / 2;  // логика
System.out.printf("Средняя зарплата: %s", averageSum);  // взаимодействие с пользователем

То есть сейчас у нас своего рода “лапша” в коде, логика пересекается с взаимодействие с пользователем.

Убираем лапшу

Преобразуем код так чтобы, логика у нас была строга отделена от общения с юзером. Например, так:

// НАЧАЛО взаимодействия с пользователем
var scanner = new Scanner(System.in);
var ivanovSum = scanner.nextInt();
var petrovSum = scanner.nextInt();
// КОНЕЦ взаимодействия с пользователем

// НАЧАЛО логики
String outMessage = "";
if (ivanovSum > petrovSum) {
    outMessage = "Иванов заработал больше";
} else if (petrovSum > ivanovSum) {
    outMessage = "Петров заработал больше";
} else {
    outMessage = "Студенты заработали одинаковое количество денег";
}
var averageSum = (petrovSum + ivanovSum) / 2;
// КОНЕЦ логики

// НАЧАЛО взаимодействия с пользователем
System.out.printf(outMessage);
System.out.printf("Средняя зарплата: %s", averageSum);
// КОНЕЦ взаимодействия с пользователем

Таким образом мы разбили приложение на три этапа,

  • в первой части мы “взаимодействуем с пользователем”: запрашиваем значения;
  • вторая часть включает логику, тут мы добавили новую переменную, в которую будем фиксировать сообщение, которое позже покажем юзеру, и также считаем среднее арифметическое сумм полученных студентами;
  • третья часть – снова общаемся с пользователем: отображаем рассчитанные результаты и ждем нажатия любой клавиши.

Производим декомпозицию

Однако, такого преобразования пока недостаточно, чтобы приступить к автотестированию кода. Следующее что мы сделаем – это произведем декомпозицию кода. У нас имеется:

public class Main {
    public static void main(String[] args) {
        // НАЧАЛО взаимодействия с пользователем
        var scanner = new Scanner(System.in);
        var ivanovSum = scanner.nextInt();
        var petrovSum = scanner.nextInt();
        // КОНЕЦ взаимодействия с пользователем

        // НАЧАЛО логики
        String outMessage = "";
        if (ivanovSum > petrovSum) {
            outMessage = "Иванов заработал больше";
        } else if (petrovSum > ivanovSum) {
            outMessage = "Петров заработал больше";
        } else {
            outMessage = "Студенты заработали одинаковое количество денег";
        }
        var averageSum = (petrovSum + ivanovSum) / 2;
        // КОНЕЦ логики

        // НАЧАЛО взаимодействия с пользователем
        System.out.printf(outMessage);
        System.out.printf("Средняя зарплата: %s", averageSum);
        // КОНЕЦ взаимодействия с пользователем
    }
}

Добавим класс, в который запихаем логику нашего приложения. Тыкаем правой кнопкой на папку в которой лежит класс Main

Imgur

вводим имя для файла

Imgur

// файл Logic.java
package com.company;

public class Logic {
    /**
     * Так как технически по заданию у нас целых два действия, придется создать две функции
     * функция Compare нужна нам, чтобы сформировать сообщение о сравнимости полученных денег
     */
    public static String Compare(int ivanovSum, int petrovSum) {
        String outMessage = "";
        if (ivanovSum > petrovSum) {
            outMessage = "Иванов заработал больше";
        } else if (petrovSum > ivanovSum) {
            outMessage = "Петров заработал больше";
        } else {
            outMessage = "Студенты заработали одинаковое количество денег";
        }
        return outMessage;
    }

    /**
     * А эта функция нам нужна, чтобы получить среднее значение двух заработков
     */
    public static int GetAverage(int ivanovSum, int petrovSum) {
        var averageSum = (petrovSum + ivanovSum) / 2;
        return averageSum;
    }
}

Теперь соответсвующим способом обновляем наш основной код.

Так как мы большую часть кода вынесли в отдельный файл Logic, то больше нет необходимости хранить ее в Main. Вместо этого заменим код на соответсвующие вызовы функций:

// файл Main.java
package com.company;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // НАЧАЛО взаимодействия с пользователем
        var scanner = new Scanner(System.in);
        var ivanovSum = scanner.nextInt();
        var petrovSum = scanner.nextInt();
        // КОНЕЦ взаимодействия с пользователем

        // НАЧАЛО логики
        var outMessage = Logic.Compare(ivanovSum, petrovSum);
        var averageSum = Logic.GetAverage(ivanovSum, petrovSum);
        // КОНЕЦ логики

        // НАЧАЛО взаимодействия с пользователем
        System.out.printf(outMessage);
        System.out.printf("Средняя зарплата: %s", averageSum);
        // КОНЕЦ взаимодействия с пользователем
    }
}

То, что мы сейчас сделали, называется декомпозицией. Тело программы, которое находится в вызове функции Main, у нас “похудело” и теперь отвечает в основном за взаимодействие с пользователем, а всю логику делегирует классу Logic. Собственно, этот класс и его функции мы будем тестировать.

Добавляем тесты

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

И так, добавляем папку test

Imgur

Imgur

пометим папку как папку с тестами

Imgur

она должна стать зелененькой

Imgur

Теперь идем в класс Logic, ставим курсор на название класса, кликаем правой кнопкой мыши и выбираем Show context options (либо жмем Alt+Enter

Imgur

кликаем Create Test

Imgur

Откроется окно в котором сначала кликаем Fix, оно установит специальный модуль для тестирования.

Imgur

откроется еще один диалог, в котором просто жмем Ok

Imgur

ждем

Imgur

как дождемся, просто жмем Ok

Imgur

И так, наблюдаем следующую картину

Imgur

перед нами новый класс LogicTest с длинным импортом вверху.

Добавим какой-нибудь тест. Например проверим что 2 + 2 = 4

package com.company;

import org.junit.jupiter.api.Test; // добавил эту строчку
import static org.junit.jupiter.api.Assertions.*;

class LogicTest {
    @Test // помечаем функцию как тестовую
    void CompareTest() {
        assertTrue(2 + 2 == 4); // проверяем истиность условия
    }
}

кликаем стрелочку слева от названия функции и кликаем Run CompareTest

сначала тест сбилдится, а потом слева в нижнем углу откроется информация с результатми тестирования

Imgur

давайте попробуем сломать тест, например пусть он попробует проверить что 2 + 2 = 5

правим код

@Test
void CompareTest() {
    assertTrue(2 + 2 == 5);
}

запускаем и видим как результат выводится оранжевым

Imgur

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

А в больших приложениях позволяет например ловить так называемые регрессивные баги. То есть когда добавляя что-то в одном месте есть риск чего-нибудь поломать в другом.

Тестируем код задачи

Короче, добавляем чего-нибудь осознаное. Добавим тест который проверят, что если Иванов заработал больше чем Петров то выведется корректное сообщение.

И так, правим код:

class LogicTest {
    @Test
    void IvanovGorMoreThanPetrovTest()
    {
        /*
         * Этот тест проверяет что если Иванов заработал больше чем Петров,
         * наша программа вернет корректное сообщение
         */
        var petrovSum = 100; // предположим что Петров заработал сотню
        var ivanovSum = 200; // а Иванов -- две

        // запрашиваем результаты у программы
        var message = Logic.Compare(ivanovSum, petrovSum);

        // проверяем корректность полученных значений
        assertEquals("Иванов заработал больше", message);
    }
}

Запускаем

Imgur

смотрим результат

Imgur

У нас есть еще как минимум два варианта исхода событий, когда Иванов зарабатывает меньше чем Петров, а также, когда они зарабатывают поровну. Добавим соответствующие тесты:

class LogicTest {
    @Test
    void IvanovGorMoreThanPetrovTest()
    {
        /* ... */
    }

    @Test
    void PetrovGotMoreThenIvanovTest()
    {
        /*
         * Этот тест проверяет что если Петров заработал больше чем Иванов,
         * наша программа вернет корректное сообщение
         */
        var petrovSum = 200; // предположим что Петров заработал сотню
        var ivanovSum = 100; // а Иванов -- две

        // запрашиваем результаты у программы
        var message = Logic.Compare(ivanovSum, petrovSum);

        // проверяем корректность полученных значений
        assertEquals("Петров заработал больше", message);
    }
}

тест если студенты заработали поровну:

class LogicTest {
    @Test
    void IvanovGorMoreThanPetrovTest()
    {
        /* ... */
    }

    @Test
    void PetrovGotMoreThenIvanovTest()
    {
        /* ... */
    }

    @Test
    void PetrovAndIvanovHasTheSameSalaryTest()
    {
        var petrovSum = 100;
        var ivanovSum = 100;

        var message = Logic.Compare(ivanovSum, petrovSum);

        assertEquals("Студенты заработали одинаковое количество денег", message);
    }
}

запускаем

Imgur

получаем

Imgur

Прекрасно!

А! Еще у нас же есть функция на расчет среднего значения, ее тоже надо проверить. Добавляем тест

class LogicTest {
    @Test
    void IvanovGorMoreThanPetrovTest() { /* ... */ }

    @Test
    void PetrovGotMoreThenIvanovTest() { /* ... */ }

    @Test
    void PetrovAndIvanovHasTheSameSalaryTest() { /* ... */ }

    @Test
    void GetAverageTest() {
        var petrovSum = 200;
        var ivanovSum = 100;
        var average = Logic.GetAverage(ivanovSum, petrovSum);
        assertEquals(150, average); // 150, я сам, на листике, рассчитал
    }
}

Пример 2

Задача: посчитать сумму введенных пользователем чисел.

Разберем этот пример с минимум комментариев. И так ищем в исходном коде логику и общение с пользователем:

// переменные под взаимодействие с пользователем
var scanner = new Scanner(System.in);
String inputNumber;
var numbers = new ArrayList<Integer>();

// весь цикл у нас это взаимодействие с пользователем
while (true) {
    inputNumber = scanner.nextLine();
    if (inputNumber.equals("")) {
        break;
    }
    numbers.add(Integer.parseInt(inputNumber));
}

// весь цикл как логика
int sum = 0;
for (var value : numbers) {
    sum += value;
}

// и снова взаимодействие с пользователем
System.out.printf("Сумма равна: %s", sum);

Как видно, тут все достаточно удачно разделено, поэтому можно пропустить этап “разгребания лапши” и сразу перейти к декомпозиции

Декомпозиция

// файл Logic.java
package com.company;

import java.util.ArrayList;

public class Logic {
    /**
     * Хватит и одной функции, так как у нас только одно значение
     * в результате: собственно сумма чисел
     */
    public static int getSum(ArrayList<Integer> numbers) {
        int sum = 0;
        for (var value : numbers) {
            sum += value;
        }
        return sum;
    }
}

соответственно правим функцию Main

// файл Main.java
package com.company;

import java.util.ArrayList;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        var scanner = new Scanner(System.in);

        String inputNumber;
        var numbers = new ArrayList<Integer>();

        while (true) {
            inputNumber = scanner.nextLine();
            if (inputNumber.equals("")) { 
                break;
            }
            numbers.add(Integer.parseInt(inputNumber));
        }
        
        // ТУТ МЕНЯЕМ ЦИКЛ НА ВЫЗОВ НАШЕЙ ФУНКЦИИ
        int sum = Logic.getSum(numbers);

        System.out.printf("Сумма равна: %s", sum);
    }
}

Пишем тесты

По аналогии с первой примером

  • кликаем правой кнопкой мыши на Logic
  • затем Show Context Actions -> Create Tests

добавляем свои тесты:

package com.company;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

class LogicTest {
    @Test
    public void Sum10Test() {
        // проверка на сложение 4 чисел
        // чтобы передать список приходится использовать функцию Arrays.asList
        // еще и подсунуть в качестве параметра в ArrayList<>
        var sum = Logic.getSum(new ArrayList<>(
                Arrays.asList(1, 2, 3, 4)
        ));
        assertEquals(10, sum);
    }

    @Test
    public void SumZeroTest() {
        // проверка на рассчет суммы пустого списка
        var sum = Logic.getSum(new ArrayList<>());
        assertEquals(0, sum);
    }
}

Об особенностях тестирования массивов

Если надо проверить на равенство значения двух массивов, необходимо использовать assertArrayEquals, для динамических списков assertIterableEquals, например,

package com.company;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.*;

class LogicTest {
    @Test
    public void ArrayTest() {
        var expected = new int[]{ 1, 2, 3, 4 };
        var realArray = new int[] { 1, 2, 3, 4 };
        assertArrayEquals(expected, realArray);
    }

    @Test
    public void ListTest() {
        var expected = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
        var realArray = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
        assertIterableEquals(expected, realArray);
    }
}