Как автоматизировать тестирование программы
Суть автоматизированного тестирования в том, чтобы все тесты которые вы прописали в таблице тестов, не пришлось проверять вручную. Для этого создается специальная программа для вашей программы, которая подставляет необходимые значения и проверяет выданный уже вашей программой результат.
Однако тут есть небольшая проблема: те программы, которые мы писали до этого, строго говоря, никуда ничего не возвращали, а просто выводили информацию на экран.
Поэтому прежде чем начать писать автоматизированные тесты для программы необходимо произвести декомпозицию проекта. То есть выделить ту часть кода, которая ответственна непосредственно за расчет результатов и ту, которая будет непосредственно взаимодействовать с пользователем.
Рассмотрим пару примеров.
Пример 1
Задача: Студенты Иванов и Петров за время практики заработали определенную сумму. Кто из них заработал большую сумму? Определить средний заработок.
Возьмем код из предыдущих разделов и определим какая часть кода отвечает за взаимодействие с пользователем, а какая часть за логику:
var ivanovSum = int.Parse(Console.ReadLine()); // взаимодействие с пользователем
var petrovSum = int.Parse(Console.ReadLine()); // взаимодействие с пользователем
if (ivanovSum > petrovSum) // логика
{
Console.WriteLine("Иванов заработал больше"); // взаимодействие с пользователем
}
else if (petrovSum > ivanovSum) // логика
{
Console.WriteLine("Петров заработал больше"); // взаимодействие с пользователем
}
else
{
Console.WriteLine("Студенты заработали одинаковое количество денег"); // взаимодействие с пользователем
}
var averageSum = (petrovSum + ivanovSum) / 2; // логика
Console.WriteLine(averageSum); // взаимодействие с пользователем
Console.ReadLine(); // взаимодействие с пользователем
То есть сейчас у нас своего рода “лапша” в коде, логика пересекается с взаимодействие с пользователем.
Убираем лапшу
Преобразуем код так чтобы, логика у нас была строга отделена от общения с юзером. Например, так:
// НАЧАЛО взаимодействия с пользователем
var ivanovSum = int.Parse(Console.ReadLine());
var petrovSum = int.Parse(Console.ReadLine());
// КОНЕЦ взаимодействия с пользователем
// НАЧАЛО логики
string outMessage = "";
if (ivanovSum > petrovSum)
{
outMessage = "Иванов заработал больше";
}
else if (petrovSum > ivanovSum)
{
outMessage = "Петров заработал больше";
}
else
{
outMessage = "Студенты заработали одинаковое количество денег";
}
var averageSum = (petrovSum + ivanovSum) / 2;
// КОНЕЦ логики
// НАЧАЛО взаимодействия с пользователем
Console.WriteLine(outMessage);
Console.WriteLine(averageSum);
Console.ReadLine();
// КОНЕЦ взаимодействия с пользователем
Таким образом мы разбили приложение на три этапа,
- в первой части мы “взаимодействуем с пользователем”: запрашиваем значения;
- вторая часть включает логику, тут мы добавили новую переменную, в которую будем фиксировать сообщение, которое позже покажем юзеру, и также считаем среднее арифметическое сумм полученных студентами;
- третья часть – снова общаемся с пользователем: отображаем рассчитанные результаты и ждем нажатия любой клавиши.
Производим декомпозицию
Однако, такого преобразования пока недостаточно, чтобы приступить к автотестированию кода. Следующее что мы сделаем – это произведем декомпозицию кода. У нас имеется:
namespace FirstApp
{
class Program
{
static void Main(string[] args)
{
// НАЧАЛО взаимодействия с пользователем
var ivanovSum = int.Parse(Console.ReadLine());
var petrovSum = int.Parse(Console.ReadLine());
// КОНЕЦ взаимодействия с пользователем
// НАЧАЛО логики
string outMessage = "";
if (ivanovSum > petrovSum)
{
outMessage = "Иванов заработал больше";
}
else if (petrovSum > ivanovSum)
{
outMessage = "Петров заработал больше";
}
else
{
outMessage = "Студенты заработали одинаковое количество денег";
}
var averageSum = (petrovSum + ivanovSum) / 2;
// КОНЕЦ логики
// НАЧАЛО взаимодействия с пользователем
Console.WriteLine(outMessage);
Console.WriteLine(averageSum);
Console.ReadLine();
// КОНЕЦ взаимодействия с пользователем
}
}
}
Добавим класс, в который запихаем логику нашего приложения:
namespace FirstApp
{
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;
}
}
// а этот класс так и оставляем
class Program
{
static void Main(string[] args)
{
// НАЧАЛО взаимодействия с пользователем
var ivanovSum = int.Parse(Console.ReadLine());
var petrovSum = int.Parse(Console.ReadLine());
// КОНЕЦ взаимодействия с пользователем
// НАЧАЛО логики
// тут мы теперь просто вызываем наши новосформированные функции
var outMessage = Logic.Compare(ivanovSum, petrovSum);
var averageSum = Logic.GetAverage(ivanovSum, petrovSum);
// КОНЕЦ логики
// НАЧАЛО взаимодействия с пользователем
Console.WriteLine(outMessage);
Console.WriteLine(averageSum);
Console.ReadLine();
// КОНЕЦ взаимодействия с пользователем
}
}
}
То, что мы сейчас сделали, называется декомпозицией. Тело программы, которое находится в вызове функции Main, у нас “похудело” и теперь отвечает в основном за взаимодействие с пользователем, а всю логику делегирует классу Logic. Собственно, этот класс и его функции мы будем тестировать.
Добавляем тесты
Скролим вверх к классу Logic, наводим мышкой на название функции Compare, кликаем правой кнопкой мыши и выбираем Создание модульных тестов
увидим такое окошко, ничего не меняем, просто жмем Ok
ждем прока Visual Studio создаст вложенный проект под тесты:
а в панели обозревателя увидим, как появился новый подпроект
также откроется код уже непосредственно тестов, посмотрим, что у нас там присутствует
using Microsoft.VisualStudio.TestTools.UnitTesting; // импортируем методы для тестирования
using FirstApp; // импортируем код нашей программы
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FirstApp.Tests
{
[TestClass()]
public class LogicTests // класс под тесты
{
[TestMethod()]
public void CompareTest() // функция которая тестируется
{
Assert.Fail(); // по умолчанию тест завершится неудачей
}
}
}
Давайте запустим наш тест, для этого нажимаем правой кнопкой мыши на название теста и выбираем “Выполнить тесты”:
подождем пока тесты скомпилируются и выполнятся, слева от кода откроется Окно обозревателя тестов, в котором будет виден результат выполнения тестов, то есть прошли ли они успешно или неудачно:
В нашем случае выполнение завершилось неудачно, потому что в коде тестов явно написано, что должна случится неудача:
[TestMethod()]
public void CompareTest()
{
Assert.Fail(); // неудача
}
Такое нам не интересно, так что давайте добавим какой-нибудь осознанный тест.
Пишем автотесты
И так, правим код:
[TestMethod()]
namespace FirstApp.Tests
{
[TestClass()]
public class LogicTests
{
[TestMethod()]
public void IvanovGorMoreThanPetrovTest()
{
/*
* Этот тест проверяет что если Иванов заработал больше чем Петров,
* наша программа вернет корректное сообщение
*/
var petrovSum = 100; // предположим что Петров заработал сотню
var ivanovSum = 200; // а Иванов -- две
// запрашиваем результаты у программы
var message = Logic.Compare(ivanovSum, petrovSum);
var average = Logic.GetAverage(ivanovSum, petrovSum);
// проверяем корректность полученных значений
Assert.AreEqual("Иванов заработал больше", message);
Assert.AreEqual(150, average);
}
}
}
Уже лучше, но у нас есть еще как минимум два варианта исхода событий, когда Иванов зарабатывает меньше чем Петров, а также, когда они зарабатывают поровну. Добавим соответствующие тесты:
[TestMethod()]
namespace FirstApp.Tests
{
[TestClass()]
public class LogicTests
{
[TestMethod()]
public void IvanovGorMoreThanPetrovTest()
{
/* ... */
}
[TestMethod()]
public void PetrovGotMoreThenIvanovTest()
{
var petrovSum = 200;
var ivanovSum = 100;
var message = Logic.Compare(ivanovSum, petrovSum);
var average = Logic.GetAverage(ivanovSum, petrovSum);
Assert.AreEqual("Петров заработал больше", message);
Assert.AreEqual(150, average);
}
[TestMethod()]
public void IvanovEqualPetrovTest()
{
var petrovSum = 200;
var ivanovSum = 200;
var message = Logic.Compare(ivanovSum, petrovSum);
var average = Logic.GetAverage(ivanovSum, petrovSum);
Assert.AreEqual("Студенты заработали одинаковое количество денег", message);
Assert.AreEqual(200, average);
}
}
}
Пример 2
Задача: посчитать сумму введенных пользователем чисел.
Разберем этот пример с минимум комментариев. И так ищем в исходном коде логику и общение с пользователем:
// переменные под взаимодействие с пользователем
string inputNumber;
var numbers = new List<int>();
// весь цикл у нас это взаимодействие с пользователем
while(true)
{
inputNumber = Console.ReadLine();
if (string.IsNullOrEmpty(inputNumber))
{
break;
}
numbers.Add(int.Parse(inputNumber));
}
// весь цикл как логика
int sum = 0;
foreach(var value in numbers)
{
sum += value;
}
// взаимодействие с пользователем
Console.WriteLine("Сумма равна: {0}", sum);
Console.ReadLine();
Как видно, тут все достаточно удачно разделено, поэтому можно пропустить этап “разгребания лапши” и сразу перейти к декомпозиции
Декомпозиция
namespace FirstApp
{
public class Logic // класс где будем хранить логику
{
/**
* Хватит и одной функции, так как у нас только одно значение
* в результате: собственно сумма чисел
*/
public static int getSum(List<int> numbers)
{
int sum = 0;
foreach (var value in numbers)
{
sum += value;
}
return sum;
}
}
// а этот класс так и оставляем
class Program
{
static void Main(string[] args)
{
// взаимодействие с пользователем
string inputNumber;
var numbers = new List<int>();
while (true)
{
inputNumber = Console.ReadLine();
if (string.IsNullOrEmpty(inputNumber))
{
break;
}
numbers.Add(int.Parse(inputNumber));
}
// вызываем функцию логики, чтобы подсчитать сумму
var sum = Logic.getSum(numbers);
// взаимодействие с пользователем
Console.WriteLine("Сумма равна: {0}", sum);
Console.ReadLine();
}
}
}
Пишем тесты
По аналогии с первой примером
- кликаем правой кнопкой мыши на getSum
- выбираем Создание модульных тестов
- появляется окно, в котором ничего не меняем и жмем Ok
- удаляем тест getSumTest
добавляем свои тесты:
namespace FirstApp.Tests
{
[TestClass()]
public class LogicTests
{
[TestMethod()]
public void Sum10Test()
{
// проверка на сложение 4 чисел
var sum = Logic.getSum(new List<int>{ 1, 2, 3, 4 });
Assert.AreEqual(10, sum);
}
[TestMethod()]
public void SumZeroTest()
{
// проверка на рассчет суммы пустого списка
var sum = Logic.getSum(new List<int> { });
Assert.AreEqual(0, sum);
}
}
}
Об особенностях тестирования массивов (добавленно 15.10)
Если надо проверить на равенство значения двух массивов, необходимо использовать CollectionAssert, например,
namespace FirstApp.Tests
{
[TestClass()]
public class LogicTests
{
[TestMethod()]
public void ArrayTest()
{
var expected = new int[]{ 1, 2, 3, 4 };
var realArray = new int[] { 1, 2, 3, 4 };
CollectionAssert.AreEqual(expected, realArray);
}
[TestMethod()]
public void ListTest()
{
var expected = new List<int>{ 1, 2, 3, 4 };
var realArray = new List<int>{ 1, 2, 3, 4 };
CollectionAssert.AreEqual(expected, realArray);
}
}
}
Собственно, и всё.