Страницы

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

вторник, 26 ноября 2019 г.

Как тестировать методы для работы с бд?


В своем приложении я использую entity-framework версии 6.1.3 для работы с БД mssql.

Назрел вопрос покрытия своего кода тестами.

Подскажите,пожалуйста, способы тестирования для данной связки.

Мне в голову приходит создание локальной тестовой БД, наполнение её данными и тестировани
методов на данной БД, при данном подходе я смогу проверить как методы получения данных так и CRUD методы.

Так же я слышал что для тестирования методов для работы с БД можно создать generic репозиторий public interface IRepository where T: class { }
и для тестирования подсовывать fake репозиторий с тестовыми данными.

Я пока склоняюсь больше к первому сценарию, т.е. созданию локальной тестовой БД
которая будет подвержена тем же изменениям что и реальнай бд(засчет механизма миграций в ef).

Подскажите насколько правильным будет тестирование описанным мной способом? Буд
рад услышать о других способах.
    


Ответы

Ответ 1



Тестирование — это процесс исследования и испытания программного обеспечения (ПО), преследующий две основные задачи: убедиться в том, что ПО рабочее и соответствует требованиям, а также выявить ситуации, в которых поведение ПО является неправильным, нежелательным или не соответствует начальным требованиям. Вы рассматриваете два варианта тестирования. Для того, чтобы выявить более подходящий необходимо выяснить плюсы и минусы обоих подходов и отталкиваясь от полученных результатов сделать выбор. Я всего лишь Вам посоветую, а выберете Вы сами. Вариант #1 Создание локальной тестовой БД, которая будет подвержена тем же изменениям что реальная БД(за счет механизма миграций в EF) Данный подход может существовать, но глядя на него возникают вопросы, а готовы ли Вы пожертвовать некоторыми недостатками этого подхода? К недостаткам я бы отнес: Прямая зависимость тестов от данных, хранящихся в БД Пересечение данных для нескольких тестов. Бывают такие ситуации, в тестах, когд мы имитируем ошибку или ожидаем неправильное поведение - это вполне нормально. Но как быть в этом случае? У нас будут данные и хорошие и плохие. Зависимость от миграций. Тут несколько подпунктов. Первый: если забудем сделать миграци - упадут тесты. Вторая: необходимо всегда помнить про то, что нужно сделать миграцию и для тестов. Третья: сопровождение таких тестов. Что будет делать новый программист? Что если БД вдруг упала? Тесты снова не работают. Данный подход не назовешь простым, имеются свои сложности. Думаю, что это тоже стоит вписать, в комментарии увидел: Для проверки миграции всегда можно держать бэкап нужной версии Вариант #2 Для тестирования методов для работы с бд можно создать generic репозиторий publi interface IRepository where T: class { } и для тестирования подсовывать fake репозиторий с тестовыми данными. Это более удобный вариант тестирования, на мой взгляд. Давайте рассмотрим почему: Мы не зависим от данных в БД, так как данные подготавливаются в самих тестах. Данные не пересекаются, мы всегда знаем, что тесты работают только с тем, что м им подсунем. Имитация ситуаций, когда мы ожидаем ошибку не ломает наши тесты и не приводит к проблемам. Простота использования. Не нужно делать миграций. Есть много документации, которая проиллюстрирует примеры использования моков (фэйков). Скорость тестирования. Тут очень большое преимущество у моков(фэйков). Они работаю намного быстрее. Есть некий недостаток этого подхода, как мне сказали - это тестирование логики, которая заложена в БД: индексы, конкурентный доступ и т.п. Вариант #3 Можно объединить эти подходы, вывести что-то промежуточное. К примеру перед запуско писать данные в базу - это подготовка данных. Затем работать с конкретной БД, в которо лежат наши данные, то есть своего рода БД для тестов. Главное не забывать убирать за собой - удалять данные, которые используются для теста. Такой подход довольно громоздкий и тоже не простой. Использование такого подхода подразумевает под собой написание вспомогательной плюшки которая будет помогать. Это поможет тестировать CRUD методы. Но тут возникает и другой вопрос: кто будет тестировать вспомогательную утилиту? То есть и такой подход имеет место быть. Ну и еще один вопрос: Вы собираетесь тестировать Ваш код или EF? Если вести разработку по Test Driven Development, то использование фэйков существенно упрощает разработку. Я не хочу конкретно акцентировать свое внимание на одном варианте, я всего лишь хоч посоветовать сделать выбор, который будет более правильным и в дальнейшем не вызовет проблем. Чтобы спустя время Вы не оглянулись на проделанную работу и не сказали себе "А почему я не пошел другим путем?". Выбор необходимо делать основываясь на поставленной задаче. Использовать только один подход вряд ли получится - скорее всего необходимо использовать несколько подходов.

Ответ 2



Перечисленные вами способы относятся к разным видам тестирования. Создание базы запросы к ней — интеграционное тестирование. Создание базы, её наполнение, прохождени тестов — всё это будет отнимать много времени и вы не захотите запускать эти тесты ежеминутно С другой стороны, эти тесты можно включить в процесс сборки (интеграции, непрерывной интеграции). Если у вас нет сервера непрерывной интеграции, запускайте их вручную, просто реже. Чтобы сделать это, пометьте их атрибутом TestCategory, или разместите все в одной сборке: Visual Studio позволит вам запускать тесты только из отдельного проекта или только одной категории. Второй вариант — модульное тестирование (unit testing). Именно модульные тесты запускаютс постоянно и они, естественно, не должны выполнять длительные операции: обращаться к файлам, обращаться к сетевым ресурсам, к серверу баз данных. Именно в этих тестах используют mock и stub-объекты (они же fake). Поэтому мой ответ: вам придётся делать и те, и другие тесты. Теперь о том, как это делать. Модульные тесты (unit tests) Очень часто программисты задают вопрос — как мне модульно протестировать Entity Framework Ответ: никак, модульные тесты на Entity Framework пишут его разработчики, а вы пишите тесты на свой код. Ошибкой также будет написание сложных тестов, которые значительно сложнее вашего кода. public class EfUserRepository : IUserRepository { private readonly IDbContextFactory dbContextFactory; public EfUserRepository(IDbContextFactory dbContextFactory) { this.dbContextFactory = dbContextFactory; } public UserData ReadById(int id) { using (var dbContext = _dbContextFactory.Create()) { return dbContext.Users.Single(u => u.Id == id); } } } Что вы действительно можете протестировать в этом коде? Сразу хочется протестироват работу Single и убедиться, что метод вернёт один-единственный ответ или выдаст исключение Но Single — не ваш код, более того, по некоторым причинам, довольно трудно проверит его вызов, поскольку это метод расширения, а не метод интерфейса IQueryable. Но вы можете проверить, что у dbContextFactory будет вызыван метод Create и у созданного контекста будет обращение к свойству Users. Вот как это можно сделать просто, с помощью библиотеки Moq public void ReadById_WhenCalled_CallsDbContextFactoryCreate() { var dbContextFactoryMock = new Mock(); var dbContextFactory = dbContextFactoryMock.Object; var userRepository = new EfUserRepository(dbContextFactory); var user = userRepository.ReadById(1); dbContextFactoryMock.Verify(x => x.Create()); } Интеграционное тестирование (integrating testing) Здесь вам потребуется база и реальные запросы. Именно в интеграционном тестировани вы проверяете код в целом — создаете запись в таблице, потом читаете и убеждаетесь, что она там есть. Ваш вопрос — как заполнить эту базу, и основных отета здесь два: вы можете либо отдельн заполнить базу тестовыми данными и отдельно писать тесты, и вы можете в каждом тест создавать свой тестовый набор данных (создавать нужные записи в таблицах). Второй вариант кажется плохим — будет много лишних записей, тесты будут работать долго. Но, если ваш проект большой, это единственное решение, поскольку вы довольно быстро запутаетесь, потому что заполнение базы будет в одном месте, а использование — в нескольких других местах. Но сейчас, как я понимаю, вы работаете над проектом один и таблиц там в предела десяти-пятнадцати? Подойдёт первый вариант. Надо ли чистить базу после тестов? Можно, но в большинстве работающих проектов удалени данные приводит к проблемам. Обычное решение — всё-таки запускать тесты на отдельной базе, может быть даже на каждый раз новой. Лучше всего навесить интеграционное тестирование на макроопределение, похожее н DEBUG, например, на DB_INTEGRATION. В миграциях Entity Framework (в папке Migrations) лежит класс Configuration, у которого есть метод Seed. Этот метод вызывается для заполнения данных. internal sealed class Configuration : DbMigrationsConfiguration { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(DbContext context) { FillTestData(context); } [Conditional("DB_INTEGRATION")] private void FillTestData(DbContext context) { // с помощью context.AddOrUpdate добавляем тестовые записи // во все таблицы context.SaveChanges(); } } Вставляем метод FillTestData и навешиваем на него атрибут Conditional, благодар которому метод будет вызываться и вставляться в сборку только в том случае, если объявлено макроопределение DB_INTEGRATION. Точно такой же атрибут надо добавить на все интеграционные тестовые методы, которые будут обращаться к этим данным. [TestMethod] [TestCategory("Integration")] [Conditional("DB_INTEGRATION")] public void UserCreation() { var dbContextFactory = new DbContextFactory(); var userRepository = new EfUserRepository(dbContextFactory); var username = Utils.GenerateRandomString(200); var user1 = userRepository.Create(username); var user2 = userRepository.ReadById(user1.Id); Assert.AreEqual(user1.Name, user2.Name); } Теперь зайдите на страницу свойств тестового проекта, и на вкладке Build впишит DB_INTEGRATION в список Conditional compilation symbols. После перекомпиляции проекта в базе появятся тестовые данные, а в панели тестов — тесты. В Test Explorer левая верхня кнопка Group By, выбирайте Group By Traits и тесты категории Integration будут показаны в отдельной группе, которую и можно будет запустить. Обычные модульные тесты окажутся в группе No Traits, так что их удобно будет запускать отдельно от интеграционных тестов и часто. Можно ли сделать интеграционное тестирование быстрым? Вот если очень хочется сделать реальный тест обращений к базе, но чтобы работал быстро? Да, возможно. Второй вопрос: модульные тесты должны работать даже в случае, если у вас нет никаки внешних сервисов и баз данных, можно ли проводить тестирование без внешнего сервера БД? Тоже да. Многие современные СУБД позволяют хранить базы в памяти. Некоторые СУБД специальн спроектированы так, чтобы эмулировать работу внешнего сервера, хотя они встроены в ваше приложение, см. например SQLite и SQL Server Compact Edition. Эти тесты всё ещё не будут модульными по сути, но по быстродействию будут на уровне модульных, и их можно будет запускать часто. Начните изучения вопроса с http://weblogs.asp.net/scottgu/vs-2010-sp1-and-sql-ce https://www.nuget.org/packages/EntityFramework.SqlServerCompact/

Ответ 3



Придумывать обертку над EF только для того, чтобы при тестировании не зависеть от БД - не самая лучшая идея. Потому что эту обертку тоже кто-то должен тестировать :) Поэтому если у вас нет отдельного DAL - тестировать надо на отдельной тестовой БД Поддерживать ее в актуальном состоянии - не проблема, создать базу и накатить миграции можно прямо в тестах. Можно это делать в каком-нибудь SetUpFixture Если же у вас есть отдельный DAL - то его надо тестировать точно так же, на тестовой базе. А вот все, что выше DAL - уже можно тестировать подменой DAL. PS Если тестируемый класс не управляет соединениями и транзакциями внутри - можн провести тест внутри транзакции, а потом откатить ее - это решит проблему накопления ошибок в тестовой БД.

Ответ 4



По возможности, полноценные тесты должны быть аналогичны работе реального приложения. Т.е. на запуск тестов создается база, как она создается для реальной работы, накачиваетс данными необходимыми для работы (и тестовыми, если нужно нагрузочное тестирование). Дальше, соответственно идёт имитация работы клиента - подключение, выполнение операций, получение результатов и их проверка на валидность. Стоит иметь в виду, что некоторые тесты начинают пересекаться по данным, поэтому по возможности стоит удалять данные за тестом. Т.е. тест сам создает себе тестовые данные, сам их обрабатывает и проверяет, а потом ещё и сам удаляет, для минимизации влияния своих данных на другие тесты. Как примерно выглядит такой тест: [TestMethod] public void RegisterLetter() { this.Given(_ => this.GivenNewlyLetter()) .And(_ => this.GivenNewlyDocumentRegister(DocumentFlow.Incoming)) .When(_ => this.WhenRegisterDocument(this.letter)) .Then(_ => this.LetterShouldBeRegistred()) .TearDownWith(_ => this.DeleteLetter()) .TearDownWith(_ => this.DeleteDocumentRegister()) .BDDfy(); } Создаем письмо, создаем для него журнал. Регистрируем в журнале письмо, проверяем что регистрация успешна. Удаляем письмо, удаляем журнал. Удаление сущностей происходит в любом случае, хоть успешен тест, хоть нет. Ещё один серьезный плюс такого подхода в том, что если у вас не разворачивается новая база, вы об этом узнаете быстро. Есть и серьезный минус, на который я пока так и не нашел удобного решения. Если вас нет возможности почистить базу после теста (бизнес-ограничение, которое не получается обойти легально и просто), то делать связанные тесты на эти же данные становится сложнее. Тестирование вторым способом, через Моки\Фейки будет быстрее, потому как не над будет стучаться в базу, но и пользы от него будет меньше, потому что соединение с базо и какие то специфические особенности EF могут быть упущены. С другой стороны, если вы хотите тестировать бизнес логику приложения и на особенности хранения в базе вам без разницы - это отличный вариант, который намного легче развернуть, запустить и получить результаты. Стоит иметь в виду, что так нельзя тестировать например нагрузку, потому как вы выкинул целый слой приложения. Так нельзя тестировать сложные запросы, потому как EF их в SQL завернёт, где то упадёт при материализации и прочее, а тут у вас фейк, который работает в памяти и врядли будет имитировать любую ORM с достаточной достоверностью. Собственно, плюсы этого тестирования, на мой взгляд, сильно проседают под реальностью. Как вывод - на мой взгляд, тесты должны быть клиентскими в первую очередь. Т.е. работать так, как будет работать клиент - авторизация, операция, результат проверка, выход. Пока не меняется клиентский интерфейс, ему одновременно неважно, что спрятано в базе данных, и одновременно он проверяет сразу все слои приложения. Естественно, тут возникает проблема, что развернуть такого клиента гораздо сложнее чем просто написать тест на функцию, которой скормили параметр и получили результат. С другой стороны, разворачивание такого клиента обычно почти равнозначно разворачиванию реального клиента, унифицировать не так уж и сложно. И да, не стоит сильно задумываться над тем, как за собой почистить базу. В большинстве случаев, достаточно развернуть чистую базу и дропнуть её за собой. А когда данные от других тестов начинают влиять друг на друга - у вас они либо реальному клиенту мешают, либо тест недостаточно корректно изображает из себя клиента.

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

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