2017-02-21 52 views
14

Я реализую модульные тесты в финансовой системе, которая включает в себя несколько расчетов. Один из методов получает объект по параметру с более чем 100 свойствами и на основе свойств этого объекта вычисляет возврат. Для реализации модульных тестов для этого метода, Мне нужно, чтобы весь этот объект был заполнен действительными значениями.

Итак ... вопрос: сегодня этот объект заселен через базу данных. На моих модульных тестах (я использую NUnit) мне нужно было бы избежать базы данных и создать макет объекта, чтобы проверить только возврат метода. Как я могу эффективно протестировать этот метод с помощью этого огромного объекта? Нужно ли мне вручную заполнять все 100 его свойств? Есть ли способ автоматизировать создание этого объекта с помощью Moq (например)?

obs: Я пишу модульные тесты для уже созданной системы. В настоящий момент невозможно переписать всю архитектуру.
Спасибо, миллион!Как я могу выполнить модульные тесты в больших и сложных классах?

+4

Возможно, вы разместите свой вопрос на [softwareengineering.stackexchange.com] (http://softwareengineering.stackexchange.com/), я считаю, что это будет лучше. –

+5

У меня нет ответа, чтобы добавить к хорошим уже здесь, но просто для повышения - это «Дизайн» D в TDD. Когда что-то трудно проверить, это говорит вам, что что-то не так с дизайном кода. –

+1

@ VincentSavard, ссылаясь на другие сайты, часто бывает полезно указать, что [перекрестная публикация недоказана] (http://meta.stackexchange.com/tags/cross-posting/info) – gnat

ответ

11

Если эти 100 значений не имеют значения, и вам нужны только некоторые из них, у вас есть несколько вариантов.

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

var obj = new HugeObject(); 
obj.Foo = 42; 
obj.Bar = "banana"; 

Вы также можете использовать некоторые библиотеки как AutoFixture которая будет присваивал фиктивные значения для всех свойств в вашем объекте:

var fixture = new Fixture(); 
var obj = fixture.Create<HugeObject>(); 

можно назначить требуемые свойства вручную, или вы можете использовать арматуру строитель

var obj = fixture.Build<HugeObject>() 
        .With(o => o.Foo, 42) 
        .With(o => o.Bar, "banana") 
        .Create(); 

Другая полезная библиотека для тех же целей является NBuilder


Примечание: Если все свойства имеют отношение к функции, которая тестируется, и они должны иметь конкретные значения, то нет библиотеки, которая будет угадать значения необходимых для вашего теста. Единственный способ - указать значения теста вручную. Хотя вы можете устранить много работы, если вы настроите некоторые значения по умолчанию перед каждым тестом и просто измените то, что вам нужно для конкретного теста. То есть создать вспомогательный метод (ы), который будет создавать объект с определенным набором значений:

private HugeObject CreateValidInvoice() 
{ 
    return new HugeObject { 
     Foo = 42, 
     Bar = "banaba", 
     //... 
    }; 
} 

, а затем в тесте просто переопределить некоторые поля:

var obj = CreateValidInvoice(); 
obj.Bar = "apple"; 
// ... 
+0

Эй, Сергей. Спасибо за ответ на первое место. К сожалению, мне нужно, чтобы весь этот объект был заполнен для тестирования метода. Чтобы проверить результаты расчета метода, мне нужно иметь действительные записи для получения известного результата. Поэтому я не могу использовать случайные входы. Помогло бы мне NBuilder? –

+3

@ Lisa, если вам нужны точные значения, как любая библиотека угадает, какие значения вам нужны? Вы должны указать такие значения вручную –

+0

Это точно моя точка зрения. Интересно, является ли заполнение вручную более 100 свойств единственным способом заполнения этого объекта. Честно говоря, я даже искал какую-то библиотеку, которая могла бы получить населенный объект и создать для меня «свойство популяции». Аналогично тому, как вы запрашиваете какую-либо таблицу в sql для генерации сценариев ввода данных текущего содержимого. –

1

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

Если бы я был на вашем месте, я бы извлек фактическую логику и написал тесты в отношении этого нового класса «калькулятор». Сломайте все как можно больше. Если вход имеет 100 свойств, но не все они имеют значение для каждого расчета - используйте интерфейсы, чтобы разделить их. Это сделает видимым видимый ввод, улучшив код.

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

public class BigClass : ISet1 
    { 
     public string Prop1 { get; set; } 
     public string Prop2 { get; set; } 
     public string Prop3 { get; set; } 
    } 

    public interface ISet1 
    { 
     string Prop1 { get; set; } 

     string Prop2 { get; set; } 
    } 

    public interface ICalculator 
    { 
     CalculationResult Calculate(ISet1 input) 
    } 
+2

Спасибо, Стефан. Но мой вопрос заключается не в том, как создать объект, а в том, как его заполнять. На вашем примере мой класс BigClass имеет более 100 свойств, которые могут быть int, строками, списками и другими классами. Мой вопрос в том, как я могу оптимизировать создание этого объекта, имея в виду, что мне нужно, чтобы все это было заполнено известными значениями, чтобы генерировать достоверные результаты. –

+1

Я пытаюсь сказать, что вы не должны смотреть, как заполнить объект значениями, которые, очевидно, не заботятся о конкретном вычислении. Вместо этого вы должны попытаться «извлечь» логику, которую хотите протестировать. Если вы попытаетесь протестировать весь набор всех ваших начальных классов, тесты не будут поддерживаться, даже если вы вручную установите все 100 полей. Мое предложение состоит в том, чтобы извлечь «калькулятор» и убедиться, что он ожидает только того значения, которое ему нужно. В моем примере это ISet. В своем тесте вы создаете экземпляр BigClass и передаете его в ISet, задаете только те свойства, которые вам интересны. –

4

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

Потенциальных зелий

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

  2. Мок доступа к данным и иметь его импортировать необходимые данные с помощью альтернативного источника (плоский файл может быть? CSV)

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

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

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

4

Для случаев, когда мне приходилось получать большое количество фактических правильных данных для тестирования, я сериализовал данные в JSON и поместил их непосредственно в мои тестовые классы. Исходные данные могут быть взяты из вашей базы данных, а затем сериализованы. Что-то вроде этого:

[Test] 
public void MyTest() 
{ 
    // Arrange 
    var data = GetData(); 

    // Act 
    ... test your stuff 

    // Assert 
    .. verify your results 
} 


public MyBigViewModel GetData() 
{ 
    return JsonConvert.DeserializeObject<MyBigViewModel>(Data); 
} 

public const String Data = @" 
{ 
    'SelectedOcc': [29, 26, 27, 2, 1, 28], 
    'PossibleOcc': null, 
    'SelectedCat': [6, 2, 5, 7, 4, 1, 3, 8], 
    'PossibleCat': null, 
    'ModelName': 'c', 
    'ColumnsHeader': 'Header', 
    'RowsHeader': 'Rows' 
    // etc. etc. 
}"; 

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

Чтобы получить этот JSON, вам придется отдельно запросить базу данных для этого большого объекта, сериализовать его в JSON через JsonConvert.Serialise и записать эту строку в исходный код - этот бит относительно легко, но требуется некоторое время, потому что вам нужно сделать это вручную ... только один раз.

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

p.s. вам нужен Newtonsoft.Json пакет использовать JsonConvert.DeserializeObject

0

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

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

Мое предложение - использовать SQLite (или подобное) в модульном тестировании. Существуют инструменты для извлечения и дублирования фактической базы данных в базу данных SQLite, затем вы можете сгенерировать скрипты и загрузить их в версию базы данных в памяти. Вы можете использовать инъекцию зависимостей и шаблон репозитория, чтобы установить поставщика базы данных по-разному в своих «единичных» тестах, чем в реальном коде.

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

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

SQL Server в SQLite конвертер:

https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-to-SQLite-DB

SQLite студия: https://sqlitestudio.pl/index.rvt

(Используйте что для создания скриптов для в использовании памяти)

Для использования в памяти, выполните это:

TestConnection = новый SQLiteConnection ("FullUri = fi ? LE :: памяти: кэш = общий ");

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

Надеюсь, что это поможет, и удачи.

1

я бы такой подход:

1 - Написать тесты для каждой комбинации объекта входного параметра 100 свойств, используя инструмент, чтобы сделать это для вас (например, PEX, intellitest) и убедиться, что они все работают зеленый. На данный момент обратитесь к модульным тестам как к интеграционным тестам, а не к модульным тестам по причинам, которые станут очевидными позже.

2 - Рефакторинг тестов в SOLID-фрагменты кода - методы, которые не вызывают другие методы, могут считаться подлинными модулями, поскольку они не имеют зависимости от другого кода. Остальные методы по-прежнему проверяются только на уровне интеграции.

3 - Убедитесь, что все интеграционные тесты все еще работают зелеными.

4 - Создайте новые модульные тесты для нового тестируемого кода.

5 - Все, что работает зеленые, позволяет удалить все/некоторые из лишних оригинальных тестов интеграции - только вам, если вам удобно.

6 - Все зеленые вы можете начать уменьшать 100 свойств, необходимых в модульных тестах, только для тех, которые строго необходимы для каждого отдельного метода. Вероятно, это выделит области для дополнительного рефакторинга, но в любом случае упростит объект параметров.Это, в свою очередь, приведет к тому, что будущие помощники по работе с кодом хуже будут работать, и я бы поставил пари, что исторические неудачи в решении вопроса о размере объекта параметра, когда у него было 50 свойств, почему это сейчас 100. Неспособность решить проблему теперь будет означать, В конечном итоге мы вырасти до 150 параметров, что позволяет ему противостоять, никто не хочет.