|
@@ -0,0 +1,306 @@
|
|
|
|
|
+# Создание UNIT-тестов
|
|
|
|
|
+
|
|
|
|
|
+## Немного теории
|
|
|
|
|
+
|
|
|
|
|
+### Именование проектов
|
|
|
|
|
+
|
|
|
|
|
+в С# тест реализуется как отдельный проект в том же "решении".
|
|
|
|
|
+
|
|
|
|
|
+К наименованию проекта добавляется суффикс **.Tests**. Т.е. если основной проект у нас называется, например, *Demo*, то тестовый проект должен называться *Demo.Tests*
|
|
|
|
|
+
|
|
|
|
|
+### Именование классов
|
|
|
|
|
+
|
|
|
|
|
+Внутри тестового проекта тестирующие классы тоже должны заканчиваться словом **Tests** (*тестирующий* класс, это класс, который тестирует соответсвующий класс в основном приложении. В тестирующем приложении могут быть и другие вспомогательные классы, правила именования для них обычные), например, для класса *UserManager* создается тестирующий класс *UserManagerTests*.
|
|
|
|
|
+
|
|
|
|
|
+### Именование методов
|
|
|
|
|
+
|
|
|
|
|
+Принцип именования методов тестирующего класса:
|
|
|
|
|
+
|
|
|
|
|
+`[Тестирующийся метод]_[Сценарий]_[Ожидаемое поведение]`
|
|
|
|
|
+
|
|
|
|
|
+Примеры:
|
|
|
|
|
+
|
|
|
|
|
+* Sum_10plus20_30returned
|
|
|
|
|
+* GetPasswordStrength_AllChars_5Points
|
|
|
|
|
+
|
|
|
|
|
+### Концепция ААА
|
|
|
|
|
+
|
|
|
|
|
+Любой тест проходит три стадии:
|
|
|
|
|
+
|
|
|
|
|
+1. **Arrange** - подготовка тестовых данных
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ int x = 10;
|
|
|
|
|
+ int y = 20;
|
|
|
|
|
+ int expected = 30;
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+2. **Act** - выполнение основного действия тестируемым классом
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ int actual = Calc.Add(x, y)
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+3. **Assert** - проверка результата
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ 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 файле проекта):
|
|
|
|
|
+
|
|
|
|
|
+```cs
|
|
|
|
|
+public class Calc
|
|
|
|
|
+{
|
|
|
|
|
+ public float Div(float a, float b)
|
|
|
|
|
+ {
|
|
|
|
|
+ return a / b;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Тестовый метод переобзываем в соответствии с соглашением: **Div_2div2_1expected**, т.е. мы делим "2" на "2" и ожидаем получить "1".
|
|
|
|
|
+
|
|
|
|
|
+и реализуем этот метод:
|
|
|
|
|
+
|
|
|
|
|
+```cs
|
|
|
|
|
+[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** вызывают исключения, по которым система и определяет пройден тест или нет.
|
|
|
|
|
+
|
|
|
|
|
+## Запуск тестов
|
|
|
|
|
+
|
|
|
|
|
+1. Пересоберите тестовый проект, чтобы убедиться что нет ошибок: *Сборка -> Собрать решение*
|
|
|
|
|
+
|
|
|
|
|
+2. Откройте "обозреватель тестов": *Тест -> Обозреватель тестов* и выполните тест
|
|
|
|
|
+
|
|
|
|
|
+ 
|
|
|
|
|
+
|
|
|
|
|
+## Оптимизация
|
|
|
|
|
+
|
|
|
|
|
+Считается хорошим тоном в тестовых методах делать только то, что непосредственно относится к тестированию. В нашем случае создание экземпляра класса *MyCalc* выбивается из общей картины и, к тому же, мы будем вынуждены это делать в каждом тестовом методе.
|
|
|
|
|
+
|
|
|
|
|
+Чтобы избежать повторений и очистить код добавим служебные методы инициализации и финализации:
|
|
|
|
|
+
|
|
|
|
|
+```cs
|
|
|
|
|
+// сам экземпляр класса "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)
|
|
|
|
|
+
|
|
|
|
|
+При тестировании мы должны проверить не только правильное выполнение тестируемого метода, но и поведение при ошибках.
|
|
|
|
|
+
|
|
|
|
|
+* проверяем что результат НЕ РАВЕН заведомо не правильному результату
|
|
|
|
|
+
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ [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
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ [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**.
|
|
|
|
|
+
|
|
|
|
|
+ >По идее тут нужно вообще добавлять интерфейсы и при тестировании работать не с реальной базой, а с заглушкой. Возможно позже я это распишу, а пока делаем как написано ниже.
|
|
|
|
|
+
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ // добавляем исключения на каждый вариант проверки
|
|
|
|
|
+ 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", иначе код ошибки (а можно и сразу текст ошибки).
|
|
|
|
|
+
|
|
|
|
|
+2. Реализуем тестирование
|
|
|
|
|
+
|
|
|
|
|
+ ```cs
|
|
|
|
|
+ namespace UnitTestProject1
|
|
|
|
|
+ {
|
|
|
|
|
+ [TestClass]
|
|
|
|
|
+ public class UnitTest1
|
|
|
|
|
+ {
|
|
|
|
|
+ [TestMethod]
|
|
|
|
|
+ [ExpectedException(typeof(ServiceEmptyCost), "Не заполнена цена")]
|
|
|
|
|
+ public void TestMethod1()
|
|
|
|
|
+ {
|
|
|
|
|
+ var NewService = new Service();
|
|
|
|
|
+ Core.SaveService(NewService);
|
|
|
|
|
+ // досюда мы доходить не должны
|
|
|
|
|
+ Assert.Fail();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+ Тут используется атрибут **ExpectedException**. Он означает, что НОРМАЛЬНЫМ завершением этого теста будет исключение указанного типа.
|