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

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

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

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

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

Пример 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);
        }
    }
}

Собственно, и всё.