Как автоматизировать тестирование программы
Суть автоматизированного тестирования в том, чтобы все тесты которые вы прописали в таблице тестов, не пришлось проверять вручную. Для этого создается специальная программа для вашей программы, которая подставляет необходимые значения и проверяет выданный уже вашей программой результат.
Однако тут есть небольшая проблема: те программы, которые мы писали до этого, строго говоря, никуда ничего не возвращали, а просто выводили информацию на экран.
Поэтому прежде чем начать писать автоматизированные тесты для программы необходимо произвести декомпозицию проекта. То есть выделить ту часть кода, которая ответственна непосредственно за расчет результатов и ту, которая будет непосредственно взаимодействовать с пользователем.
Рассмотрим пару примеров.
Пример 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
вводим имя для файла
// файл 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
пометим папку как папку с тестами
она должна стать зелененькой
Теперь идем в класс Logic, ставим курсор на название класса, кликаем правой кнопкой мыши и выбираем Show context options (либо жмем Alt+Enter
кликаем Create Test
Откроется окно в котором сначала кликаем Fix, оно установит специальный модуль для тестирования.
откроется еще один диалог, в котором просто жмем Ok
ждем
как дождемся, просто жмем Ok
И так, наблюдаем следующую картину
перед нами новый класс 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
сначала тест сбилдится, а потом слева в нижнем углу откроется информация с результатми тестирования
давайте попробуем сломать тест, например пусть он попробует проверить что 2 + 2 = 5
правим код
@Test
void CompareTest() {
assertTrue(2 + 2 == 5);
}
запускаем и видим как результат выводится оранжевым
Собственно вся задача тестов это выводить зелененькие/оранжевые строчки. На маленьких приложениях это не очень актуально, хотя в нашем случае например избавляет вас от необходимости вводить значения в терминале.
А в больших приложениях позволяет например ловить так называемые регрессивные баги. То есть когда добавляя что-то в одном месте есть риск чего-нибудь поломать в другом.
Тестируем код задачи
Короче, добавляем чего-нибудь осознаное. Добавим тест который проверят, что если Иванов заработал больше чем Петров то выведется корректное сообщение.
И так, правим код:
class LogicTest {
@Test
void IvanovGorMoreThanPetrovTest()
{
/*
* Этот тест проверяет что если Иванов заработал больше чем Петров,
* наша программа вернет корректное сообщение
*/
var petrovSum = 100; // предположим что Петров заработал сотню
var ivanovSum = 200; // а Иванов -- две
// запрашиваем результаты у программы
var message = Logic.Compare(ivanovSum, petrovSum);
// проверяем корректность полученных значений
assertEquals("Иванов заработал больше", message);
}
}
Запускаем
смотрим результат
У нас есть еще как минимум два варианта исхода событий, когда Иванов зарабатывает меньше чем Петров, а также, когда они зарабатывают поровну. Добавим соответствующие тесты:
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);
}
}
запускаем
получаем
Прекрасно!
А! Еще у нас же есть функция на расчет среднего значения, ее тоже надо проверить. Добавляем тест
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);
}
}