5_3_1_10_unit_test.md 16 KB

Предыдущая лекция   Следующая лекция
Создание библиотеки классов Содержание Fake data. Тестирование методов получающих внешние данные из удалённых источников

Создание UNIT-тестов

Немного теории

Именование проектов

в С# тест реализуется как отдельный проект в том же "решении".

К наименованию проекта добавляется суффикс ".Tests". То есть, если основной проект у нас называется, например, "Demo", то тестирующий проект должен называться "Demo.Tests"

Именование классов

Внутри тестового проекта тестирующие классы тоже должны заканчиваться словом "Tests" (тестирующий класс, это класс, который тестирует соответсвующий класс в основном приложении. В тестирующем приложении могут быть и другие вспомогательные классы, правила именования для них обычные), например, для класса "UserManager" создается тестирующий класс "UserManagerTests".

Именование методов

Принцип именования методов тестирующего класса:

[Тестируемый метод]_[Сценарий]_[Ожидаемое поведение]

Примеры:

  • Sum_10plus20_30returned
  • GetPasswordStrength_AllChars_5Points

Концепция ААА

Любой тест проходит три стадии:

  1. Arrange - подготовка тестовых данных

    // исходные данные
    int x = 10;
    int y = 20;
    // ожидаемый результат
    int expected = 30;
    
  2. Act - выполнение основного действия тестируемым классом

    int actual = Calc.Sum(x, y)
    
  3. Assert - проверка результата

    Assert.AreEqual(expected, actual);
    

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

Атрибуты

Перед методами тестирующего класса могут быть указаны атрибуты:

  • [TestClass] - тестирующий класс
  • [TestMethod] - тестирующий метод

  • [TestInitialize] - метод для инициализации, вызывается перед каждым тестирующим методом

  • [TestCleanup] - метод для освобождения ресурсов, вызывается после каждого тестирующего метода

  • [ClassInitialize] - вызывается один раз для тестирующего класса, перед запуском тестирующего метода

  • [ClassCleanup] - вызывается одина раз для тестирующего класса, после завершения тестирующих методов

  • [AssemblyInitialize] - вызывается перед тем, как начнут работать тестирующие методы в сборке

  • [AssemblyCleanup] - вызывается после завершения тестирующих методов в сборке

Assertion - сравнение разных типов данных

  • Assert

    • сравнение двух входящих значений
    • множество методов для сравнения
  • CollectionAssert

    • сравнение двух коллекций
    • проверка элементов в коллекции
  • StringAssert

    • сравнение строк

Основные методы класса Assert

Assert.AreEqual()
Проверка двух аргументов на равенство

Assert.AreSame()
Проверяет, ссылаются ли переменные на одну и ту же область памяти

Assert.InstanceOfType()
Метод для проверки типа объекта

Assert.IsTrue, Asser.IsFalse
Проверка логических конструкций

Практика

В решение, где у вас создана библиотека классов, добавьте проект "Unit Test Project":

  • Правила формирования имени тестового проекта были выше, у меня получилось CompanyCoreLib.Tests.
  • Расположение не трогаем, по-умолчанию тестовый проект сохранится в том же решении (рядом с основным проектом).
  • Тип тестов: MSTest

Получится такая "рыба":

namespace CompanyCoreLib.Tests;

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
    }
}

Связь с основным проектом

Добавьте связь с основным проектом (как делали в прошлой лекции в консольном приложении).

Написание тестов

Для начала нужно выбрать в тестируемом проекте (CompanyCoreLib) класс, который мы хотим протестировать - у нас там один класс, созданный именно для тестирования: Analytics.

Тестирующий класс переименуйте в AnalyticsTests (по соглашению к имени тестируемого класса добавляется суффикс Tests)

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

и реализуем этот метод:

[TestMethod]
public void PopularMonths_UniqueDates_OnlySort()
{
    // Arrange - подготовка исходных данных
    var srcDates = new List<DateTime>()
    {
        new DateTime(2023,12,18,0,0,0),
        new DateTime(2023,11,21,0,0,0),
    };
    
    /**
    * Act - действие
    * тут мы должны создать экземпляр класса, чтобы протестировать его
    */
    var myAnalytics = new Analytics();
    
    // выполняем метод и получаем результат
    var outDates = myAnalytics.PopularMonths(srcDates);
    
    /**
    * подгатавливаем данные для 
    * ожидаемого результата
    * просто оригинальный список, 
    * но с сортировкой по возрастанию
    */
    var expectedDates = new List<DateTime>()
    {
        new DateTime(2023,11,1,0,0,0),
        new DateTime(2023,12,1,0,0,0)
    };
    
    // Assert - сравниваем ожидаемый результат и данные полученные от тестируемого метода
    CollectionAssert.AreEqual(expectedDates, outDates);

}

При сравнении скалярных данных (числа, строки...) используется класс Assert. Но так как у нас не скалярный тип данных (List), то нужно использовать соответствующий класс - нам подходит CollectionAssert

При ошибке сравнения данных методы Assert вызывают исключения, по которым система и определяет пройден тест или нет.

Запуск тестов

  1. Пересоберите решение, чтобы убедиться что нет ошибок

  2. Запустите тест: Tests -> Run Unit Test

Оптимизация

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

Чтобы избежать повторений и очистить код добавим служебные методы инициализации и финализации:

// сам экземпляр класса "Analytics" мы объявляем 
// как свойство тестирующего класса
private static Analytics myAnalytics = null;

/* используем атрибут [ClassInitialize], 
который скажет тестировщику, 
что этот метод нужно выполнить ДО запуска тестов 
(поэтому и этот метод и свойство myAnalytics 
должны быть объявлены статическими)*/
[ClassInitialize]
static public void Init(TestContext tc)
{
    myAnalytics = new Analytics();
}

// и аналогично для завершения
[ClassCleanup]
static public void Done()
{
    myAnalytics = null;
}

А в методе PopularMonths_UniqueDates_OnlySort создание экземпляра myAnalytics убрать.

Добавление тестовых ситуаций (case)

При тестировании мы должны проверить не только правильное выполнение тестируемого метода, но и поведение при ошибках.

  • проверяем что результат НЕ РАВЕН заведомо не правильному результату

    [TestMethod]
    public void PopularMonths_UniqueDates_NotEmpty()
    {
        // Arrange - подготовка исходных данных
        var srcDates = new List<DateTime>()
        {
            new DateTime(2023,12,1,0,0,0),
            new DateTime(2023,11,1,0,0,0),
        };
            
        // Act - действие
        var outDates = myAnalytics.PopularMonths(srcDates);
            
        /**
         * Arrange - подгатавливаем данные для ожидаемого результата
         * пустой список для проверки не ожиданного результата
         */
        var expectedDates = new List<DateTime>();
            
        CollectionAssert.AreNotEqual(expectedDates, outDates);
    }
    
  • используя атрибут ExpectedException, проверяем наличие исключения при делении на 0

    Наш метод исключений не вызывает, поэтому тут "сферический конь в вакууме" от другого метода

    [TestMethod]
    [ExpectedException(typeof(DivideByZeroException), "Деление на 0")]
    public void Div_2div0_exceptionexpected()
    {
        //arrange
        float a = 2;
        float b = 0;
        float expected = 2;
    
        //act
        float actual = MyCalc.Div(a, b);
    
        //assert
        Assert.AreNotEqual(actual, expected);
    }
    

    То же самое можно сделать без использования атрибута, а просто завернув код в блок try..catch, и в секции try после вычисления вставить Assert.Fail(); (т.е. если при делении на 0 вдруг попадем на этот код, то тест не пройден)

Ближе к телу

В тестовом задании, которое вы делаете на лабораторных, есть сохранение услуг. Перед сохранением нужно проверить верно ли заполнены поля услуги.

Сделаем тестирование сохранения услуги в БД.

  1. Вынесем код, сохраняющий данные об услуге, в отдельный метод класса Core.

    По идее тут нужно вообще добавлять интерфейсы и при тестировании работать не с реальной базой, а с заглушкой. Возможно позже я это распишу, а пока делаем как написано ниже.

    // добавляем исключения на каждый вариант проверки
    public class ServiceEmptyCost : Exception {
        public ServiceEmptyCost(string Mesage): base(Mesage) { }
    }
    
    public class ServiceInvalidDiscount : Exception
    {
        public ServiceInvalidDiscount(string Mesage) : base(Mesage) { }
    }
    
    public class Core
    {
        public static spenkinEntities DB = new spenkinEntities();
    
        // добавляем статический метод, который осуществляет все проверки
        // если есть ошибки, то он выкинет исключение
        public static void SaveService(Service SavedService) {
            if (SavedService.Cost <= 0)
                throw new ServiceEmptyCost("Не заполнена цена");
    
            if (SavedService.Discount < 0 || SavedService.Discount > 1)
                throw new ServiceInvalidDiscount("Скидка должна быть в диапазоне 0..1");
    
            if (SavedService.ID == 0)
                DB.Service.Add(SavedService);
    
            DB.SaveChanges();
        }
    }
    
  2. Реализуем тестирование

    namespace UnitTestProject1
    {
        [TestClass]
        public class UnitTest1
        {
            [TestMethod]
            [ExpectedException(typeof(ServiceEmptyCost), "Не заполнена цена")]
            public void TestMethod1()
            {
                var NewService = new Service();
                Core.SaveService(NewService);
                // досюда мы доходить не должны
                Assert.Fail();
            }
        }
    }
    

    Тут используется атрибут ExpectedException. Он означает, что НОРМАЛЬНЫМ завершением этого теста будет исключение указанного типа.

Предыдущая лекция   Следующая лекция
Создание библиотеки классов Содержание Fake data. Тестирование методов получающих внешние данные из удалённых источников