Про курс
Разработка через тестирование (Test Driven Development, TDD) представляет собой такой подход к разработке программного обеспечения, при котором сперва происходит написание тестов на несуществующий функционал, а лишь затем написание самого функционала. Благодаря технологии TDD код становится понятным и “чистым”, сводя к минимуму количество ошибок в работе программ.
Ни один толковый программист не может позволить себе хаос в коде - это верный признак дурного тона и непрофессионализма. Потому обладание технологиями, которые приводят в порядок “рабочее место” является очень важным пунктом как для опытных кодеров, так и для тех, кто желает иметь весомое преимущество во время собеседования. Изучение трёх уроков общей продолжительностью в 6 часов позволит Вам писать грамотный код, который повысит качество разрабатываемого программного обеспечения и пресечет любые ошибки.
Цей курс входить до спеціальності:
Попередні Вимоги
- Понимание ООП и знание синтаксиса C#
- Владение основными библиотеками .NET Framework
- Опыт программирования на C#
- Опыт работы с Visual Studio
Ви навчитеся
- Понимать и использовать на практике технологию TDD;
- Писать “чистый” и понятный код с минимальным количеством “багов”;
- Проверять работоспособность кода с помощью unit тестирования;
- Писать автоматизированное unit-тесты;
- Использовать в тестировании stub- и mock-объекты.
- 4 год 38 хв
- 24.05.2013
- 4
- 12.11.2019
- російська
Що входить до курсу
×
Ви дійсно бажаєте відкрити доступ до тестування за курсом TDD - Разработка через тестирование на 40 днів?
Данный видео урок посвящен основам TDD и модульному (Unit) тестированию.
Изучив материалы этого урока, Вы сможете:
- понимать принцип разработки программного обеспечения через Unit тестирование;
- настраивать среду Visual Studio под использование юнит-тест фреймворка NUnit;
- понимать основные принципы написания правильных тестов;
- создавать простые Unit-тесты.
Здравствуйте. Мы начинаем видеокурс по разработке через тестирование. Наш видеокурс будет состоять из 3 уроков: на первом уроке мы дадим определение модульному тестированию, рассмотрим преимущества модульного тестирования, принцип разработки ПО через модульное тестирование, то есть так называемый принцип Test Driven Development, и инструменты, которые нам для этого понадобятся. Но сперва давайте рассмотрим какие основные стратегии тестирование существуют.
Существуют две основные стратегии: так называемое интеграционное тестирование и модульное тестирование. Интеграционное тестирование позволяет протестировать несколько связанных модулей ПО как единого целого. Тестирование этих модулей выполняется через их интерфейс с использованием техники тестирования «черного ящика». При интеграционном тестировании существуют много критических точек, в которых приложение может дать сбой, что, собственно говоря, усложняет процесс поиска ошибок. Поэтому, интеграционному тестированию предшествует ряд модульных тестов, которые направлены на тестирование отдельных модулей ПО, что позволяет изолировать отдельные части программы, и показать что эти части по отдельности – работоспособны. Тем самым модульное тестирование играет ключевую роль в обеспечении должного уровня качества и скорости создание ПО.
Перейдём на следующий слайд и дадим определение Unit тесту. Unit тестом мы будем называть метод, который вызывает некий блок кода и проверяет его правильность работы. При этом существует только два варианта исхода теста: тест может быть успешно пройден, если результат работы тестируемого кода совпадает с ожидаемым результатом; и тест может быть провален, если результат работы тестируемого кода не совпадает с ожидаемыми результатами.
И для того чтобы методология модульного тестирования действительно способствовала быстрому созданию качественных продуктов, юнит-тесты должны обладать рядом свойств: они должны быть простыми в реализации, то есть занимать минимум строк кода – дело в том, что создание больших и долгих тестов может занят много времени программиста, что замедлит скорость реализации проекта; тесты должны быть повторяемыми – для облегчения обнаружения и устранения регрессионных ошибок, то есть тех ошибок, которые появляются уже в оттестированных частях программы; также тесты должны выполнятся быстро и, что немаловажно, у кого угодно в вашей команде должна быть возможность запустить юнит-тест одним нажатием кнопки – для того, чтобы убедиться, что его изменения не привели к появлению ошибок в других частях системы.
На четвертом слайде нам хотят напомнить, что такое интеграционное тестирование. Мы с вами это уже оговорили – это тестирование нескольких связанных модулей программного обеспечения как одного целого. На пятом слайде нам продемонстрирована структура некого приложения, в котором мы хотим протестировать класс under test. Используя методологию интегрального тестирования это сделать будет не так просто, потому как класс under test имеет внешние зависимости от других объектов, каждый из которых является критической точкой: это классы helper class, data helper и база данных. Эти критические точки могут дать сбой и провалить тест, и, как вы понимаете, поиск и исправление ошибок при таком подходе может быть довольно сложной задачей. Модель модульного тестирования в таком случае предлагает заменить объект helper class фиктивным объектом, который моделирует поведение реального, при этом не используя связь с объектом data helper, тем самым такой подход позволяет избавиться от критических точек и проверить правильность работы только класса under test. Техниками понижения зависимости и создания фиктивных объектов будет посвящен наш следующий урок.
Ну а пока что мы знаем достаточно о модульном тестирования для того, чтобы разобраться что собой представляет подход к разработке ПО через тестирование, то есть подход Test-Driven Development. Принцип TDD заключается в написании юнит-теста перед написанием самого кода. Для того, чтобы более детально разобраться в этой технике давайте рассмотрим различия между традиционным написанием кода, и написанием кода по стратегии TDD.
Традиционное юнит-тестирование предполагает сначала создание работающего кода, а потом написание теста. Если после выполнения теста замечаются ошибки, они исправляются и цикл повторяется. Что касается разработки через тестирование, то, в отличии от традиционной разработки TDD предполагает сначала написание теста, а потом создание кода, удовлетворяющего всем требованиям этого теста. После прохождения теста проводится рефакторинг нового кода, и далее цикл повторяется. Методика TDD заключается главным образом именно в организации автоматических модульных тестов и выработке определённых навыков тестирования, потому как именно от логики и качества тестов зависит то, будет ли код соответствовать техническому заданию.
Методика TDD имеет ряд преимуществ, по сравнению с традиционной разработкой: кроме того что этот подход позволяет более детально продумать интерфейс до реализации, позволяет избавиться от ошибок на раннем этапе их появлении, сократить время разработки, а также TDD влияет и на дизайн программы, заставляя создавать чёткие и небольшие интерфейсы, тем самым понижая зависимость между классами.
Теперь давайте рассмотрим, какие инструменты нам понадобятся при написании юнит-тестов. Для написания юнит-тестов существуют специальные библиотеки, или юнит-тест фреймворки. Главное преимущество этих фреймворков состоит в возможности выполнять автоматическое тестирование без необходимости писать одни и те же тесты много раз и запоминать результаты их выполнения. Эти юнит-тест фреймворки называют xUnit фреймворками, потому что их имена начинаются с первых букв языка, для которых они были созданы. Сейчас подобные фреймворки доступны для многих языков программирования. Существует приблизительно 150 юнит-тест фреймворков. Платформа .Net имеет 9 таковых, ниже мы видим самые популярные из них: это NUnit, который мы будем использовать на протяжении всего нашего курса, это MS тест и xUnit.Net. Вы сможете перейти по ссылкам и узнать более подробную информацию о них.
Теперь давайте поговорим о проверке юнит-тестов. Процесс создание тесов, как правило, состоит из нескольких последовательных действий: это подготовка участников теста, это действия с тестируемым объектом, и, в конце концов, это проверка результатов. Проверка результатов основывается на утверждениях «истина/ложь». Для таких утверждений NUnit имеет ряд статических методов которые инкапсулированы в класс Assert. Это методы, которые позволяют проверить равенства, идентичности, позволяют сравнивать объекты, узнавать какого типа объект и проверять некие условия.
Дальше мы рассмотрим различные атрибуты, которые можно использовать при создании и выполнении NUnit тестов. Когда мы давали определение юнит-тесту, мы говорили, что это метод который позволяет проверить правильность выполнения некоего блока кода. При использовании NUnit фреймворк результаты тестов должны быть помечены атрибутом TestAttribute и должны храниться в классе, который помечен атрибутом TestFixture, который вы видите на этом слайде. Также в классе содержащем юнит-теста возможно создание и дополнительных методов, которые позволяют провести различные настройки перед запуском и после выполнения каждого теста. Метод, который должен быть запущен перед выполнением каждого теста, должен быть помечен атрибутом SetUp. Метод, который должен запускаться после выполнения теста, должен быть помечен атрибутом TearDown. Метод SetUp можно использовать для создания нового тестируемого объекта перед выполнением нового теста. Метод TearDown можно использовать для освобождения состояния тестируемых объектов перед прохождением нового теста.
Также существуют возможности создание методов, которые должны быть выполнены только один раз: перед и после выполнения всех тестов. Эти методы должны быть помечены атрибутом TestFixtueSetUp и TestFixtueTearDown.
Один из распространённых сценариев модульных тестов предусматривает проверку генерации исключительных ситуаций в тестируемых классах при выполнении определённых условий. Специально для этого NUnit фреймворк имеет атрибут ExpectedException, которым можно декорировать метод теста, при этом тест будет успешно выполнен только в том случае, если в ходе теста будет сгенерирована та исключительная ситуация, тип которой был указан при создании атрибута ExpectedException. Используя именной параметр Massage атрибута ExpectedException также можно указать, какое сообщение должна иметь исключительная ситуация для успешного прохождения теста. Как правило в тестах, ожидающих исключительной ситуации не имеет смысла использовать метод класса Assert.
В первом примере нашего урока мы будем тестировать класс Calculator, который содержит набор методов, позволяющих производить простейшие арифметический действия над целыми числами. При этом мы не будем использовать юнит-тест фреймворки. Метода Add класса Calculator позволяет вернуть сумму своих аргументов, метод Sub – разницу, Mul – произведение и Div – частное. Обратите внимание на то, что метод Mul имеет логическую ошибку.
Специально для того, чтобы протестировать методы класса Calculator мы создали класс CalculatorTest, который содержит тестовый метод TestOperations. В теле метода TestOperations производится ряд логических проверок, позволяющих определить правильности работы методов класса Calculator. Логика теста построена на сравнении ожидаемых результатов с возвращаемыми значениями методов класса Calculator. При соответствии с ожидаемым результатом, тест будет выводит на экран сообщение зелёного цвета, при несоответствии – красного. В теле метода main метода try-catch мы запускаем на выполнение тестовый метод и получаем результаты теста. Все методы класса Calculator, кроме метода Mul, успешно прошли тесты. Главный недостаток этого примера состоит в том, что тестирование всех методов класса Calculator находятся в одном тестовом методе. При генерации исключительной ситуации в хотя бы одном из тестируемых методов – весь тест будет провален. Давайте спровоцируем генерацию исключительной ситуации методом Div. Для этого в качестве второго параметра метода Div передадим 0 и посмотрим на результат. Генерация исключительной ситуации завершила работу метода TestOperations, тем самым методы Add, Sub и Mul остались не оттестированными.
Теперь давайте перейдём к следующему примеру и попробуем устранить те недостатки, о которых мы только что говорили. Во втором примере мы изменили класс CalculatorTest, для того, чтобы избавиться от тех недостатков, которые мы видели в предыдущем примере. Теперь за тестирование каждого функционального требования класса Calculator отвечает отдельный метод в теле класса CalculatorTest. Вызовы этих методов находятся в теле метода TestOperations c 17 по 21 строки. Методы с 17 по 20 строку проверяют корректность возвращаемых значений методов класса Calculator. Метод, вызов которого находится на 21 строке кода, проверяет генерацию исключительных ситуаций методом Div при соответствующих условиях. В тестовых методах, проверяющих возвращаемое значение методов класса Calculator, создаётся конструкция try-catch, в блоке try которых создаётся проверка возвращаемых значений с помощью условной конструкции if-else. Если в конструкции try была сгенерирована исключительная ситуация, то тест можно считать проваленным. Разделение тестирование класса Calculator на несколько тестовых методов позволяет тесты по отдельности, а также приводит к более понятному коду теста.
Теперь давайте рассмотрим тестовый метод, который проверяет генерацию исключительных ситуаций методом Div при соответствующих условиях. Этот метод находится на 90 строке кода. Он может быть успешно пройден только при условии, что вызов метода Div будет генерировать исключительную ситуацию при соответствующих условиях. Только переход к блоку catch будет свидетельствовать о успешном завершении теста. Переход к 95 или же 101 строке будет свидетельствовать о неуспешном завершении теста. Как вы видите создание модульных тестов без использование специальных инструментов может быть довольно утомительной задачей, поэтому давайте перейдём к следующему примеру и познакомимся с NUnit тест фреймворком, который упростит создание и выполнение модульных тестов.
В этом примере мы попробуем протестировать тот же класс Calculator используя фреймворк NUnit. Для этого нам придётся скачать библиотеку NUnit Framework и добавить ссылку на эту библиотеку в наш проект. Это сделать не так сложно, как кажется. Для этого нужно найти раздел References нашего проекта, кликнуть по нему правой клавишей мыши и выбрать меню NuGet Packages. В появившемся окошки в строке поиска набрать Nunit, после чего нажать кнопку Install.
Как мы уже говорили, для того, чтобы начать создавать модульные тесты NUnit, сперва нужно создать класс, который помечен атрибутом TestFixture, что мы и сделали на 8 строке нашего кода. Обратите внимание, что нам потребовалось импортировать пространство имён NUnit.Framework. На 11 строке мы создали метод TestOperations, который пометили атрибутом Test. Этот метод является единственным модульным тестом для класса Calculator. В теле метода TestOperations мы воспользовались статическим методом AEqual класса Assert для проверки возвращаемых методов класса Calculator. Для того, чтобы запустить тесты, потребуется открыть окошко Test Explorer, найти нужный тест и запустить его. Нас интересует тест с именем TestOperations. Наш тест при выполнении будет провален. Он будет провален, потому как метод Mul класса Calculator имеет логическую ошибку. Тут же следует сказать, что тестирование нескольких методов в одном методе не является хорошей практикой. Обратите внимание на метод AreNotEqual класса Assert – этот метод позволяет успешно завершить тест в том случае, если результат работы тестируемого метода не соответствует ожидаемому результату. Если закомментировать 17 строку и раскомментировать 20, тест будет успешно прйден. В следующих примерах мы будем рассматривать различные методы класса Assert.
В этом примере мы рассмотрим различные методы проверки класса Assert. Метод AreSame класса Assert позволяет проверить, ссылаются ли переменные на одну и ту же область памяти. В теле тестового метода AreSame мы создали две переменные типа string, которые инициализировали начальными значениями. Переменную а – строковым литералом «hello», переменную b – строковым литералом «world!». В таком случае переменные a и b ссылаются на разные экземпляры класса string в памяти, и при вызове метода AreSame должна сгенерироваться исключительная ситуация, которая провалит тест. Давайте это проверим. Тест завершается неудачей с сообщением «Expected: same as “hello” But was: “world!”». Если раскомментировать строку номер 27 и повторно запустить тест, он будет успешно пройден, потому как теперь переменные а и b ссылаются на один экземпляр класса string в памяти.
Также класс Assert имеет метод Contains? Который позволяет проверить наличие какого-либо значения в массиве или же коллекции. Перегрузка метода Contains, которую мы используем в этом примере, принимает два аргумента6 первый – это элемент, который мы желаем обнаружить, второй элемент – любой объект, реализующий интерфейс icollection. Тест Contains успешно завершается, так как в коллекции array имеется элемент «Alex».
В следующем примере мы научимся пользоваться методами класса Assert, которые позволяют сравнивать два значения. Это методы: Greater, Less, GreaterOrEqual, LessOrEqual. Обратите внимание, что в теле класса ComparisonsTest создан метод с атрибутом SetUp. Как мы уже говорили, метод помеченный этим атрибутом будет выполняться перед выполнением каждого теста. Поэтому при входе в тело метода Greater или же Less, поля а и b класса ComparisonsTest будут гарантированно иметь значения 10 и 20 соответственно. Поэтому тест Less должен успешно выполнится, потому как 10 менше 20, а Greater – провалиться, потому как 10 не больше 20.
В следующем примере показан пример работы метода IsInstanceOf класса Assert. Этот метод позволяет проверить принадлежность объекта к какому-либо типу. Перегрузка, которую мы используем в этом примере, принимает в качестве первого аргумента ожидаемый тип объекта, в качестве второго – сам объект. Этот тест должен успешно завершиться, потому как строковой литерал «hello» является типом string. Целочисленный литерал «5» не является типом string. Если объект является типом-наследником от ожидаемого типа, то тест, использующий метод IsInstanceOf также будет успешно пройден.
Также класс Asserrt имеет методы, позволяющие конкретные условия. Это такие методы, как IsTrue, IsFalse, IsNone, IsEmpty и IsNotEmpty. Обратите внимание, что все эти методы начинаются с префикса «Is». Тестовые методы IsTrue и IsFalse показывают одноимённые методы класса Assert. В этих тестах используется уже знакомый нам класс Calculator. В качестве аргументов метода IsTrue и IsFalse передаются логические выражения. Тест IsTrue проверяет соответствие возвращаемого значения метода Add с ожидаемым результатом. Тест IsFalse проверяет несоответствие возвращаемого значения метода Mul с ожидаемым результатом. Оба эти теста будут успешно выполнены.
Тест IsNone позволяет проверить, является ли значение не числом. Значение «Not a number» можно получить разделив 0, целого или вещественного типа на значение 0 вещественного типа. Тест IsNone также успешно будет пройден.
Метод IsEmpty класса Assert позволяет обнаружить пустую строку или же пустую коллекцию. Тест IsNotEmpty класса Assert позволяет обнаружить непустую строку или же непустую коллекцию. Тест IsEmpty также будет успешно пройден.
Следующий пример нам продемонстрирует работу методов Ignore и Fail класса Assert. Метод Fail позволяет принудительно провалить тест. Метод Ignore позволяет проигнорировать тест. Вот, мы видим, что то тест, в котором был вызов метода Fail был провален. Тест в котором был метод Ignore был проигнорирован. Если до вызова метода Ignore в теле теста будет сгенерирована исключительная ситуация, тест, конечно же, будет провален. Давайте это проверим. Также для игнорирования теста можно использовать одноименный атрибут – атрибут Ignore. Давайте его раскомментируем. Сейчас тест Ignore будет игнорировать будет игнорироваться, даже если в нём сгенерируется исключительная ситуация.
NUnit фреймворк имеет две основные модели для проверки результата. Первая модель основывается на основе статических методов класса Assert. Вторая модель основывается на неких ограничениях. Для этого класс Assert имеет специальный метод That, для того, чтобы поддерживать эту модель. Существует несколько различных перегрузок этого метода, большинство из них принимает в качестве первого параметра тот объект, который мы хотим проверить, в качестве второго параметра – правило проверки. Второй параметр – правило проверки – как раз и является ограничением. Ограничением может быть любой класс, который реализует интерфейс IresolveConsistant.
На самом деле в NUnit фреймворк уже есть реализовано большое количество ограничений, которые можно найти в следующем пространстве времен. Давайте откроем Object Browser и перейдём к библиотеке NUnit framework и в этой библиотеке перейдём в пространство времён NUnit.Framework.Constraints. Вот тут вы можете увидеть большое количество ограничений, которое уже доступны в .Net Framework. Каждое из них позволяет создать различные проверки для того объекта, которого вы переедете в качестве первого параметра метода That. Эта модель имеет ряд преимуществ перед классической моделью, то есть моделью использования статический методов класса Assert. Первое – мы получаем более гибкую модель, потому как, кроме того, что мы можем в качестве первого параметра передавать объект который хотим проверить. Также можем передавать целый блок кода с помощью делегата ActualValueDelagate. Также можно сообщить сразу несколько вызовов, который нужно проверять с помощью одного ограничения.
Давайте перейдём к телу нашего примера. И первое что мы сделаем – будем проверять некие выражения с помощью классической модели. Для этого на 15 строке мы создали тестовый метод ClassicAreEqual, который на 18 строке c помощью метода AreEqual проверяет, что строковой литерал «Hello» является типом string. На 19 мы проверяем это же условие. Ожидаемый тип у нас System.String и имя типа, которым является строковой литерал мы получаем с помощью свойства FullName объекта type, который нам вернёт метод GetType на строковом литерале. И на 21, 22 строках мы проверяем, что строка не является типом int. И вот тут мы используем обычную, классическую модель. На 26 строке мы создали метод HelperAreEqual, который будет проверять, делает те же действия, только для этого использовать уже модель ограничений. На 29 строки мы вызывает That на объекте Assert, и мы используем перегрузку, которая принимает два аргумента: первый параметр – это то объект, который мы хотим проверить, второй – ограничение, которое мы хотим использовать. При этом обратите внимание, что мы в качестве первого параметра передаём строку «Hello», в качестве второго – некий объект, который нам предоставляет метод typeof объекта Is. Объект Is представляет ряд методов, которые позволяют над создавать различного вида ограничения. Вот видите некоторое количество методов, которые возвращают ограничение. Есть большое количество этих методов.
В нашем примере мы будем использовать метода typeof, который вернёт ограничение, позволяющее проверить принадлежность объекта к типу. В данном случае на 29 строке мы будем проверять, что строковое значение «Hello» является типом string. И на 30 строке мы хотим проверить другое условие: что строковой литерал «Hello» не является типом int. Для этого мы будем пользоваться тем же ограничением TypeOf, но также мы к вызову этого метода на объекте Is добавим обращение к свойству Not, которое позволяет инвертировать результат того ограничения, которое мы укажем дальше, то есть ограничение TypeOf.
На 34 строке мы будем использовать модель для проверок, основанную на ограничениях, только для этого будем пользоваться унаследованным синтаксисом. Дело в том, что унаследовав тестовый класс от некого метода AssertionHeler мы получаем по наследству множество разных членов, метод Expect, который нам заменить конструкцию Assert.That на вызов метода Expect. Вы видите, что на самом деле с 26 по 31, и с 35 по 39 строки – это одни и те же действия, то есть использования моделей проверок, основанных на ограничениях. К тому же, обратите внимание на ещё одно преимущество использования вот такой модели: те выражения, которые мы с вами строим для проверки результатов очень легко читаются. Вот к примеру: “Assert.That(“hello”, Is.TypeOf(string)) “ – читается просто как предложение. Проверить, что строковой литерал “hello” является типом string. В следующем примере мы воспользуемся той же моделью, основанной на ограничениях, но её мы будем использовать для проверки элементов коллекции. Давайте его откроем.
В данном тестовом классе ContextSyntax мы создали три тестовых метода, и каждый из них будем выполнять одни и те же действия, только для этого использовать различные виды синтаксиса создания проверок NUnit. На 14 строке мы создали метод, который для этого будет использовать классическую модель. На 16 строке мы создали массив с элементами типа int. На 17 строке мы массив с элементами типа string. Дальше мы хотим с помощью объекта Assert есть ли в массиве “iarray” элемент со значением “3”, и есть ли в массиве “sarray” элемент со значением “b”. 20 строка, на объекта Assert вызываем контейнер Contains, для этого в качестве первого параметра передаем то значение, которое хотим увидеть в массиве, и в качестве второго параметра параметра – сам массив. 21 Строка – делаем то же самое для массива “sarray”. На 25 строке мы будет выполнять те же действия, только для этого будем использовать модель, основанную на ограничениях. И, в данном примере мы, конечно же, будем использовать метод That, который поддерживает модель ограничений NUnit, но теперь мы будем использовать другой класс: то есть не класс Is – для создания ограничений, а класс Has. Класс Has владеет рядом методов, которые могут создавать ограничения, которые можно применить к коллекциям или же массивам. У нас есть те же два массива “iarray” и “sarray”, и на 31 строке мы вызываем метод That на объекте Assert, в качестве первого первого параметра передаём тот объект, который хотим проверить, а в качестве второго передаём то условие, по которому хотим проверять наш объект. Это у нас ограничение, которое мы получаем посредством вызова метода с именем Member на объекте Has. И вот этот вот метод нам будет возвращать ограничение с типом CollectionContainsConstraint. В качестве параметра мы должны передать то значение, которое мы хотим обнаружить в том массиве, который мы передали в качестве первого параметра. На 33 строке мы хотим сделать то же самое, но теперь мы хотим проверить, что в массиве “sarray” не существует элемента “x”. Для этого мы будем пользоваться уже знакомым нам свойством Not, которое позволяет на инвертировать результат того ограничения, которое мы создаем после вызова этого свойства. На 38 строке мы создаём те же проверки, но для этого использую унаследованный синтаксис. Напомню, что для того, чтобы пользоваться этим синтаксисом нужно класс теста унаследовать от класса AssertionHelper, что мы и сделали. Теперь, вместо класса Assert.That можно просто использовать Expect.
В этом примере мы вспомним атрибуты SetUp и TearDown, которые есть в NUnit Framework и также познакомимся с новым атрибутом – Expected Exception, который нам позволит обнаружить требуемую исключительную ситуацию, которая может произойти при выполнение теста. Начнём мы с атрибутом SetUp и TearDown, для того, чтобы показать работу этих атрибутов мы специально создали некую коллекцию, то есть мы создали свою коллекцию, которая у нас обобщенная, и может хранить любые значения в себе. Эта коллекция позволяет добавлять элементы с помощью метода Add, и удалять элементы с помощью метода Remove. Также у коллекции есть свойство Count, которое позволяет получить количество элементов, которое хранится в нашей коллекции. Наша задача – протестировать методы Add и Remove.
На 25 строке мы создаём тест, который позволит протестировать работу метода Add. Мы будем тестировать его таким образом: с помощью метода Add попробуем добавить три элемента в коллекцию, а затем проверить значение свойства Count. Мы будем ожидать значение 3. Если свойство Count будет иметь значение 3 – значит наш тест успешно пройден, и метод Add правильно работает. 35 строка – мы также создали метод, который позволит протестировать метод Remove, который позволяет удалить элемент из коллекции. Для этого мы сделаем следующее: мы вызовем три раза метод Add для нашей коллекции и добавим три элемента: “first”, “second” и “third”. После чего мы вызовем метод Remove и удалим один элемент из нашей коллекции, а именно: мы будем удалять элемент со значением “first”. 43 строка: после удаления элемента мы хотим проверить значение свойства Count нашей коллекции: если Count будем возвращать значение “2” – это будет говорить о том, что действительно из нашей коллекции удалился элемент, и метод Remove имеет правильную логику, он правильно работает. И вот тут нам целесообразно использовать методы, помеченные атрибутом SetUp и TearDown, потому как без них наши тесты было бы сложно организовать.
Давайте запустим наш тест. Нас сейчас интересуют тесты TestAddingNewElementToUserCollection и TestRemoveElementFromUserCollection. Мы видим, что тесты у нас успешно сработали и это говорит о том, что методы Add и Remove имеют правильную логику, то есть они правильно работают. Если бы мы не использовали методы, помеченные атрибутами. Давайте посмотрим почему. TestRemoveElementFromUserCollection не сработал и TestAddingNewElementToUserCollection также не сработал. Это потому, что мы не инициализировали нашу коллекцию. Давайте её инициализируем. После запуска теста видим, что Adding у нас сработал, TestRemove не сработал. Почему? Потому как в данном случае у нас один раз создастся экземпляр класса TestClass и поэтому только один раз будет инициализировано поле MyCollection. Дальше мы будем выполнять тестовые методы. Когда мы войдём в тестовый метод, который проверяет правильность работы метода Adding, в коллекцию будет добавлено 3 элемента, и мы это проверим, и это действительно будет так.
Дальше мы начнём выполнять тестовый метод, который проверят правильность работы метода Remove. Мы добавим ещё 3 элемента в нашу коллекцию – уже элементов 6. Дальше мы один из элементов удалим из нашей коллекции и проверим ожидаемое значение 2, хотя в коллекции, конечно же, 5 элементов. Тут неплохо было бы сделать, чтоб перед каждым тестом создавался новый экземпляр класса UserCollection, он сохранялся в поле MyCollection и в каждый метод сам уже добавлял элементы и что-то с ними делал. Вот как раз для этого мы и использовали метод, который помечен атрибутом SetUp и метод, который помечен атрибутом TearDown.
Второй пример, который мы тут рассмотрим будет касаться атрибута ExpectedException. Мы с вами говорил, что метод, помеченный атрибутом ExpectedException будет пройден только тогда, если в его теле будет обнаружена исключительная ситуация того типа, которая указана в параметре этого атрибута. Мы будем тестировать класс Calculator, и вы помните, что в классе Calculator есть метод Div, который позволяет поделить одно значение на другое. В этом методе может быть сгенерирована исключительная ситуация, если в качестве второго аргумента мы будем передавать значение 0. В таком случае мы будем ожидать исключительную ситуация типа “DivideByZeroException”. Как раз мы хотим проверить генерацию этой исключительной ситуации. Поэтому мы создали тестовый метод TestCalculatorDivideByZeroException, который пометили атрибутом ExpectedException, указали позиционный параметр, который говорит о том, какого типа должна быть исключительная ситуация – мы указали, что тип исключительной ситуации должен быть DivideByZeroException, и второй параметр – текстовый параметр – позволяет указать, какое сообщение должно быть в исключительной ситуации. В нашем случае будет сообщение «Попытка деления на нуль.». И если мы выполним наш тест, он будет успешно пройден, потому что действительно , при вызове метод с аргументами 1 и 0, будет генерироваться исключительная ситуация с сообщением «Попытка деления на нуль.»
В этом примере мы с вами посмотрим, как обнаружить исключительные ситуации одного типа, при тестировании одного метода. Мы будем тестировать сборку Converter, которая позволяет нам конвертировать деньги из одной валюты в другую. В этой сборке у нас есть базовый абстрактный класс с именем Converter, который предоставляет интерфейс для всех конвертеров, которые можно создать на основе этого базового класса. В этом базовом абстрактном классе есть поле inputCurrency типа Currency на 12 строке. На 15 мы создаем свойство InputCurrency для того, чтобы получить значение поля inputCurrency Currency нашего класса. На 14 строке мы создаем свойство OutputCurrency для записи и для чтения, и создаём свойство Value для записи и для чтения. Также у нас есть конструктов, который позволяет инициализировать начальные значения поля inputCurrency. Также у нас есть в этой сборке перечисление Currency, которые предоставляют несколько различных валют: украинскую гривну, американский доллар, российские рубли и евро. И также у нас есть один конкретный класс Converter – это класс UahConverter, который позволяет конвертировать гривну в любую другую валюту, которая есть в перечислении Currency. В классе Converter у нас создаётся 3 readonly поля типа double с именами rur, usd и eur. 15 строка: мы создаём поле типа double с именем uahValue и 16 строка мы создаём поле outputCurrency типа Currency. 18 строка: мы создаём открытый конструктор UahConverter нашего класса, который принимает курсы рубля, евро и доллара по отношению к гривне. И тут же мы вызываем конструктор базового класса, который принимает один параметр – этим параметром является типом элементом причисления Currency, и в теле конструктора мы проверяем, если хотя бы одно из тех значений, которое пользователь передал в конструктор меньше или равно нулю, то будет генерироваться исключительная ситуация типа ArgumentException с сообщением “Ctor”. Дальше, если у нас не будет сгенерирована исключительная ситуация, мы перейдём к 27, 28 и 29-ым строкам и инициализируем поля rur, usd, eur для нашего класса UahConverter. На 32 строке мы переопределяем свойство outputCurrency из базового класса Converter. Мы реализуем метод get, и реализует метод set, а ткаже переопределяем метод Value из абстрактного класса, и вот тут get и set этого метода будут иметь дополнительную логику. Если мы захотим присвоить некоторое значение этому свойству, то у нас сперва будет проверка: если то значение, которое этому свойству присваивает пользоватль меньше нуля, то будет генерироваться исключительная ситуация типа ArgumentException c сообщением “Value”. 73 строка: если исключительная ситуация не была сгенерирована, то значение, которое мы присваиваем свойству будет присвоено полю uahValue. В методе get мы создали переключатель switch-case, который будет проверять, если outputCurrency, то есть выходная валюту у нас евро – значит нужно вернуть значение uah и поделить на курс евро и вывести гривны. Таким же образом мы реализовали и другие ветки нашего switch-a. Если в значении outputCurrency будет значение, отличающееся от перечисленных в case нашего switch-a, мы будем генерировать исключительную ситуацию.
Теперь давайте создадим тесты, которые позволят протестировать нашу сборку Converter. На 17 строке мы создали тест, который будет проверять правильность тех значений, которые нам будет возвращать класс UahConverter. На 19 строке мы создали экземпляр класса UahConverter и указали тут же курсы рубля, евро и доллара относительно гривны. Это 0,25, 10 и 8 соответственно. Дальше, 21 строка: мы установил входную валюту – это евро. 22 строка: мы указали, что мы хотим конвертировать 1000 гривен в евро. И 24 строка: мы будем использовать метод AreEqual, для того, чтобы проверить, правильно ли у нас сработала логика нашего Converter. Если она сработает правильно, то 1000 гривен у нас должно конвертироваться в 100 евро. И если тест будет успешно пройден, то это будет говорить о том, что действительно наш класс UahConverter имеет правильную логику. Давайте запустим наш тест. Visual Studio определяет какие тесты у нас есть, поэтому пойдём дальше. Также мы видим, что класс UahConverter может сгенерировать исключительную ситуацию типа ArgumentException в двух случаях: при присвоении отрицательного или нулевого значения какому-нибудь параметру конструктора, или же при присвоении отрицательного или нулевого значения свойству “Value”. 33 строка: мы создали экземпляр класса UahConverter и передали туда три значения: “25”, “-10” и “8”. Дальше мы установили выходную валюту и присвоили свойству “Value” значение “1000”. Вот тут будет генерироваться исключительная ситуацию. Мы её ожидаем, поэтому мы на 30 строке пометили этот метод атрибутом ExpectedException, и указали, что мы ожидаем исключительную ситуацию тпа ArgumentException. Понятное дело, что наш тест будет успешно пройден: мы хотим обнаружить исключительную ситуацию ArgumentException, которая генерируется в результате передачи неправильного параметра конструктора. Но представим и такую ситуацию, что мы будем передавать отрицательное значение свойству “Value”, и вот тут наш тест будет работать, он выполнится, но он не покажет тех результатов, которые мы ожидаем. Потому что мы ожидаем исключительной ситуации в конструкторе, а на самом деле исключительная ситуация будет происходить в свойстве “Value” и это не то, что нам нужно. И, тем не менее, мы получим с вами успешно пройденный тест. Поэтому дальше мы посмотрим, как исключить правильное прохождение теста в том случае, если исключительную ситуацию сгенерировал не тот метод, который мы хотели. 42 строка: мы создаём ещё один тест, который должен быть успешно пройден, только в том случае, если исключительная ситуация будет сгенерирована при передаче неправильных параметров. Для этого мы создали конструкцию try-catch. В try на 46 строке мы создаём экземпляр класса UahConverter, которому мы передаём неправильные значения. То есть тут у нас должна быть сгенерирована исключительная ситуация. Если это будет действительно так, то мы перейдём в блок catch, перехватим нашу сгенерированную ситуацию и на 53 строке мы проверим, если наша исключительная ситуация имеет строку “Ctor” – конструктор – значит всё хорошо, сгенерировалась та исключительная ситуация, которую мы ожидали. Если исключительную ситуацию у нас сгенерирует свойство “Value”, значит у нас метод Contains объекта StringAssert будет генерировать исключительную ситуацию, то есть, тем самым тест у нас будет пройден неуспешно. Если же в блоке try у нас не будет сгенерирована какая-либо ситуация, в таком случае мы попадём сразу же на 57 строку и завалим тест, потому что у нас не было сгенерировано исключительных ситуаций. Таким же образом мы реализуем логику теста для проверки исключительно ситуация, которая будет сгенерирована именно при присвоении свойству “Value” неправильного значения. К тому же, можно ещё пользоваться и именными параметрами атрибута ExpectedException. Если мы хотим ожидать генерацию исключительной ситуации именно от конструктора, мы можем свойству UserMessage нашего атрибута присвоить значение “Ctor”. Таким образом тест будет успешно пройден только в том случае, если эту исключительную ситуацию будет порождать конструктор.
В этом примере мы попробуем дополнить класс Caluclator, чтобы он мог считать квадратные корни, и для этого мы будем использовать методологию TDD. Как мы уже говорили, методолагия предусматривает сначала создание теста, а потом создание кода. Поэтому давайте создадим тест, который будет проверять работу метода класса калькулятора, который должен рассчитывать квадратный корень. Этот тест находится с 14 по 31 строку нашего кода. Давайте его раскомментируем. В теле этого тестового метода мы создаём экземпляр класса Calculator, на 21 строке мы определяем выходное значение, то есть то значение, которое должен вернуть метод, рассчитывающий квадратный корень. На 23 строке мы определяем входное значение, которое записываем в переменную Input. 26 строка: на экземпляре класса Calculator мы вызываем метод SquareRoot, передаём туда входное значение, которое мы рассчитали на 23 строке, получаем результат работы этого метода, и на 29 строке с помощью метода AreEqual мы проверяем соответствие работы метода SquareRoot с тем результатом, который мы хотим увидеть. На данный момент наш код не будет компилироваться, так как наш метод SquareRoot отсутствует в классе Calculator. Поэтому давайте создадим заглушку для этого метода. Я воспользовался Visual Studio для автоматического создания заглушки, если я перейду , и если я перейду в класс Calculator, то я увижу, что появился метод SquareRoot, который принимает значение типа double и возвращает значение типа double. Но этот метода не реализован и в его теле будет генерироваться исключительная ситуация NotImplementedException. На данный момент, наш тест, конечно же, не будет успешно пройден, и это будет говорить о, просто-напросто, производственной недоработке нашего кода. Поэтому, на следующих шагах нашего примера будем реализовывать. Видите, наш тест не проходит.
Я перейду в класс Calculator, метод SquareRoot, и попробую его реализовать , для того чтобы пройти метод BasicRootTest . Для расчёта корня числа мы будем пользоваться итерационной формулой Герона, которая у нас продемонстрирована на 8 строке. Эта формула задаёт убывающую прогрессию, которая при любом x1 быстро сводится к величине корня числа а. Давайте реализуем эту формулу в методе SquareRoot. Для этого сперва я создам две переменные типа double. Это переменные result – то значение, которое мы будем возвращать, и переменная previousResult. И этим переменным нужно каким-то образом задать начальные значения. У нас тут не написано в комментарии, но на самом деле предыдущий результат, то есть previousResult – x0, должен иметь значение input, то есть того значение, корень которого мы хотим найти. Result может быть абсолютно любым значением. Давайте так же будет присваивать значение input. Следующее, что нам потребуется сделать – это создать конструкцию for для расчёт элементов той прогрессии, которую задаёт наша формула. Для этого я создам цикл for, но пока не знаю количество итераций, которое нужно выбрать. На 11 строке написано, что при любом x1 наша прогрессия будет быстро сводиться к величине корня числа а. Сперва создам цикл на 5 итераций. В теле цикла я буду рассчитывать значение переменной result. Result присвоить, и, дальше я напишу нашу формулу. Формула имеет следующий вид: “(result+input/result)/2”. Всё, что мне потребуется дальше – это просто-напросто вернуть значение переменной result. Давайте запустим тест, но ожидать значение, допустим, “4”, для того, чтобы наша прогрессия более быстро сходилась к корню числа 16. Я запускаю тест, и мы видим, что он у нас успешно пройден. Но логика метода SquareRoot класса Calculator будет правильно работать не во всех случаях: допустим если я захочу посчитать корень некого такого числа, то я получу ошибку, потому как мне возвращается не то значение, которое мне нужно. Это из-за того, что 5 итераций недостаточно, чтобы получить наш ожидаемый результат с точность одна тысячная ожидаемого результата. Придётся создать цикл с большим количеством итераций: давайте попробуем 20. Вот теперь наш тест успешно пройден. Далее что мы сделаем – создадим ещё один тест, который будет тестировать, что наш метод SquareRoot класса Calculator должен уметь считать квадратные корни в неком диапазоне чисел: от “1*10^-8” до “1*10^8”. Также мы будем задавать точность не меньше одной тысячной предполагаемого результата.
Давайте раскомментируем тест RooterValueRange. В теле нашего теста на 41 строке мы будем создавать экземпляр класса Calculator, на 44 строке мы будем создавать цикл for, который каждую итерацию будет вызывать метод RooterOneValue, который будет считать корень от некоего числа. При этом цикл for нам позволит протестировать наш метод в диапазоне значений. Здесь нужно изменить на 53 строке. Мы работаем не с классом Rooter, а с классом Calculator. Когда я скомпилируюсь – у меня всё будет компилироваться. Давайте запустим тест и посмотрим, действительно ли он у нас успешно выполняется. Мы видим, что это не так. Поэтому придётся что-то сделать с классом Calculator, для того, чтобы этот тест был успешно пройден. Для этого я сделаю следующее: цикл for я заменю на циклическую конструкцию while, в котором буду проверять точность рассчитываемого результата. То есть теперь количество итераций будет зависеть оттого, насколько точный результат мы хотим получить. И для расчёта точности я буду использовать следующую формулу: модуль от предыдущего результата минус текущий результат, и буду проверять это значение с одной тысячной результата. Что мне ещё потребуется сделать – это переменной previousResult каждый раз присваивать значение переменной result. Теперь давайте попробуем запустить наш тест, и посмотрим, успешно ли он у нас выполняется. Запускаем тест, но пока ещё есть какая-то ошибка. А ошибка у нас заключается вот в чём. Давайте запустим тест и убедимся, что теперь он будет правильно отрабатывать.
Что ещё нам потребуется сделать в нашем методе SquareRoot: нам потребуется создать логику, которая будет проверять, если мы хотим посчитать корень от отрицательного числа, то нам нужно генерировать исключительную ошибку. И специально для этого мы создали ещё один тест, который у нас находится на 65 строке. В теле этого теста мы будем создавать экземпляр класса Calculator, создавать конструкцию try-catch. В блоке try мы будем считать корень от отрицательного числа и если у нас будет при этом генерироваться исключительная ситуация ArgumentOutOfRangeException, значит наш тест будет успешно проходить, иначе – будем вызывать метод Fail на объекте Assert, и это будет свидетельствовать о том, что тест не прошёл. Давайте попробуем запустить наш тест. Ждём выполнения. Ну а окончания выполнения тесты мы с вами никогда не дождёмся, потому что наша логика будет считать бесконечно. Пока что давайте перейдём к классу Calculator, и перед созданием переменных и перед началом работы цикла while будем создавать проверку: если то значение, от которого мы хотим посчитать корень – меньше или равно нуль, значит нужно сгенерировать исключительную ситуацию типа ArgumentOutOfRangeException. Давайте запустим TestRooter, TestNegativeInput и видим,что наш тест также успешно проходит. Тем самым с помощью методологии TDD мы дополнили класс Calculator новой возможностью. Я думаю вы сами можете оценить преимущества этого подхода. К тому же, теперь наши тесты описывают возможности метода SquareRoot лучше любой документации. Взглянув на эти тесты мы сможем узнать, что метод SquareRoot читает корни чисел в некоем диапазоне с точностью до тысячной ожидаемого результата, и при попытке посчитать корень от отрицательного результата, будет генерироваться исключительная ситуация типа ArgumentOutOfRangeException.