Unit Testing в C# с использованием NUnit и NSubstitute. Часть 1

Unit Testing в C# с использованием NUnit и NSubstitute. Часть 2

Unit Testing в C# с использованием NUnit и NSubstitute. Часть 3

Давненько я не писал ничего в блоге. Задумок на самом деле хватает, даже материл для части уже подготовлен. Но это же надо сесть и написать, а учитывая, что блог я никак не продвигал и посетителей практически не было – запал поугас еще в конце прошлого года. И тут я зашел в комментарии от Вконтакте и увидел, что есть вопросы и отзывы и от января, и от февраля. Т.е. посетители есть, блог пользу приносит. И решил продолжить.

Первая статья будет на тему Unit Test‘ов применимо к C#.
Давным давно я как мог увиливал от написания юнит тестов. Это выходило за зону моего комфорта, и сейчас я жалею об этом. (О зоне комфорта и искусстве избегании неприятной работы можно не один трактат накатать). Но лучше поздно, чем никогда.
За последние месяца три-четыре были прочитаны книги касательно этой тематики:
Test Driven iOS Development
The Art Of Unit testing
Эффективная работа с унаследованным кодом

Test Driven iOS Development  – читал раньше всех, и к сожалению не успел применить на практике, т.к. срочно надо было сначала заняться разработкой сайта, а затем C# проектом, поэтому многое успело выветриться из головы. Когда вернусь к iOS разработке прийдется перечитывать, или по крайней мере пробежаться глазами. На самом деле мой большой iOS проект, который начинался как прототип, и в итоге вырос в полноценный практически готовый для AppStore проект и сподвиг меня сесть за изучение Unit Test‘ов. Ведь проектирование хромало на все 4 конечности (а ведь умные люди не зря писали – сделал прототип, проверил, – выбрось прототип и пиши с нуля), про ВОЗМОЖНОСТЬ тестирования я промолчу. Но не стоит думать, что писалось абы как. Старался писать нормально, но между хорошо бы делать код тестируемым и писать только тестируемый лежит бесконечная пропасть. А по книге – рекомендую, она довольно последовательная, с множеством примеров. Естественно рекомендую я ее в основном iOS разработчикам, т.к. немало материала относится конкретно к Objective-C и Cocoa разработке. Без набора и выполнения кода для закрепления материала уже к середине книге я начал вязнуть. Но впрочем это говорит лишь о глубине поданного материала.
P.S. Уже в процессе написания статьи увидел, что оказывается вышел перевод на русский этой книги. Разработка через тестирование для iOS, причем есть как электронный вариант, так и бумажный. Сам я ее не читал, так что качество перевода оценить не смогу

The Art Of Unit testing – неплохая книга для того, чтобы разобраться в основах темы. Но на мой взгляд ей недостает практических примеров. И все же крайне рекомендую начать именно с нее. Потом взять простенький класс, у которого особо нет внешних зависимостей, и попробовать покрыть тестами. В идеале попробовать сразу TDD (Test Drive Development). Я так и сделал, для первого необходимого мне класса я сначала писал тесты, и лишь затем писал код. Что меня поразило, – благодаря тестам – еще до интеграции нового класса в реальную систему, – практически все недочеты были отловлены. Проектируя второй подобный класс в условиях ограниченного времени (как всегда) я самонадеянно решил, что тесты можно уже и не писать. В итоге получил несколько не очень приятных багов, потратил еще больше времени, чем если бы писал тесты сразу. И после этого решил взяться за тестирование серьезно.

Т.к. эта книга по сути написана для C# программистов, именно из нее последовал выбор библиотек NUnit, NSubstitute, так же была оставлена без изменений система именования.

Эффективная работа с унаследованным кодом – книга посерьезней в плане объема да и подача материала потяжелее второй. И плюс и минус, что в отличии от предыдущей, здесь рассмотрены примеры для нескольких языков: Java, C++, C. Советую прочитать для расширения кругозора.

Итак, пожалуй что начнем. Я расскажу как настроить Visual Studio для написания тестов, так же приведу 3 примера. Первый – в этой статье будет элементарный и надуманный, чтобы дать базис. Во второй части будет сравнительно простой пример, вспомню как покрывал тестами в первый раз. Во третьей же части статьи будет пример посложнее и будет мной писаться параллельно коду. Чтобы было видно, что не боги горшки обжигают.

В первую очередь нужно установить NUnit, но для начала стоит установить в VisualStudioNuGet, это расширение позволяющее устанавливать пакеты в одну команду из консоли.
Откройте Visual Studio, и выберите в меню Tools | Extensions and Updates (раньше был Extension Manager)

visual_studio_nuget_context_menu

Выберите пункт Online слева и в поиске справа наберите Nuget, т.к. у меня стоит VS 2013 – я выбрал именно выделенный аддон. Жмем Download и устанавливаем

Extensions and Updates

Так же можно установить NUnit Test Adapter – интеграция запуска тестов в студию (хотя я пользуюсь Resharper, поэтому мне особо не нужно)
Пока не рекомендую устанавливать Unit Test Generator, прочитал о проблеме в отзывах но не обратил внимания. После установки – перестает работать переход в имплементацию класса/метода по Ctrl+Click. Проблема сразу исчезла после удаления плагина.

Итак, теперь у нас есть NuGet, но нам нужны NUnit (я буду использовать именно этот фреймворк вместо встроенного в Visual Studio) совместно с NSubstitute.

Создадим тестовый консольный проект и назовем его SampleProject, я установил галочку напротив Create directory for solution, чтобы папка с тестами для проекта была не вложенной в сам проект, а была на одном уровне.

sample_Project_create

Теперь создадим проект, содержащий юнит тесты для нашего проекта

create_unit_test_project

Прописываем имя проекта

path_to_save

Заметьте что в Location я поднялся на уровень выше, изначально мне рекомендовали создать проект юнит тестов внутри папки с проектом.

Как я уже писал выше, name convention будет взят из книги The Art Of Unit testing. Для именования проекта с тестам оно следующее – <Тестируемый проект>.UnitTests. Таким образом у нас проект с тестами будет называться SampleProject.UnitTests

После создания проекта SampleProject.UnitTests – откроем созданный файл UntTest1.cs

mstest

Как мы видим сейчас используется встроенный VisualStudio фреймворк для тестирования. Понять это можно по атрибутам – у класса [Test], у метода [TestMetod]. Мы же хотим использовать NUnit у которого вместо этих другие атрибуты [TestFixture] и [Test] соответственно. Попробуем поменять, но они подчеркиваются красным, т.к. пока VisualStudio о них ничего не знает.

try_nunit

Открываем консоль NuGet

console

И набираем команду
Install-Package NUnit
а затем
Install-Package NSubstitute

ВАЖНО: не забудьте выбрать в качестве дефолтного проекта ваш проект с юнит тестами а не с программой. Иначе библиотеки установятся не в тот проект.

console_result

Добавляем в UnitTest1
using NUnit.Framework;

и видим, что компилятор теперь понимает наши атрибуты, старый using можно удалить (using Microsoft.VisualStudio.TestTools.UnitTesting)

new_using

Я надеюсь вы прочитали книгу The Art Of Unit testing или просто имеете понятие как работать в общих чертах с NUnit, т.к. глубоко базис я объяснять не планирую, немного не тот формат статьи.

Если уж совсем вкратце – главное в юнит тесте – это утверждение об истинности предположения (Assert).
Создадим в основном проекте SampleProject папку Utilites и создадим там интерфейс IAccount (папка создается для визуального выделения классов и вовсе не обязательна)

и класс его реализующий Account

Пример простой как 2 копейки. мы хотим проверить, что после создания обьекта Account c определенным id мы его получим обратно через метод GetAccountId()

Итак, для начала в проекте юнит тестов создадим класс AccountTests (методика именования класса <Тестируемый класс>Tests). Причем, т.к. в SampleProject класс Account расположен не в корне а в папке Utilites, то и в проекте тестов стоит создать такую же папку и положить туда класс с тестами.
Причем с одной стороны класс AccountTests можно создать из контекстного меню Add -> Unit Test… – но создастся опять UnitTest1.cs, причем настроенный для работы с MS фреймворком а не NUnit.
Потому на мой взгляд проще создать просто класс и вручную прописать атрибуты [TestFixture] и [Test] соответственно. Хотя это дело вкуса. Как я упоминал выше, для автоматизации создания шаблонов тестов я устанавливал аддон Unit Test Generator, он позволяет в диалоге прописать какой тип тестов – как называть, и т.д. но его установка повлекла проблемы с функционированием переходов через Ctrl+Click, что для меня фатально.

Итого получаем такой код:

Создадим собственно тест. Именование тестов вопрос спорный, я придерживаюсь методикой взятой из книги.
ЧтоТестируем_СостояниеТеста_ОжидаемоеПоведение (в оригинале UnitOfWork_StateUnderTest_ExpectedBehavior)
Хотел бы подробнее коснуться StateUnderTest – я бы его перевел более кратко как заметки. Т.е. по сути вкратце описать что мы делаем в тесте: AddToAccount, AddNullAccount и все в том же духе. Скажу честно – корректное именование для меня больной вопрос. В свое время я мог по полчаса рождать гору, а в итоге все равно рождалась мышь. Так что сейчас моя позиция такова – подумай минуту, если что то получилось похожее на правду – пусть и будет правдой. Прийдет гениальная идея позже – переименуешь, благо благодаря утилитам по рефакторингу это сейчас занимает 5 секунд. Такой подход сэкономил мне тучу времени и нервных клеток.
В нашем случае мы тестируем метод GetAccountId
в заметках укажем, что мы проинициализировали обьект, а получаем мы на выходе корректный (тот же) id

Изначально я назвал метод GetAccountId_InititalizedObject_ReturnCorrectId, затем переименовал в
GetAccountId_WasInitializedWithId_ReturnSameId

Начинаем создавать в коде Account
Account account = new Account();
и видим ошибку, наш проект юнит тестов ничего пока не знает о тестируемом проекте
что ж, надо добавить reference на исходный SampleProject. Вызываем диалог

add_reference

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

select_solution

После этого в класс AccountTests добавляем using SampleProject.Utilites; и код начинает компилироваться

дописываем код, должно получиться что то вроде этого

запускаем тест (я запускаю с помощью runner’а от Resharper, но есть и бесплатно решение, тот же NUnit Test Adapter, который запускается так:)

nunit_test_runner

и видим что он проходит

first_passed

Что в тесте? Мы создаем обьект Account, инициализируя его id, а затем командой Assert.AreEqual(account.GetAccountId(), initId); проверяем, что метод GetAccountId() возвращает то же значение, что и было при инициализации.

Если хотите увидеть, как тест не проходит измените строку с проверкой на

Assert.AreEqual(account.GetAccountId(), 0);
либо на
Assert.AreNotEqual(account.GetAccountId(), initId);

В общем работает, хотя слишком просто и притянуто за уши. Прошу немного терпения.
Разберемся с тем, зачем на NSubstitute
Самое сложное пожалуй в unit testing‘е – это работа с объектами которые не тестируются. Представим себе, что в методе GetAccountId() на самом деле выполняется сотня строк кода, делается вызов по сети, обращения к базе данных и все в таком духе. Не самая весела перспектива тащить все это в UnitTest’ах, которые должны быть легковесными и быстрыми, чтобы в идеале хоть при каждой компиляции их запускать и убеждаться, что код не сломан внесенными изменениями. А обращения к сети, к файловой системе –
а) требуют настройки
б) могут занимать ощутимое время
в) по сути являются интеграционными тестами, которые тоже важны, но сейчас не о них.

Какое решение – конечно врать и подменять ложью все что нам неподвластно ) Создавать Fake, Dumb, Stub объекты. Обьекты которые реализуют интерфейс, но по сути делающие то, что им скажут.

До появления фреймворков для тестирования программисты так и создавали вручную FakeAccount, в котором метод GetAccountId возвращал хоть жестко прописанное значение, хоть настраиваемое из тестов и подсовывали его в тестируемый код через конструктор, свойство или фабрику.
Подсовывать приходится и сейчас, чудес не бывает, но вот создавать на каждый чих Stub объекты больше нет нужды (в основном). Представим себе, что у нас есть класс AccountManager у которого есть метод, который по переданному Account‘у – возвращает строку описывающую аккаунт

И представим себе, что ну нет у нас возможности получить Account, т.к. он еще в конструктор делает обращения к сети. Но у нас есть его интерфейс и NSubstitute

Пишем тест

Итак, что у нас тут.
var account = Substitute.For(<IAccount>);
Это и есть магия NSubstitute. Этой простой командой мы создали фейк обьект для интерфейса IAccount.
а этой строкой
account.GetAccountId().Returns(id);
мы указали, что при любом вызове метода GetAccountId для нашей обманки – будет возвращаться наш id, т.е 777
Дальше мы подставили фейковый аккаунт в тестируемый метод и сравнили полученное и ожидаемое, все сошлось. (хотя врать не буду, изначально тест упал, т.к. я указал @”id = ” + id, а не @”Id = ” + id, мелочь, но такие мелочи могут стоить многих часов отладки)

У NSubstitute огромные возможности, я крайне рекомендую хотя бы пробежаться по диагонали на официальном сайте http://nsubstitute.github.io/help/getting-started/
Вкратце:
– создавать фейк можно для делегатов, сразу для нескольких интерфейсов, более того, хоть и с ограничениями но и для класса
– можно указывать что возвращать в зависимости от переданных параметров calculator.Add(1, 2).Returns(3); – вернет 3 только если метод вызовут именно с параметрами 1 и 2
– или посложнее calculator.Add(Arg.Any(), 5).Returns(10); вернет 10 для любого первого параметра и 5 – в качестве второго
– можно указывать разные значения в качестве возвращаемых в зависимости от того в какой по счету раз были вызван метод или свойство

Ну и многое, многое другое.
Проект можно скачать отсюда.

Продолжение в Unit Testing в C# с использованием NUnit и NSubstitute. Часть 2

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.