Нужна помощь в определении того, какие юнит-тесты написать - PullRequest
5 голосов
/ 31 марта 2011

У меня есть следующий метод, и я собираюсь написать эффективные модульные тесты, которые также дадут мне хорошее покрытие путей кода:

public TheResponse DoSomething(TheRequest request)
{
    if (request == null)
        throw new ArgumentNullException("request");

    BeginRequest(request);

    try
    {
        var result = Service.DoTheWork(request.Data);

        var response = Mapper.Map<TheResult, TheResponse>(result);

        return response;
    }
    catch (Exception ex)
    {
        Logger.LogError("This method failed.", ex);

        throw;
    }
    finally
    {
        EndRequest();
    }
}

Объекты Service и Logger, используемые этим методом, внедряются в классконструктор (не показан).BeginRequest и EndRequest реализованы в базовом классе (не показан).А Mapper - это класс AutoMapper, используемый для отображения объектов на объекты.

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

Я сторонник принципа «один тест - одно утверждение» и использую Moq для фальсификации фреймворка в VS-Test (хотя я не зацикливался на этой части для этого обсуждения).Хотя некоторые из тестов (например, проверка передачи нулевых результатов в исключении) очевидны, я задаюсь вопросом, имеют ли смысл другие, которые приходят на ум, или нет;особенно когда они используют один и тот же код по-разному.

Ответы [ 5 ]

2 голосов
/ 01 апреля 2011

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

  • Проверка исключения нулевого аргумента
  • Mock Service и Logger, чтобы проверить, вызваны ли они с правильными данными
  • Stub Mapper (и, возможно, Service), чтобы проверить, действительно ли возвращены правильные результаты

Теперь трудная часть. В зависимости от того, есть ли у вас базовый класс, к которому у вас есть доступ (например, можете ли вы изменить его без особых проблем), вы можете попробовать подход под названием Extract & Override :

  1. Пометить BeginRequest / EndRequest как виртуальный в базовом классе
  2. Ничего не делать с ними в производном классе
  3. Введите новый, тестируемый класс, производный от класса, который вы хотите протестировать; переопределить методы из базового класса (BeginRequest / EndRequest), сделав их, например. изменить некоторое внутреннее значение, которое вы позже сможете легко проверить

Код может выглядеть примерно так:

Base 
{
    protected virtual void BeginRequest(TheRequest request) { ... }
    protected virtual void EndRequest() { ... }
}

Derived : Base // class you want to test
{
    // your regular implementation goes here
    // virtual methods remain the same
}

TestableDerived : Derived // class you'll actually test
{
    // here you could for example expose some properties 
    // determining whether Begin/EndRequest were actually called,
    // calls were made in correct order and so on - whatever makes
    // it easier to verify later

    protected override void BeginRequest(TheRequest request) { ... }
    protected override void EndRequest() { ... }  
}

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

Конечно, проблемы возникают, когда у вас нет доступа к базовому классу, и вы не можете изменить его код. К сожалению, у меня нет «нестандартного» решения для такой ситуации, но, возможно, вы могли бы создать виртуальную оболочку вокруг Begin / EndRequest в Derived и при этом использовать извлечение и переопределение TestableDerived.

1 голос
/ 01 апреля 2011

Вопрос помечен как TDD, но сам вопрос очень проверен, что является частью проблемы того, почему вам трудно совместить эти два вопроса.Если вы сделали TDD, может появиться другой, более тестируемый дизайн.Почему BeginRequest и EndRequest должны быть частью наследования.Могут ли они быть частью TransactionManager, который вводится?

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

Вы также можете внедрить какой-то альтернативный TransactionManager (предполагая, что это имеет смысл) только при тестировании, который делегирует, а в реальном коде просто вызывает сам себя.

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

0 голосов
/ 31 марта 2011

Это лишь небольшая часть большой статьи, которую я написал год или два назад.Моя компания использует это частично как стандарт для модульного тестирования.Надеюсь, это поможет.

Золотое правило модульного теста

«Для каждого открытого метода на бизнес-уровне приложения должен быть хотя бы 1 модульный тест.Для каждого класса в приложении должен быть хотя бы один тестовый класс ».

Элементы для модульного теста

В целом существует четыре типа методов, которыесвязаны с классами;

  • Модификаторы: Изменяет одно или несколько
    значений (или объектов), связанных с
    атрибутами объекта
  • Средства доступа: Возвращает значение (или объект), которое зависит от состояния объекта
  • Конструкторы: Вызывается один раз при создании объекта
  • Деструкторы: Вызывается при уничтожении объекта.

При написании модульных тестов основной задачей разработчика должны быть любые общедоступные методы, которые используются в их классе на бизнес-уровнесам (методы, которые вы будете использовать в своем веб-слое).Таким образом, разработчик также будет тестировать любые частные методы и любые открытые методы классов в DAL, которые используются классом, который тестируется модулем.

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

Как настроить / использовать проекты модульных тестов

В каждом решении должен быть проект модульного тестирования, который можно сохранить и использовать в любой архитектуре, используемой для обмена кодом.Делая это, и каждый разработчик работает над определенным проектом, обновляя и сохраняя свои модульные тесты, это позволит другим разработчикам (текущим и будущим) использовать одни и те же модульные тесты.Тщательно документируя каждый модульный тест относительно того, как он должен функционировать и при каких обстоятельствах, он создает документ, который позволяет разработчикам, которые являются новыми для приложения, ознакомиться со структурой и предполагаемым использованием каждого объекта.При создании юнит-тестов (Visual Studio) автоматически создаст тестовый класс в рамках тестового проекта для каждого из методов, которые выбирает пользователь.Это позволяет организовывать модульные тесты и отделять их от других модульных тестов других классов.

Как успешно выполнить модульный тест

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

• Модификаторы

o Вставки

o Обновления

o Удаление

o Полиморфы

• Аксессоры

o Единственное извлечение

o Групповое извлечение

o Массовое извлечение

Вставки, обновления, удаления и полиморф

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

Updates Обновления аналогичны вставкам, за исключением того, что вместо добавления новых данных они изменяют существующие данные.Как и вставки, они не ограничиваются SQL.

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

Полиморфы Проще говоря, полиморфизм - это характеристика способности присваивать различное значение или использование чему-либо в разных контекстах, в частности, чтобы объект, такой как переменная, функция или объект, мог иметь более одной формы.

Одиночное, групповое и массовое извлечение

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

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

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

Подтипы методов и модульное тестирование

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

1) Каждому экземпляру вставки, обновления или удаления должен предшествовать извлечение, а затем поиск, причем требуемый тип извлечения зависит от типа / формата изменяемых данных.

2) За каждым экземпляром вставки должно следовать удаление. Это гарантирует, что любая вставка данных будет удалена.

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

4) Хотя извлечения могут быть проверены во время испытания модификатора, они также должны быть проверены независимо.

5) Во время тестирования поиска нет необходимости проверять фактические данные, только те данные были возвращены.

0 голосов
/ 01 апреля 2011

Мне кажется, что тестирование

var result = Service.DoTheWork(request.Data);

для различных заклинаний запроса. Данные являются частью высокой стоимости. Остальное - инфраструктура, и я хотел бы абстрагироваться от нее до более чистого (см. Ниже очень грубого и готового подхода), более тестируемого стиля, чтобы справиться с очевидной ценностью Begin + End Request. проверяется отдельно.

Как вы говорите, нет необходимости проверять, что Mapper и логгер делают свое дело.

Последнее, что вам нужно, это множество хрупких испытаний, которые причиняют вам боль, когда вы решаете переделать внутренности ваших объектов

public TheResponse DoSomething(TheRequest request)
{
    Guard.NotNull( () => request );

    BeginRequest( (request) =>
    {
        var result = Service.DoTheWork(request.Data);

        var response = Mapper.Map<TheResult, TheResponse>(result);

       return response;
    });
}
// This is a base class method and can be tested elsewhere
void base::BeginRequest(Action<Request> execute)
{
    BeginRequest();
    try                 { execute(request);
                          return Mapper.Map<TheResult, TheResponse>(result); }
    catch(Exception ex) { Logger.Log(ex);
                          throw; }
    finally             { EndRequest(); }
}
0 голосов
/ 31 марта 2011

Я бы добавил утверждения, что BeginRequest и EndRequest вызываются, поскольку они, вероятно, очень важны.Кроме этого, вероятно, здесь нет ничего более важного.Вы не хотите тестировать вещи, которые на самом деле не так важны, как, например, регистрация ошибки.Большинство ваших тестов, вероятно, будут сосредоточены на том, что делает сервис.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...