Как я могу реализовать модульные тесты в больших и сложных классах?
Я выполняю модульные тесты в финансовой системе, которая включает в себя несколько расчетов. Один из методов получает объект по параметру с более чем 100 свойствами и на основе этих свойств объекта вычисляет возврат.
Чтобы реализовать модульные тесты для этого метода, мне нужно, чтобы весь этот объект был заполнен допустимыми значениями.
Итак... вопрос: сегодня этот объект заселен через базу данных. На моих модульных тестах (я использую NUnit) мне нужно было бы избежать базы данных и создать макет объекта, чтобы проверить только возврат метода. Как я могу эффективно протестировать этот метод с помощью этого огромного объекта? Нужно ли мне вручную заполнять все 100 его свойств?
Есть ли способ автоматизировать создание этого объекта с помощью Moq (например)?
obs: Я пишу модульные тесты для уже созданной системы. В настоящий момент невозможно переписать всю архитектуру.
Спасибо миллион!
Ответы
Ответ 1
Если эти 100 значений не релевантны, и вам нужны только некоторые из них, у вас есть несколько вариантов.
Вы можете создать новый объект (свойства будут инициализированы значениями по умолчанию, такими как null
для строк и 0
для целых чисел) и назначить только требуемые свойства:
var obj = new HugeObject();
obj.Foo = 42;
obj.Bar = "banana";
Вы также можете использовать некоторую библиотеку, например AutoFixture, которая присваивает фиктивные значения для всех свойств в вашем объекте:
var fixture = new Fixture();
var obj = fixture.Create<HugeObject>();
Вы можете назначить требуемые свойства вручную, или вы можете использовать конструктор fixture
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";
// ...
Ответ 2
Учитывая ограничения (плохой дизайн кода и технический долг... я ребенок) unit test будет очень громоздким для заполнения вручную. Гибкий интеграционный тест понадобится там, где вам придется ударить по фактическому источнику данных (а не к производству).
Потенциальные зелья
-
Сделайте копию базы данных и заполните только таблицы/данные, необходимые для заполнения зависимого сложного класса. Надеемся, что код достаточно модулогизирован, чтобы доступ к данным мог получить и заполнить сложный класс.
-
Откажитесь от доступа к данным и импортируйте необходимые данные через альтернативный источник (возможно, файл ccv)
Весь другой код может быть сфокусирован на издевательстве любых других зависимостей, необходимых для выполнения unit test.
Запрет на то, что остальная опция остается только для заполнения класса вручную.
В стороне, это имеет плохой запах кода во всем этом, но это выходит за рамки OP, учитывая, что в настоящий момент он не может быть изменен. Я бы предложил вам упомянуть об этом лицам, принимающим решения.
Ответ 3
В случаях, когда мне приходилось получать большое количество фактических правильных данных для тестирования, я сериализовал данные в 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
Ответ 4
Во-первых, сначала вы должны сделать получение этого объекта через интерфейс, если код в настоящее время вытаскивает его из БД. Затем вы можете издеваться над этим интерфейсом, чтобы вернуть все, что захотите, в модульные тесты.
Если бы я был на вашем месте, я бы извлек фактическую логику расчета и написал тесты по отношению к этому новому классу "калькулятор". Сломайте все как можно больше. Если вход имеет 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)
}
Ответ 5
Я бы взял такой подход:
1 - Напишите единичные тесты для каждой комбинации из объекта параметров ввода свойств 100, используя инструмент для этого (например, pex, intellitest) и убедитесь, что все они запущены зеленым. На этом этапе обратитесь к модульным тестам как к тестам интеграции, а не к модульным тестам по причинам, которые станут очевидными позже.
2 - Рефакторинг тестов в SOLID-фрагменты кода - методы, которые не вызывают другие методы, могут считаться подлинно единичными, поскольку они не имеют зависимости от другого кода. Остальные методы все еще проверяют только интеграцию.
3 - Убедитесь, что ВСЕ тесты интеграции все еще зеленые.
4 - Создайте новые модульные тесты для нового тестируемого кода.
5 - Когда все работает зеленым, вы можете удалить все/некоторые из лишних оригинальных тестов интеграции - до вас, только если вам будет удобно.
6 - Когда все работает зеленым, вы можете начать уменьшать 100 свойств, необходимых в модульных тестах, только для тех, которые строго необходимы для каждого отдельного метода. Вероятно, это выделит области для дополнительного рефакторинга, но в любом случае упростит объект параметров. Это, в свою очередь, приведет к тому, что будущие помощники по работе с кодом хуже будут работать, и я бы поставил пари, что исторические неудачи в решении вопроса о размере объекта параметра, когда у него было 50 свойств, почему это сейчас 100. Неспособность решить проблему теперь будет означать, В конце концов, возрастет до 150 параметров, что позволяет ему противостоять, никто не хочет.
Ответ 6
Используйте базу данных в памяти для ваших модульных тестов
Итак... это технически не ответ, как вы сказали, модульное тестирование, и использование базы данных в памяти делает это интеграционное тестирование, а не модульное тестирование. Однако я нахожу, что иногда, когда вы сталкиваетесь с невозможными ограничениями, вам нужно дать где-нибудь, и это может быть одно из тех случаев.
Мое предложение - использовать SQLite (или подобное) в тестировании вашего устройства. Существуют инструменты для извлечения и дублирования фактической базы данных в базу данных SQLite, затем вы можете сгенерировать скрипты и загрузить их в версию базы данных в памяти. Вы можете использовать инъекцию зависимостей и шаблон репозитория, чтобы установить поставщика базы данных по-разному в своих "единичных" тестах, чем в реальном коде.
Таким образом, вы можете использовать существующие данные, модифицировать их, когда вам нужны предварительные условия для ваших тестов. Вам нужно будет признать, что это не истинное модульное тестирование... означает, что вы ограничены тем, что база данных может действительно генерировать (т.е. Ограничения таблиц будут препятствовать тестированию определенных сценариев), поэтому вы не можете выполнить полный unit test в этом смысле, Кроме того, эти тесты будут работать медленнее, потому что они действительно выполняют работу с базой данных, поэтому вам нужно будет запланировать дополнительное время, необходимое для запуска этих тестов. (Хотя они все еще довольно быстр.) Обратите внимание, что вы можете придерживаться любых других объектов (например, если в дополнение к базе данных есть служебный вызов, который все еще является макетным потенциалом).
Если этот подход кажется вам полезным, вот некоторые ссылки, чтобы вы начали.
Конвертер 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 = file:: memory:? cache = shared" );
У меня есть отдельный script для структуры базы данных из загрузки данных, но это личное предпочтение.
Надеюсь, что это поможет, и удачи.