14 февраля 2013 в 12:23

Юнит-тестирование для чайников tutorial

Даже если вы никогда в жизни не думали, что занимаетесь тестированием, вы это делаете. Вы собираете свое приложение, нажимаете кнопку и проверяете, соответствует ли полученный результат вашим ожиданиям. Достаточно часто в приложении можно встретить формочки с кнопкой “Test it” или классы с названием TestController или MyServiceTestClient.



То что вы делаете, называется интеграционным тестированием. Современные приложения достаточно сложны и содержат множество зависимостей. Интеграционное тестирование проверяет, что несколько компонентов системы работают вместе правильно.

Оно выполняет свою задачу, но сложно для автоматизации. Как правило, тесты требуют, чтобы вся или почти вся система была развернута и сконфигурирована на машине, на которой они выполняются. Предположим, что вы разрабатываете web-приложение с UI и веб-сервисами. Минимальная комплектация, которая вам потребуется: браузер, веб-сервер, правильно настроенные веб-сервисы и база данных. На практике все еще сложнее. Разворачивать всё это на билд-сервере и всех машинах разработчиков?

We need to go deeper



Давайте сначала спустимся на предыдущий уровень и убедимся, что наши компоненты работают правильно по-отдельности.

Обратимся к википедии:
Модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.

Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.




Таким образом, юнит-тестирование – это первый бастион на борьбе с багами. За ним еще интеграционное, приемочное и, наконец, ручное тестирование, в том числе «свободный поиск».

Нужно ли все это вам? С моей точки зрения ответ: «не всегда».

Не нужно писать тесты, если


  • Вы делаете простой сайт-визитку из 5 статических html-страниц и с одной формой отправки письма. На этом заказчик, скорее всего, успокоится, ничего большего ему не нужно. Здесь нет никакой особенной логики, быстрее просто все проверить «руками»
  • Вы занимаетесь рекламным сайтом/простыми флеш-играми или баннерами – сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление
  • Вы делаете проект для выставки. Срок – от двух недель до месяца, ваша система – комбинация железа и софта, в начале проекта не до конца известно, что именно должно получиться в конце. Софт будет работать 1-2 дня на выставке
  • Вы всегда пишете код без ошибок, обладаете идеальной памятью и даром предвидения. Ваш код настолько крут, что изменяет себя сам, вслед за требованиями клиента. Иногда код объясняет клиенту, что его требования — гов не нужно реализовывать


В первых трех случаях по объективным причинам (сжатые сроки, бюджеты, размытые цели или очень простые требования) вы не получите выигрыша от написания тестов.

Последний случай рассмотрим отдельно. Я знаю только одного такого человека, и если вы не узнали себя на фото ниже, то у меня для вас плохие новости.



Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля



В своей практике я много раз встречался с проектами старше года. Они делятся на три категории:
  • Без покрытия тестами. Обычно такие системы сопровождаются спагетти-кодом и уволившимися ведущими разработчиками. Никто в компании не знает, как именно все это работает. Да и что оно в конечном итоге должно делать, сотрудники представляют весьма отдаленно.
  • С тестами, которые никто не запускает и не поддерживает. Тесты в системе есть, но что они тестируют, и какой от них ожидается результат, неизвестно. Ситуация уже лучше. Присутствует какая-никакая архитектура, есть понимание, что такое слабая связанность. Можно отыскать некоторые документы. Скорее всего, в компании еще работает главный разработчик системы, который держит в голове особенности и хитросплетения кода.
  • С серьезным покрытием. Все тесты проходят. Если тесты в проекте действительно запускаются, то их много. Гораздо больше, чем в системах из предыдущей группы. И теперь каждый из них – атомарный: один тест проверяет только одну вещь. Тест является спецификацией метода класса, контрактом: какие входные параметры ожидает этот метод, и что остальные компоненты системы ждут от него на выходе. Таких систем гораздо меньше. В них присутствует актуальная спецификация. Текста немного: обычно пара страниц, с описанием основных фич, схем серверов и getting started guide’ом. В этом случае проект не зависит от людей. Разработчики могут приходить и уходить. Система надежно протестирована и сама рассказывает о себе путем тестов.


Проекты первого типа – крепкий орешек, с ними работать тяжелее всего. Обычно их рефакторинг по стоимости равен или превышает переписывание с нуля.

Почему есть проекты второго типа?

Коллеги из ScrumTrek уверяют, что всему виной темная сторона кода и властелин Дарт Автотестиус. Я убежден, что это очень близко к правде. Бездумное написание тестов не только не помогает, но вредит проекту. Если раньше у вас был один некачественный продукт, то написав тесты, не разобравшись в этой теме, вы получите два. И удвоенное время на сопровождение и поддержку.

Для того чтобы темная сторона кода не взяла верх, нужно придерживаться следующих основных правил.
Ваши тесты должны:

  • Быть достоверными
  • Не зависеть от окружения, на котором они выполняются
  • Легко поддерживаться
  • Легко читаться и быть простыми для понимания (даже новый разработчик должен понять что именно тестируется)
  • Соблюдать единую конвенцию именования
  • Запускаться регулярно в автоматическом режиме

Чтобы достичь выполнения этих пунктов, нужны терпение и воля. Но давайте по порядку.

Выберите логическое расположение тестов в вашей VCS

Только так. Ваши тесты должны быть частью контроля версий. В зависимости от типа вашего решения, они могут быть организованы по-разному. Общая рекомендация: если приложение монолитное, положите все тесты в папку Tests; если у вас много разных компонентов, храните тесты в папке каждого компонента.

Выберите способ именования проектов с тестами

Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.
У вас есть части системы <PROJECT_NAME>.Core, <PROJECT_NAME>.Bl и <PROJECT_NAME>.Web? Добавьте еще <PROJECT_NAME>.Core.Tests, <PROJECT_NAME>.Bl.Tests и <PROJECT_NAME>.Web.Tests.

У такого способа именования есть дополнительный сайд-эффект. Вы сможете использовать паттерн *.Tests.dll для запуска тестов на билд-сервере.

Используйте такой же способ именования для тестовых классов

У вас есть класс ProblemResolver? Добавьте в тестовый проект ProblemResolverTests. Каждый тестирующий класс должен тестировать только одну сущность. Иначе вы очень быстро скатитесь в унылое го во второй тип проектов (с тестами, которые никто не запускает).

Выберите «говорящий» способ именования методов тестирующих классов

TestLogin – не самое лучшее название метода. Что именно тестируется? Каковы входные параметры? Могут ли возникать ошибки и исключительные ситуации?

На мой взгляд, лучший способ именования методов такой: [Тестируемый метод]_[Сценарий]_[Ожидаемое поведение].
Предположим, что у нас есть класс Calculator, а у него есть метод Sum, который (привет, Кэп!) должен складывать два числа.
В этом случае наш тестирующий класс будет выглядеть так:

сlass CalculatorTests
{
        public void Sum_2Plus5_7Returned()
        {
 	    // …
        }
}

Такая запись понятна без объяснений. Это спецификация к вашему коду.

Выберите тестовый фреймворк, который подходит вам

Вне зависимости от платформы не стоит писать велосипеды. Я видел много проектов, в которых автоматические тесты (в основном, не юнит, а приемочные) запускались из консольного приложения. Не надо этого делать, все уже сделано за вас.

Уделите чуть больше внимания обзору фреймворков. Например, многие .NET разработчики используют MsTest только потому, что он входит в поставку студии. Мне гораздо больше по душе NUnit. Он не создает лишних папок с результатами тестов и имеет поддержку параметризированного тестирования. Я могу так же легко запускать мои тесты на NUnit с помощью Решарпера. Кому-то понравится элегантность xUnit’а: конструктор вместо атрибутов инициализации, реализация IDisposable как TearDown.

Что тестировать, а что – нет?

Одни говорят о необходимости покрытия кода на 100%, другие считают это лишней тратой ресурсов.
Мне нравится такой подход: расчертите лист бумаги по оси X и Y, где X – алгоритмическая сложность, а Y – количество зависимостей. Ваш код можно разделить на 4 группы.


Рассмотрим сначала экстремальные случаи: простой код без зависимостей и сложный код с большим количеством зависимостей.

  1. Простой код без зависимостей. Скорее всего здесь и так все ясно. Его можно не тестировать.
  2. Сложный код с большим количеством зависимостей. Хм, если у вас есть такой код, тут пахнет God Object’ом и сильной связностью. Скорее всего, неплохо будет провести рефакторинг. Мы не станем покрывать этот код юнит-тестами, потому что перепишем его, а значит, у нас изменятся сигнатуры методов и появятся новые классы. Так зачем писать тесты, которые придется выбросить? Хочу оговориться, что для проведения такого рода рефакторинга нам все же нужно тестирование, но лучше воспользоваться более высокоуровневыми приемочными тестами. Мы рассмотрим этот случай отдельно.

Что у нас остается:
  1. Cложный код без зависимостей. Это некие алгоритмы или бизнес-логика. Отлично, это важные части системы, тестируем их.
  2. Не очень сложный код с зависимостями. Этот код связывает между собой разные компоненты. Тесты важны, чтобы уточнить, как именно должно происходить взаимодействие. Причина потери Mars Climate Orbiter 23 сентября 1999 года заключалась в программно-человеческой ошибке: одно подразделение проекта считало «в дюймах», а другое – «в метрах», и прояснили это уже после потери аппарата. Результат мог быть другим, если бы команды протестировали «швы» приложения.


Придерживайтесь единого стиля написания тела теста

Отлично зарекомендовал себя подход 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);
        }
}


Теперь у контроллера появилась новая точка входа, и мы можем передать туда другие реализации интерфейсов.

Fakes: stubs & mocks

Мы переписали класс и теперь можем подсунуть контроллеру другие реализации зависимостей, которые не станут лезть в базу, смотреть конфиги и т.д. Словом, будут делать только то, что от них требуется. Разделяем и властвуем. Настоящие реализации мы должны протестировать отдельно в своих собственных тестовых классах. Сейчас мы тестируем только контроллер.

Выделяют два типа подделок: стабы (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 для создания моков и стабов. Довольно распространен фреймворк Rhino Mocks. Оба фреймворка — бесплатные. На мой взгляд, они практически эквивалентны, но Moq субъективно удобнее.

На рынке есть также два коммерческих фреймворка: TypeMock Isolator и Microsoft Moles. На мой взгляд они обладают чрезмерными возможностями подменять невиртуальные и статические методы. Хотя при работе с унаследованным кодом это и может быть полезно, ниже я опишу, почему все-таки не советую заниматься подобными вещами.

Шоукейсы перечисленных изоляционных фреймворков можно посмотреть тут. А информацию по техническим аспектам работы с ними легко найти на Хабре.

Тестируемая архитектура

Вернемся к примеру с контроллером.

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’ы и другие сущности с гораздо более сложной логикой.

Тестируемая архитектура VS OOP

Многие разработчики начинают жаловаться, дескать «этот ваш тестируемый дизайн» нарушает инкапсуляцию, открывает слишком много. Я думаю, что существует только две причины, когда это может вас беспокоить:

Серьезные требования к безопасности
Это значит, что у вас серьезная криптография, бинарники упакованы, и все обвешано сертификатами.
Даже если так, скорее всего, вы сможете найти компромиссное решение. Например, в .NET вы можете использовать internal-методы и атрибут [InternalsVisibleTo], чтобы дать доступ к тестируемым методам из ваших тестовых сборок.

Производительность
Существует ряд задач, когда архитектурой приходится жертвовать в угоду производительности, и для кого-то это становится поводом отказаться от тестирования. В моей практике докинуть сервер/проапгрейдить железо всегда было дешевле, чем писать нетестируемый код. Если у вас есть критический участок, вероятно, стоит переписать его на более низком уровне. Ваше приложение на C#? Возможно, есть смысл собрать одну неуправляемую сборку на С++.

Вот несколько принципов, которые помогают писать тестируемый код:
  • Мыслите интерфейсами, а не классами, тогда вы всегда сможете легко подменять настоящие реализации подделками в тестовом коде
  • Избегайте прямого инстанцирования объектов внутри методов с логикой. Используйте фабрики или dependency injection. В этом случае использование IOC-контейнера в проекте может сильно упростить вам работу.
  • Избегайте прямого вызова статических методов
  • Избегайте конструкторов, которые содержат логику: вам сложно будет это протестировать.


Работа с унаследованным кодом

Под «унаследованным» мы будем понимать код без тестов. Качество такого кода может быть разным. Несколько советов, как можно покрыть его тестами.

Архитектура тестируема

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

Архитектура не тестируема

У нас есть жесткие связи, костыли и прочие радости жизни. Нам предстоит рефакторинг. Как правильно проводить комплексный рефакторинг – тема, выходящая далеко за рамки этой статьи.
Стоит выделить основное правило. Если вы не меняете интерфейсов – все просто, методика идентична. А вот если вы задумали большие перемены, следует составить граф зависимостей и разбить ваш код на отдельные более мелкие подсистемы (надеюсь, что это возможно). В идеале должно получиться примерно так: ядро, модуль #1, модуль #2 и т.д.
После этого выберите жертву. Только не начинайте с ядра. Возьмите сначала что-то поменьше: то, что вы способны отрефакторить за разумное время. Покрывайте эту подсистему интеграционными и/или приемочными тестами. А когда закончите, сможете покрыть эту часть юнит-тестами. Рано или поздно, шаг за шагом, вы должны преуспеть.
Будьте готовы, что сделать это быстро скорее всего не получится. Вам придется проявить волевые качества.

Поддержка тестов




Не относитесь к своим тестам как к второсортному коду. Многие начинающие разработчики ошибочно полагают, что DRY, KISS и все остальное – это для продакшна. А в тестах допустимо все. Это не верно. Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения. Все принципы, применямые в разработке продакшн-кода могут и должны применяться при написании тестов.
Есть всего три причины, почему тест перестал проходить:

  1. Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
  2. Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно). Возможно, что раньше он проходил ошибочно. Разберитесь и почините тест.
  3. Смена требований. Если требования изменились слишком сильно – тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.


Уделяйте внимание поддержке ваших тестов, чините их вовремя, удаляйте дубликаты, выделяйте базовые классы и развивайте API тестов. Можно завести шаблонные базовые тестовые классы, которые обязывают реализовать набор тестов (например CRUD). Если делать это регулярно, то вскоре это не будет занимать много времени.

Как «измерить» прогресс

Для измерения успешности внедрения юнит-тестов в вашем проекте следует использовать две метрики:

  1. Количество багов в новых релизах (в т.ч. и регрессии)
  2. Покрытие кода


Первая показывает, есть ли у наших действий результат, или мы впустую расходуем время, которое могли бы потратить на фичи. Вторая – как много нам еще предстоит сделать.

Наиболее популярные тулзы для измерения покрытия кода на .NET платформе это:
  • NCover
  • dotTrace
  • встроенный в студию Test Coverage


Test First?




Я умышленно не касался этой темы до самого конца. С моей точки зрения Test First – хорошая практика, обладающая рядом неоспоримых преимуществ. Однако, по тем или иным причинам, иногда я отступаю от этого правила и пишу тесты после того, как готов код.

На мой взгляд, «как писать тесты» гораздо важнее, чем «когда это делать». Делайте, как вам удобно, но не забывайте: если вы начинаете с тестов, то получаете архитектуру «в придачу». Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.

Почитать на тему

Отличную подборку ссылок и книг по теме можно найти в этой статье на Хабре. Особенно рекомендую книгу The Art of Unit Testing. Я читал первое издание. Оказывается, вышло уже и второе.
Максим Аршинов @marshinov
карма
84,0
рейтинг 5,6
Управление разработкой бизнес-приложений
Самое читаемое Разработка

Комментарии (60)

  • +1
    Хорошая статья, положу в закладки!
    у меня вопрос по существу, я совсем ничего не понимаю в юнит-тестах, поскажите пожалуйста, что следовало бы тестировать и как, в приложении для учёта расходов
    у него, собственно, две функции
    добавление в базу расхода
    отображение из базы расходов
    • +1
      Вообще, статья как раз о том, что и как тестировать. Вы точно дочитали до конца? Исходя из вашего коммента (не очень много информации) я предложил бы вам точно протестировать добавление и отображение «расходов» ;)
      • 0
        протестировать добавление и отображение «расходов» ;)

        до этого я сам дотумкал, спасибо =)
        другой вопрос, как тестировать?
        делать добавление и сразу чтение, чтобы проверить, что добавилось именно то, что я добавлял в базу?
        • +1
          нет, вам нужно отдельно тестировать слои приложения, отвечающие за Data Layer, а в другие классы передать фейки ваших Data Layer — объектов. Правило простое: один класс — один тестирующий класс. Если вы будете сразу тестировать с БД — это несколько слоев приложение. Это интеграционный, а не юнит-тест.
      • 0
        каждый метод имеющий сколь бы то ни было сложную логику. Геттеры и сеттеры например покрывать не нужно — хотя некоторые товарищи пишут тесты и для них, ради красивой цифры в coverage report =)
        • 0
          Сеттеры и геттеры покрывать вообще вредно.
    • 0
      и я свою домашнюю бухгалтерию на Asp.Net'е пишу. Ой.
  • 0
    Вот бы это стало серией статей. С указанием и подробным описанием инструментов тестирования. Такую Хабранеделю я бы лучше запомнил, чем GTD недели и недели статей про руководителей.
    • +1
      Я бы тоже почитал про создание тестов, т.к свсем начинающий разработчик и хочется изначально прививать себе правильные рефлексы
  • 0
    Шоукейсы перечисленных изоляционных фреймворков можно посмотреть тут.

    У кого-нибудь удалось запустить данные шоукейсы?
  • +1
    И вот как всегда в статье о Unit-тестировании примеры тестирования методов add и multiply. Уснул где-то в том месте, когда от первого перешли ко второму. Дальше пошли всякие «умные» вещи, которые хорошо ложаться в идеологию юнит-тестинга.

    Намного интереснее почитать о Unit-тестировании анимационных приложений (скажем, игр) или методов, которые реагируют на действия пользователя. Скажем, что Draggable объекта работает корректно. Или что плавное изменение координаты шарика по синусоиде работает корректно.

    Почему во всех статьях о юнит-тестах в качестве примеров идёт такая легкотня?

    И да.
    // arrange
    var calc = new Calculator();
        
    // act
    var res = calc.Sum(2,5);
    
    // assert
    Assert.AreEqual(7, res);
    


    Такая форма записи гораздо легче читается, чем

    
     Assert.AreEqual( 7, new Calculator().sum(2,5) );
    
    


    Нифига, имхо как раз нижний код читается намного легче
    • 0
      Калькулятор — де факто, — уже промышленный стандарт примеров тестирования. Я в этих местах писал о форме записи и способах декомпозиции. Пример умышленно упрощен, чтобы акцентировать не тестируемом методе, а на способах решения выше обозначенных проблем.
      • +1
        Калькулятор — де факто, — уже промышленный стандарт примеров тестирования

        Ну я про это и говорю. Hello-world прям. Вот только толку от этого примера — ноль. Красиво только выглядит.
    • 0
      Про форму записи — дело не в чтение — курите рефакторинг «излишний ввод локальных переменных», хотя понятно, что не надо доводить до идиотизма (т.е. функционального программирования).
  • –2
    А я себя все-таки узнал на фото :)
    • +2
      Простите, Чак, мы тут так, случайно заглянули. :)

      Вспомнилось: “It works on my machine” always holds true for Chuck Norris.
  • 0
    Спасибо, вы очень доходчиво пишете. Хотелось бы увидеть от вас статьи и об автоматизации тестирования UI, если вы это используете.
    • 0
      Следующая статья будет об этом. Ориентировочно, недели через две.
    • 0
      habrahabr.ru/post/178407/ — вот и статья
  • 0
    >>Вернемся к примеру с калькулятором
    А что, до этого в тексте уже был код с калькулятором? Не видно!
    • 0
      Спасибо, поправил.
      • –1
        >>Каждый тест должен проверять только одну вещь
        Более точная формулировка будет такой «Одна концепция на тест». А то не совсем понятно что такое «вещь»?
    • 0
      Был потерян кусок статьи, добавил недостающий текст.
  • –2
    > Есть всего три причины, почему тест перестал проходить

    :) Хотя бы честно, теперь при тестировании мне нужно ловить не только одну причину (собственно баги), а еще и баги теста, и необходимость поддержки… Ну, минусы видны невооруженным взглядом — когда плюсы начнутся?
  • –3
    > Тесты – такой-же код. Разница только в том, что у тестов другая цель – обеспечить качество вашего приложения.

    Ага, и типа без тестов мы уже качественно ПО писать не умеем — спасибо, умиляет :)
  • –3
    > Если вы сначала пишете код, вам возможно, придется его менять, чтобы сделать тестируемым.

    Боже, что за глупость… мне еще и архитектуру надо затачивать не под качество, а под «тестируемость» — а это разные задачи.
    • –1
      Одна и та же.
  • –3
    > Уделяйте внимание поддержке ваших тестов, чините их вовремя, удаляйте дубликаты, выделяйте базовые классы и развивайте API тестов.

    ага, а программировать когда?
    • –1
      В освободившееся от исправления багов время.
      • –1
        Ага, а должно быть наоборот… баги не кончаются никогда
        • 0
          Я имел в виду нечто иное. При правильном подходе к написанию автоматизированных тестов количество багов в каждом новом релизе должно снижаться. Раз вы меньше тратите время на исправление багов, у вас больше времени на новые фичи. Все просто.
          Немаловажный момент то, что тесты позволяют выловить баг, порой, до попадания кода в VCS. «Цена» исправления, в таком случае, гораздо ниже, чем при хот-патчах на продакшне с последующими мержами в основную ветку разработки.
          Баги не кончатся, но наша задача не искоренить все баги. Задача — минимизировать количество ошибок и регрессии в основных юз-кейсай нашего приложения. Это вполне посильная задача.
          • –1
            Ну, как ДОЛЖНО снижаться, и как происходит на самом деле — РАЗНЫЕ ВЕЩИ.
            • 0
              На моем опыте так и происходит. Результаты появляются с третьего-четвертого спринта. Иначе бы я не писал этой статьи.
              • –1
                Это спорное утверждение, вам может так казаться, потому что вы вложили не мало затрат на модульное тестирование. В то время как после выпуска определенной прикладной части — есть просто уровень распределения ошибок в виде «горба» — т.е. ровно так же и при ручном тестировании ошибки в готовом ПО практически исчезают… а тесты остаются невостребованными далее…
              • –1
                Впрочем замедти — что я не возражаю против автоматизации интеграционных тестов — но вот почему то про это никто не пишет, а именно это я считаю наиболее важным и не против был бы это начать эксплуатировать…
  • –3
    > «этот ваш тестируемый дизайн» нарушает инкапсуляцию

    Конечно нарушает, а ваши две причинки совсе не о том. Инкапсуляция нужна для того, чтобы другие программисты не вызывали бы то, что не нужно! Для того, чтобы они писали код в соответствии с публичным интерфейсом класса, а не делали ли бы все с черного хода… А тут вы сами портите качество кода ради «тестируемости», а еще каком то качестве кода во время приведения к тестируемости говорите… смешно.
    • 0
      Вы, видимо, не читали статьи. Сеттеры можно оставить в реализации, а работать по интерфейсу, в котором сеттера не будет, зависимости можно внедрить через конструктор. Если ваши объекты инстанцируются через фабрику или IOC-контейнер до конструктора вы тоже не дотянитесь.

      Черный ход здесь не при чем. Качество кода ради тестируемости на падает, а улучшается, т.к. тестируемый код подразуевает слабую связь компонентов.
      • –1
        Знаете если тестирование хоть как то влияет на архитектуру — значит это плохое тестирование. Задача тестирования одна — найти максимальное число ошибок. а все остальное от лукавого… когда вы ради тестирования вводите интерфейсы или изменения в конструкторах — то вы уже изменяете код, а этого не должно быть.
  • –3
    > Архитектура не тестируема
    > У нас есть жесткие связи, костыли и прочие радости жизни.

    В огороде бузина, а в Киеве дядька! Ну, с какого перепугу вы все это смешали вместе?
  • –3
    > Каждый тест должен проверять только одну вещь

    Ну кто же вас учит таким вещам? Любая теория тестирования начинается со слов «невозможно выявит все ошибки, невозможно проверить, что программа работает правильно » — а вы предлагаете написать еще по тесту на одну вещи.

    Если и есть смысл в автоматическом тестировании — то только для сложных интеграционных тестов.
  • –3
    > Как «измерить» прогресс

    А вот попробуйте лучше измерить так — время потраченная на всю эту лабуду с созданием юнит-тестов разделите на время ручного тестирования при отсутствии этого… а еще лучше разделите зарплату программиста на зарплату тестировщика
    • 0
      Хорошие QA стоят примерно столько-же, сколько хорошие программисты. В гугле существует должность software developer in test
      • –1
        А хорошие тестировщики тут не нужны!
  • –3
    И наконец, возвращаясь к Чаку — господа, вы просто забыли принципы хорошего структурного программирования, все это юнит тестирование более качественно заменяется тем, что функция должна содержать проверку граничных условий для входов и выходов функции… и тогда действительно код объясняет клиенту, что его требования — при введенной информации нельзя выполнить, это еще называется дружественный интерфейс (кто в танке), таким образом, пишите самотестирущиеся программы и проверяйте граничные условия — и не тратьте время на лабуду.
    • 0
      Если функция содержит ошибку, то что вам даст проверка входных параметров и как вы собираетесь проверять выход функции, вызовом той же функции с теми же параметрами?
      А юнит-тестирование как раз проверяет корректность работы функции с заданными параметрами, сравнивая ее результат с заранее известным результатом для этих параметров.
      • –2
        Т.е. как делать проверку в математике начальной школы не проходили?, для сложения x = y + z, надо перепроверить y = x — z и etc. В качественно написанном коде, такого рода проверки есть сами по себе.
        • 0
          Мда, думаю, продолжать бессмысленно…
          • –1
            Бессмысленно делать модульное тестирование, когда в нем нет никакой необходимости. А вы собственно похоже не понимаете, что на таком анализе ограничений построенно все функциональное программирование… только там это излишнее «парадно» представлено (и сделано через ж..), а суть как раз именно в том, что я написал выше…
        • +1
          Статья интересная и полезная, правда, она явно написана не для чайников, в ней много недомолвок (в частности, не описан принцип именования, хотя, например мне, очевиден: что-тестируем_что-делаем_ожидаемый-результат, но, думаю, это должно явно указываться; в более сложных примерах не описаны интерфейсы, не показана реализация методов, в целом выглядят перегруженно и выдернутыми из контекста, может просто я излишне придираюсь, хотя, пример с умножением хороший), но, самое главное, после ее прочтения чайник так и не узнает как создавать тесты. Не хватает пошаговых инструкций как начать писать тесты (установить то-то, создать то-то, написать то-то, запустить, исправить, повторить, etc..).
        • 0
          Ой, это я не вам, пардоньте! Промазал)
    • +1
      Чак, я же написал, что к вам эта статья не относится.
      • 0
        Ну, я тоже так — мимо проходил :)
  • +1
    Сейчас как раз изучаю модульное тестирование, и возникает следующий вопрос.
    Для тестирования модульного нам необходимо разбить программный код на части и предоставить каждой части поддельные внешние зависимости, иначе это будет уже тестирование интеграционное. Есть ли какие-либо общие рекомендации, что считать в данном случае модулем, а что внешней зависимостью? Модуль здесь — это обязательно класс или может быть компонент системы, состоящий из нескольких связанных классов? Внешние зависимости — это только БД, сеть, файловая система или связи между классами, компонентами?
    Вопрос связан с тем, что большая часть проблем возникает именно на связи классов.
    • 0
      Страустрап в своей книге о C++ предлагает следующий подход к ООП в целом: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматом класс будет модулем вашего приложения.

      Внешняя зависимость — это все, что делает ваши тесты не правдивыми и сложно-поддерживаем. Файловая система — зависимость: структура каталогов может быть другой на другой машине. БД — зависимость, ее может не быть на другой машине. Веб-сервис — зависимость: может не быть интернета или может быть злобный фаервол, а сервис, вообще может взять и упасть, скажем, от Хабра-эффекта.

      Спросите себя: «будет ли этот компонент вести себя так же на другой машине?». Если ответ «нет»: нужно его подменить. Если ответ «да» — оставьте его.

      Некоторые разработчики начинают увлекаться подменой сущностей и приходят к тому, что подменяют вообще все. Они перестают тестировать приложение и начинают тестировать свои стабы, моки. Это в корне не верно. Если «живых» реализаций в тесте нет, то этот тест не тестирует ничего.

      Существует перегиб в обратную сторону. В readme NHibernate'а, не знаю, как сейчас, в прошлых релизах был пункт «пожалуйста, не тестируйте NHibernate, у нас есть свои тесты. Тестируйте вашу бизнес-логику».
  • +1
    >>Простой код без зависимостей. Скорее всего здесь и так все ясно. Его можно не тестировать.
    Хм, практика показывает, что со временем простой код становится сложным :). А написание тестов постфактом и выборочно приводит к тому, что тесты существуют только для покрытия (тоесть применяется только вторая метрика — 100% покрытие есть — мы счастливы. То что колличество багов не стало меньше — никого не волнует).
    • 0
      Это зависит от того вашего стиля работы. Под очень простым кодом я понимаю гетеры, сетеры, экшны контроллеров вида return View(). Если во время делать рефакторинг, простой код остается простым. Если по каким-то причинам он становится сложнее, вы же можете сразу написать тест на этот «усложнившийся» участок.

      Я думаю, что вопрос покрытия следует решать индивидуально каждой команде. Нужно пробовать и выбирать то, что работает для вас, а не то «что доктор прописал». А про «красивую» цифру 100% — это вы абсолютно верно подметили.
      • 0
        >>Под очень простым кодом я понимаю гетеры, сетеры, экшны контроллеров вида return View()

        Я как раз и говорил о том, что такой код имеет тенденцию становится больше. Например — мы добавляем контроль формата в сеттер, кеш в геттер. И в этот момент мы и получаем ситуацию, когда тесты пишутся ПОСЛЕ написания кода.

        И тут получается ситуация, когда функционал сделан, а покрытие — нет. Если присутствует некое довление со стороны менеджмента, или просто много работы — велик соблазн написать тест для галочки, или не писать вовсе.
  • +1
    Статья интересная и полезная, правда, она явно написана не для чайников, в ней много недомолвок (в частности, не описан принцип именования, хотя, например мне, очевиден: что-тестируем_что-делаем_ожидаемый-результат, но, думаю, это должно явно указываться; в более сложных примерах не описаны интерфейсы, не показана реализация методов, в целом выглядят перегруженно и выдернутыми из контекста, может просто я излишне придираюсь, хотя, пример с умножением хороший), но, самое главное, после ее прочтения чайник так и не узнает как создавать тесты. Не хватает пошаговых инструкций как начать писать тесты (установить то-то, создать то-то, написать то-то, запустить, исправить, повторить, etc..).
    • 0
      Огромное вам спасибо. При публикации я «потерял» кусок статьи. Благодаря вам я это заметил и добавил недостающий текст.
  • 0
    По многочисленным просьбам будет продолжение этой статьи (практическая часть) с большим количеством примеров и пошаговыми инструкциями «как начать писать тесты».

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.