|
|
Содержание
|
Fake data. Тестирование методов получающих внешние данные из удалённых источников.
|
Создание UNIT-тестов
Немного теории
Именование проектов
в С# тест реализуется как отдельный проект в том же "решении".
К наименованию проекта добавляется суффикс .Tests. Т.е. если основной проект у нас называется, например, Demo, то тестовый проект должен называться Demo.Tests
Именование классов
Внутри тестового проекта тестирующие классы тоже должны заканчиваться словом Tests (тестирующий класс, это класс, который тестирует соответсвующий класс в основном приложении. В тестирующем приложении могут быть и другие вспомогательные классы, правила именования для них обычные), например, для класса UserManager создается тестирующий класс UserManagerTests.
Именование методов
Принцип именования методов тестирующего класса:
[Тестирующийся метод]_[Сценарий]_[Ожидаемое поведение]
Примеры:
- Sum_10plus20_30returned
- GetPasswordStrength_AllChars_5Points
Концепция ААА
Любой тест проходит три стадии:
Arrange - подготовка тестовых данных
int x = 10;
int y = 20;
int expected = 30;
Act - выполнение основного действия тестируемым классом
int actual = Calc.Add(x, y)
Assert - проверка результата
Assert.AreEqual(exprected, actusl);
Для проверки результатов используется класс Assert, который реализует множество методов для проверки.
Атрибуты
Перед методами тестирующего класса могут быть указаны атрибуты:
- [TestClass] - тестирующий класс
[TestMethod] - тестирующий метод
[TestInitialize] - метод для инициализации, вызывается перед каждым тестирующим методом
[TestCleanup] - метод для освобождения ресурсов, вызывается после каждого тестирующего метода
[ClassInitialize] - вызывается один раз для тестирующего класса, перед запуском тестирующего метода
[ClassCleanup] - вызывается одина раз для тестирующего класса, после завершения тестирующих методов
[AssemblyInitialize] - вызывается перед тем, как начнут работать тестирующие методы в сборке
[AssemblyCleanup] - вызывается после завершения тестирующих методов в сборке
Assertion - сравнение разных типов данных
Assert
- сравнение двух входящих значений
- множество методов для сравнения
CollectionAssert
- сравнение двух коллекций
- проверка элементов в коллекции
StringAssert
Основные методы класса Assert
Assert.AreEqual()
Проверка двух аргументов на равенство
Assert.AreSame()
Проверяет, ссылаются ли переменные на одну и ту же область памяти
Assert.InstanceOfType()
Метод для проверки типа объекта
Assert.IsTrue, Asser.IsFalse
Проверка логических конструкций
Практика
В контекстном меню РЕШЕНИЯ выбираете Добавить -> Создать проект
При создании проекта можете воспользоваться фильтрами "языки", "платформа" и "типы проектов". В итоге надо выбрать "Проект модульного теста (.NET Framework)"

Правила формирования имени тестового проекта были выше, у меня получилось orm3.Tests. Расположение не трогаем, по-умолчанию тестовый проект сохранится в том же решении (рядом с основным проектом)
Связь с основным проектом
Дальше нужно добавить связь с основным проектом:
В тестовом проекте находим пункт "зависимости" (теперь он называется "ссылки") и в контекстном меню "добавить ссылку на проект"

Выбрать основной проект (их может быть несколько), у меня он называется orm3.

Проверить, добавлен ли проект в зависимости, можно раскрыв "зависимости"

Написание тестов
Для начала нужно выбрать в тестируемом проекте класс, который мы хотим протестировать. Т.к. у нас там нет каких-то простых классов, то нарисуем класс "Калькулятор" с методом "Деление" (желательно отдельным файлом, т.к. это может оцениваться отдельными баллами, но можно и в любом .cs файле проекта):
public class Calc
{
public float Div(float a, float b)
{
return a / b;
}
}
Тестовый метод переобзываем в соответствии с соглашением: Div_2div2_1expected, т.е. мы делим "2" на "2" и ожидаем получить "1".
и реализуем этот метод:
[TestMethod]
public void Div_2div2_1expected()
{
//arrange
int a = 2;
int b = 2;
float expected = 1;
//act
// тут мы должны создать экземпляр класса, чтобы протестировать его
Calc MyCalc = new Calc();
float actual = MyCalc.Div(a, b);
//assert
Assert.AreEqual(actual, expected);
}
При ошибке сравнения методы Assert вызывают исключения, по которым система и определяет пройден тест или нет.
Запуск тестов
Пересоберите тестовый проект, чтобы убедиться что нет ошибок: Сборка -> Собрать решение
Откройте "обозреватель тестов": Тест -> Обозреватель тестов и выполните тест

Оптимизация
Считается хорошим тоном в тестовых методах делать только то, что непосредственно относится к тестированию. В нашем случае создание экземпляра класса MyCalc выбивается из общей картины и, к тому же, мы будем вынуждены это делать в каждом тестовом методе.
Чтобы избежать повторений и очистить код добавим служебные методы инициализации и финализации:
// сам экземпляр класса "Calc" мы объявляем как свойство тестового класса
static Calc MyCalc = null;
/* используем атрибут [ClassInitialize],
который скажет тестировщику,
что этот метод нужно выполнить ДО запуска тестов
(поэтому и этот метод и свойство Mycalc
должны быть объявлены статические)*/
[ClassInitialize]
static public void Init(TestContext tc)
{
MyCalc = new Calc();
}
// и аналогично для завершения
[ClassCleanup]
static public void Done()
{
MyCalc = null;
}
А в методе Div_2div2_1expected создание экземпляра MyCalc убрать.
Добавление тестовых ситуаций (case)
При тестировании мы должны проверить не только правильное выполнение тестируемого метода, но и поведение при ошибках.
проверяем что результат НЕ РАВЕН заведомо не правильному результату
[TestMethod]
public void Div_2div2_2notexpected()
{
//arrange
int a = 2;
int b = 2;
float expected = 2;
//act
float actual = MyCalc.Div(a, b);
//assert
Assert.AreNotEqual(actual, expected);
}
используя атрибут 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 вдруг попадем на этот код, то тест не пройден)
Ближе к телу
В тестовом задании, которое вы делаете на лабораторных, есть сохранение услуг. Перед сохранением нужно проверить верно ли заполнены поля услуги.
Сделаем тестирование сохранения услуги в БД.
Вынесем код, сохраняющий данные об услуге, в отдельный метод класса 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();
}
}
Вообще вместо исключений можно использовать код ответа, т.е. если сохранение произошло успешно, то возвращать "0", иначе код ошибки (а можно и сразу текст ошибки).
Реализуем тестирование
namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
[ExpectedException(typeof(ServiceEmptyCost), "Не заполнена цена")]
public void TestMethod1()
{
var NewService = new Service();
Core.SaveService(NewService);
// досюда мы доходить не должны
Assert.Fail();
}
}
}
Тут используется атрибут ExpectedException. Он означает, что НОРМАЛЬНЫМ завершением этого теста будет исключение указанного типа.