- PVSM.RU - https://www.pvsm.ru -
Даже если вы никогда в жизни не думали, что занимаетесь тестированием, вы это делаете. Вы собираете свое приложение, нажимаете кнопку и проверяете, соответствует ли полученный результат вашим ожиданиям. Достаточно часто в приложении можно встретить формочки с кнопкой “Test it” или классы с названием TestController или MyServiceTestClient.
То что вы делаете, называется интеграционным тестированием. Современные приложения достаточно сложны и содержат множество зависимостей. Интеграционное тестирование проверяет, что несколько компонентов системы работают вместе правильно.
Оно выполняет свою задачу, но сложно для автоматизации. Как правило, тесты требуют, чтобы вся или почти вся система была развернута и сконфигурирована на машине, на которой они выполняются. Предположим, что вы разрабатываете web-приложение с UI и веб-сервисами. Минимальная комплектация, которая вам потребуется: браузер, веб-сервер, правильно настроенные веб-сервисы и база данных. На практике все еще сложнее. Разворачивать всё это на билд-сервере и всех машинах разработчиков?
Давайте сначала спустимся на предыдущий уровень и убедимся, что наши компоненты работают правильно по-отдельности.
Обратимся к википедии:
Модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.
Таким образом, юнит-тестирование – это первый бастион на борьбе с багами. За ним еще интеграционное, приемочное и, наконец, ручное тестирование, в том числе «свободный поиск».
Нужно ли все это вам? С моей точки зрения ответ: «не всегда».
В первых трех случаях по объективным причинам (сжатые сроки, бюджеты, размытые цели или очень простые требования) вы не получите выигрыша от написания тестов.
Последний случай рассмотрим отдельно. Я знаю только одного такого человека, и если вы не узнали себя на фото ниже, то у меня для вас плохие новости.
В своей практике я много раз встречался с проектами старше года. Они делятся на три категории:
Проекты первого типа – крепкий орешек, с ними работать тяжелее всего. Обычно их рефакторинг по стоимости равен или превышает переписывание с нуля.
Коллеги из ScrumTrek уверяют, что всему виной темная сторона кода и властелин Дарт Автотестиус [1]. Я убежден, что это очень близко к правде. Бездумное написание тестов не только не помогает, но вредит проекту. Если раньше у вас был один некачественный продукт, то написав тесты, не разобравшись в этой теме, вы получите два. И удвоенное время на сопровождение и поддержку.
Для того чтобы темная сторона кода не взяла верх, нужно придерживаться следующих основных правил.
Ваши тесты должны:
Чтобы достичь выполнения этих пунктов, нужны терпение и воля. Но давайте по порядку.
Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.
Рассмотрим сначала экстремальные случаи: простой код без зависимостей и сложный код с большим количеством зависимостей.
Что у нас остается:
Отлично зарекомендовал себя подход AAA (arrange, act, assert) . Вернемся к примеру с калькулятором
class CalculatorTests
{
public void Sum_2Plus5_7Returned()
{
// arrange
var calc = new Calculator();
// act
var res = calc.Sum(2,5);
// assert
Assert.AreEqual(7, res);
}
}
Такая форма записи гораздо легче читается, чем
class CalculatorTests
{
public void Sum_2Plus5_7Returned()
{
Assert.AreEqual(7, new Calculator().sum(2,5););
}
}
А значит, этот код проще поддерживать.
Каждый тест должен проверять только одну вещь. Если процесс слишком сложен (например, покупка в интернет магазине), разделите его на несколько частей и протестируйте их отдельно.
Если вы не будете придерживаться этого правила, ваши тесты станут нечитаемыми, и вскоре вам окажется очень сложно их поддерживать.
До сих пор мы тестировали калькулятор. У него совсем нет зависимостей. В современных бизнес-приложениях количество таких классов, к сожалению, мало.
Рассмотрим такой пример.
public class AccountManagementController : BaseAdministrationController
{
#region Vars
private readonly IOrderManager _orderManager;
private readonly IAccountData _accountData;
private readonly IUserManager _userManager;
private readonly FilterParam _disabledAccountsFilter;
#endregion
public AccountManagementController()
{
_oms = OrderManagerFactory.GetOrderManager();
_accountData = _ orderManager.GetComponent<IAccountData>();
_userManager = UserManagerFactory.Get();
_disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
}
}
Фабрика в этом примере берет данные о конкретной реализации AccountData из файла конфигурации, что нас абсолютно не устраивает. Мы же не хотим поддерживать зоопарк файлов *.config. Более того, настоящие реализации могут зависеть от базы данных. Если мы продолжим в том же духе, то перестанем тестировать только методы контроллера и начнем вместе с ними тестировать другие компоненты системы. Как мы помним, это называется интеграционным тестированием.
Чтобы не тестировать все вместе, мы подсунем фальшивую реализацию (fake).
Перепишем наш класс так:
public class AccountManagementController : BaseAdministrationController
{
#region Vars
private readonly IOrderManager _oms;
private readonly IAccountData _accountData;
private readonly IUserManager _userManager;
private readonly FilterParam _disabledAccountsFilter;
#endregion
public AccountManagementController()
{
_oms = OrderManagerFactory.GetOrderManager();
_accountData = _oms.GetComponent<IAccountData>();
_userManager = UserManagerFactory.Get();
_disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
}
/// <summary>
/// For testability
/// </summary>
/// <param name="accountData"></param>
/// <param name="userManager"></param>
public AccountManagementController(
IAccountData accountData,
IUserManager userManager)
{
_accountData = accountData;
_userManager = userManager;
_disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
}
}
Теперь у контроллера появилась новая точка входа, и мы можем передать туда другие реализации интерфейсов.
Мы переписали класс и теперь можем подсунуть контроллеру другие реализации зависимостей, которые не станут лезть в базу, смотреть конфиги и т.д. Словом, будут делать только то, что от них требуется. Разделяем и властвуем. Настоящие реализации мы должны протестировать отдельно в своих собственных тестовых классах. Сейчас мы тестируем только контроллер.
Выделяют два типа подделок: стабы (stubs) и моки (mock).
Часто эти понятия путают. Разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок – это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.
С технической точки зрения это значит, что используя стабы в Assert мы проверяем состояние тестируемого класса или результат выполненного метода. При использовании мока мы проверяем, соответствуют ли ожидания мока поведению тестируемого класса.
[Test]
public void LogIn_ExisingUser_HashReturned()
{
// Arrange
OrderProcessor = Mock.Of<IOrderProcessor>();
OrderData = Mock.Of<IOrderData>();
LayoutManager = Mock.Of<ILayoutManager>();
NewsProvider = Mock.Of<INewsProvider>();
Service = new IosService(
UserManager,
AccountData,
OrderProcessor,
OrderData,
LayoutManager,
NewsProvider);
// Act
var hash = Service.LogIn("ValidUser", "Password");
// Assert
Assert.That(!string.IsNullOrEmpty(hash));
}
[Test]
public void Create_AddAccountToSpecificUser_AccountCreatedAndAddedToUser()
{
// Arrange
var account = Mock.Of<AccountViewModel>();
// Act
_controller.Create(1, account);
// Assert
_accountData.Verify(m => m.CreateAccount(It.IsAny<IAccount>()), Times.Exactly(1));
_accountData.Verify(m => m.AddAccountToUser(It.IsAny<int>(), It.IsAny<int>()), Times.Once());
}
Почему важно понимать, казалось бы, незначительную разницу между моками и стабами? Давайте представим, что нам нужно протестировать автоматическую систему полива. Можно подойти к этой задаче двумя способами:
Запускаем цикл (12 часов). И через 12 часов проверяем, хорошо ли политы растения, достаточно ли воды, каково состояние почвы и т.д.
Установим датчики, которые будут засекать, когда полив начался и закончился, и сколько воды поступило из системы.
Стабы используются при тестировании состояния, а моки – взаимодействия. Лучше использовать не более одного мока на тест. Иначе с высокой вероятностью вы нарушите принцип «тестировать только одну вещь». При этом в одном тесте может быть сколько угодно стабов или же мок и стабы.
Мы могли бы реализовывать моки и стабы самостоятельно, но есть несколько причин, почему я не советую делать это:
В примере выше я использовал фреймворк Moq [2] для создания моков и стабов. Довольно распространен фреймворк Rhino Mocks [3]. Оба фреймворка — бесплатные. На мой взгляд, они практически эквивалентны, но Moq субъективно удобнее.
На рынке есть также два коммерческих фреймворка: TypeMock Isolator и Microsoft Moles. На мой взгляд они обладают чрезмерными возможностями подменять невиртуальные и статические методы. Хотя при работе с унаследованным кодом это и может быть полезно, ниже я опишу, почему все-таки не советую заниматься подобными вещами.
Шоукейсы перечисленных изоляционных фреймворков можно посмотреть тут [4]. А информацию по техническим аспектам работы с ними легко найти на Хабре.
Вернемся к примеру с контроллером.
public AccountManagementController(
IAccountData accountData,
IUserManager userManager)
{
_accountData = accountData;
_userManager = userManager;
_disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
}
Здесь мы отделались «малой кровью». К сожалению, не всегда все бывает так просто. Давайте рассмотрим основные случаи, как мы можем внедрить зависимости:
Добавляем дополнительный конструктор или заменяем текущий (зависит от того, как вы создаете объекты в вашем приложении, используете ли IOC-контейнер). Этим подходом мы воспользовались в примере выше.
Setter можно дополнительно «спрятать» от основного приложения, если выделить интерфейс IUserManagerFactory и работать в продакшн-коде по интерфейсной ссылке.
public class UserManagerFactory
{
private IUserManager _instance;
/// <summary>
/// Get UserManager instance
/// </summary>
/// <returns>IUserManager with configuration from the configuration file</returns>
public IUserManager Get()
{
return _instance ?? Get(UserConfigurationSection.GetSection());
}
private IUserManager Get(UserConfigurationSection config)
{
return _instance ?? (_instance = Create(config));
}
/// <summary>
/// For testing purposes only!
/// </summary>
/// <param name="userManager"></param>
public void Set(IUserManager userManager)
{
_instance = userManager;
}
}
Вы можете подменить всю фабрику целиком. Это потребует выделение интерфейса или создание виртуальной функции, создание объектов. После этого вы сможете переопределить фабричные методы так, чтобы они возвращали ваши подделки.
Если зависимости инстанцируются прямо в коде явным образом, то самый простой путь – выделить фабричный protected-метод CreateObjectName() и переопределить его в классе-наследнике. После этого тестируйте класс-наследник, а не ваш первоначально тестируемый класс.
Например, мы решили написать расширяемый калькулятор (со сложными действиями) и начали выделять новый слой абстракции.
public class Calculator
{
public double Multipy(double a, double b)
{
var multiplier = new Multiplier();
return multiplier.Execute(a, b);
}
}
public interface IArithmetic
{
double Execute(double a, double b);
}
public class Multiplier : IArithmetic
{
public double Execute(double a, double b)
{
return a * b;
}
}
Мы не хотим тестировать класс Multiplier, для него будет отдельный тест. Перепишем код так:
public class Calculator
{
public double Multipy(double a, double b)
{
var multiplier = CreateMultiplier();
return multiplier.Execute(a, b);
}
protected virtual IArithmetic CreateMultiplier()
{
var multiplier = new Multiplier();
return multiplier;
}
}
public class CalculatorUnderTest : Calculator
{
protected override IArithmetic CreateMultiplier()
{
return new FakeMultiplier();
}
}
public class FakeMultiplier : IArithmetic
{
public double Execute(double a, double b)
{
return 5;
}
}
Код намеренно упрощен, чтобы акцентировать внимание именно на иллюстрации способа. В реальной жизни вместо калькулятора, скорее всего, будут DataProvider’ы, UserManager’ы и другие сущности с гораздо более сложной логикой.
Многие разработчики начинают жаловаться, дескать «этот ваш тестируемый дизайн» нарушает инкапсуляцию, открывает слишком много. Я думаю, что существует только две причины, когда это может вас беспокоить:
Это значит, что у вас серьезная криптография, бинарники упакованы, и все обвешано сертификатами.
Даже если так, скорее всего, вы сможете найти компромиссное решение. Например, в .NET вы можете использовать internal-методы и атрибут [InternalsVisibleTo] [5], чтобы дать доступ к тестируемым методам из ваших тестовых сборок.
Существует ряд задач, когда архитектурой приходится жертвовать в угоду производительности, и для кого-то это становится поводом отказаться от тестирования. В моей практике докинуть сервер/проапгрейдить железо всегда было дешевле, чем писать нетестируемый код. Если у вас есть критический участок, вероятно, стоит переписать его на более низком уровне. Ваше приложение на C#? Возможно, есть смысл собрать одну неуправляемую сборку на С++.
Вот несколько принципов, которые помогают писать тестируемый код:
Под «унаследованным» мы будем понимать код без тестов. Качество такого кода может быть разным. Несколько советов, как можно покрыть его тестами.
Нам повезло, прямых созданий классов и мясорубки нет, а принципы SOLID соблюдаются. Нет ничего проще – создаем тестовые проекты, и шаг за шагом покрываем приложение, используя принципы, описанные в статье. В крайнем случае, нам придется добавить пару сеттеров для фабрик и выделить несколько интерфейсов.
У нас есть жесткие связи, костыли и прочие радости жизни. Нам предстоит рефакторинг. Как правильно проводить комплексный рефакторинг – тема, выходящая далеко за рамки этой статьи.
Стоит выделить основное правило. Если вы не меняете интерфейсов – все просто, методика идентична. А вот если вы задумали большие перемены, следует составить граф зависимостей и разбить ваш код на отдельные более мелкие подсистемы (надеюсь, что это возможно). В идеале должно получиться примерно так: ядро, модуль #1, модуль #2 и т.д.
После этого выберите жертву. Только не начинайте с ядра. Возьмите сначала что-то поменьше: то, что вы способны отрефакторить за разумное время. Покрывайте эту подсистему интеграционными и/или приемочными тестами. А когда закончите, сможете покрыть эту часть юнит-тестами. Рано или поздно, шаг за шагом, вы должны преуспеть.
Будьте готовы, что сделать это быстро скорее всего не получится. Вам придется проявить волевые качества.
Не относитесь к своим тестам как к второсортному коду. Многие начинающие разработчики ошибочно полагают, что DRY, KISS и все остальное – это для продакшна. А в тестах допустимо все. Это не верно. Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения. Все принципы, применямые в разработке продакшн-кода могут и должны применяться при написании тестов.
Есть всего три причины, почему тест перестал проходить:
Уделяйте внимание поддержке ваших тестов, чините их вовремя, удаляйте дубликаты, выделяйте базовые классы и развивайте API тестов. Можно завести шаблонные базовые тестовые классы, которые обязывают реализовать набор тестов (например CRUD). Если делать это регулярно, то вскоре это не будет занимать много времени.
Для измерения успешности внедрения юнит-тестов в вашем проекте следует использовать две метрики:
Первая показывает, есть ли у наших действий результат, или мы впустую расходуем время, которое могли бы потратить на фичи. Вторая – как много нам еще предстоит сделать.
Наиболее популярные тулзы для измерения покрытия кода на .NET платформе это:
Я умышленно не касался этой темы до самого конца. С моей точки зрения Test First – хорошая практика, обладающая рядом неоспоримых преимуществ. Однако, по тем или иным причинам, иногда я отступаю от этого правила и пишу тесты после того, как готов код.
На мой взгляд, «как писать тесты» гораздо важнее, чем «когда это делать». Делайте, как вам удобно, но не забывайте: если вы начинаете с тестов, то получаете архитектуру «в придачу». Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.
Отличную подборку ссылок и книг по теме можно найти в этой статье на Хабре [6]. Особенно рекомендую книгу The Art of Unit Testing. Я читал первое издание. Оказывается, вышло уже и второе.
Автор: marshinov
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/27113
Ссылки в тексте:
[1] Дарт Автотестиус: http://habrahabr.ru/company/scrumtrek/blog/168485/
[2] Moq: http://code.google.com/p/moq/
[3] Rhino Mocks: http://www.hibernatingrhinos.com/oss/rhino-mocks
[4] тут: http://code.google.com/p/mocking-frameworks-compare/
[5] [InternalsVisibleTo]: http://msdn.microsoft.com/ru-ru/library/system.runtime.compilerservices.internalsvisibletoattribute.aspx
[6] этой статье на Хабре: http://habrahabr.ru/post/136049/
[7] Источник: http://habrahabr.ru/post/169381/
Нажмите здесь для печати.