Страницы

Поиск по вопросам

пятница, 31 января 2020 г.

Тестирование слоя валидации данных

#c_sharp #visual_studio #entity_framework #юнит_тесты


Люди, подскажите хорошее решение. Имеется некий слой валидации данных, бизнес логики
приложения и слой работы с БД. Есть правила накладывающие ограничения (например у некоторых
сущностей есть 2 идентификатора [Guid и string определенной длины] и должно контролироваться
отсутствие их дублирования с помощью валидации). При всем этом, при написании тестов
возникает кейс что мы тестируем работу слоя валидации и нужно что бы валидация не прошла
(т.к. в базе такой объект уже существует), либо наоборот прошла так как такого объекта
еще нет. И тут возникает проблема потому что мы не знаем что есть а чего нет в тестовой
базе. Возможный вариант решения это заранее наполнить базу некими сущностями и постоянно
работать с ними, но такой подход не нравится тем что при написании теста приходится
помнить что у нас есть в базе (да и тем более в других тестах могу создаваться новые
сущности), а хочется что бы мы могли в каждом тесте создать N удобных для этого теста
объектов и работать с ними. Но такой подход с обычной базой невозможен так как единственное
решение которое я вижу, это нужно будет чистить базу в каждом тесте а это не быстро
во всяком случае. Что приходит в голову это создать мок объекты работы с базой которые
можно будет чистить в каждом тесте или создавать заново но это не кажется изящным решением.
Собственно как решить проблему изящно ?
PS. Язык C# тесты встроенные в visual studio, использую entity-framework 
    


Ответы

Ответ 1



Ваши юнит-тесты вообще не должны подключаться к базе данных. Вы должны тестировать только ваш класс, отвечающий за валидацию, все внешние зависимости этого класса надо заменить моками. У вас должен быть мок, который будет притворяться, что он подключается к базе данных, но на деле никуда не подключаться, а просто возвращать подходящие для тестирования значения. На примере одного теста. Скажем вы проверяете, что в базе нет двух элементов с одинаковым id. Тогда тест должен выглядеть примерно так: создаете мок класса для доступа к базе данных настраиваете нужный метод этого мока так, чтобы он возвращал заведомо неверные данные передаете этот мок валидатору и убеждаетесь, что валидация не прошла Код примерно такой (в примере используется nunit и moq): [Test] public void Test1() { // arrange var dbRepositoryMock = new Mock(); dbRepositoryMock.Setup(x => x.GetItems()).Returns(new [] {new Item(){ Id = 1}, new Item(){ Id = 1}}); var validator = new Validator(); // action var validationResult = validator.ValidateItemsUnique(dbRepositoryMock.Object); // assert Assert.IsFalse(validationResult); } То есть идея вот в чем: вы создаете реальный экземпляр ТОЛЬКО для тестируемого класса. В нашем случае тестируемый класс - валидатор. Для всех зависимостей вы создаете моки, и настраиваете у этих моков ТОЛЬКО те методы, которые используются тестируемым функционалом, все остальные просто игнорируете. Например, у вашего репозитория может быть еще 20 методов, возвращающих разные сущности из базы данных, но если вы проверяете уникальность Item'ов, вы настраиваете возвращаемое значение только метода GetItems(), а все остальные методы просто игнорируете. Про подключение к базе данных вообще забудьте, ваши юнит тесты должны выполняться без этого. Если вам сложно написать код в таком стиле - без реального подключения к базе данных, значит он плохо приспособлен для юнит-тестирования, и надо выполнять его рефакторинг. Ваша попытка написать юнит-тесты с подключением к реальной бд довольно типичная ошибка для начинающих писать юнит-тесты. Я тоже пытался делать так и видел как то же самое пытаются сделать другие. Эта ошибка указывает на то, что вы не до конца понимаете смысл юнит-тестирования и вам стоит потратить время на чтение какой нибудь книги по этой теме. "Разработка через тестирование" Кента Бека будет отличным вариантом.

Ответ 2



Строго говоря, вы пишете не юнит тесты, а, скорее, интеграционные тесты. Если не ограничиваться ответом "тесты на базе это плохо, постарайтесь по возможности избегать этого" - то проще всего решить проблему чистки базы в интеграционных тестах отменой транзакции на каждом тесте: один раз создавать чистую базу при старте тестов (указанием DropCreateDatabaseAlways, или вручную, по статическому флагу) создавать новый TransactionScope в TestInitialize диспоузить этот TransactionScope в TestCleanup, без вызова Complete Работать будет не мгновенно, накладные расходы будут порядка 100-200 ms на тест, но для существующего (уже написанного без тестов) кода это самый быстрый вариант. Eсли вы планируете рефакторить код в сторону лучшей тестируемости моками - лучше если к моменту рефакторинга у вас будут готовые тесты на тот код, который уже есть, пусть даже эти тесты будут медленными и будут делать реальные запросы к базе. Рефакторить непокрытый тестами код может только Чак. Для всех остальных это черевато затягиванием сроков, толпами новых багов и "в гробу я видал ваши тесты и рефакторинг" от ближайшего вверх по иерархии нетехнического начальника. Кстати, ваш способ валидации ненадежен. Ничто не помешает гому потоку влезть в базу между проверкой с результатом "все ок" и вставить туда дубликат. Такие проверки все равно надо дублировать констрейнтами на уровне базы.

Комментариев нет:

Отправить комментарий