Создание Контекста Тестовой Бд В Тестах С Использованием Xunit

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

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

В этой статье я попытаюсь показать, как это можно сделать с помощью фикстур и коллекций фреймворка xUnit. Все решение построено на xUnit версии 2.0. от 16 марта 2015 г.

.



Сценарий выполнения теста в контексте

Самый простой сценарий для тестов, управляемых данными, может выглядеть так:
  1. создать базу данных для сбора тестов
  2. обновить базу данных до последней версии (необязательно)
  3. запускать автоматические тесты
  4. удалить базу данных
Сейчас я не хочу останавливаться на случаях, когда необходимо обновить базу данных, потому что.

для данной статьи эти технические подробности не важны.

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

Даже библиотека объектов управления SQL Server (SMO) разбивает сценарии на GO и запускает их части по отдельности (проверено).

Таким образом, 2-й пункт в статье рассматриваться не будет. xUnit поможет нам с остальным.

Вы можете найти описание общей концепции контекстов xUnit в их документация .

Fixture — это класс, созданный перед запуском тестов из одного пакета.

Класс с тестами я называю сюитой, чтобы не было тавтологии.

Коллекция — это класс, который описывает группу наборов, но никогда не создается во время выполнения теста.

Это только для описательных целей.

Ниже это будет показано на примерах.



Жизненный цикл светильников и коллекций

Вы можете найти примеры на GitHub .

Начиная с xUnit 2.0, авторы заменили IUseFixture на ICollectionFixture и IClassFixture. Чтобы продемонстрировать, как xUnit создает экземпляры классов, я создал три набора.

Два из них должны выполняться в одном контексте.

  
  
  
   

public class CollectionFixture : IDisposable { public CollectionFixture() public void Dispose() } public class ClassFixture : IDisposable { public ClassFixture() public void Dispose() } [CollectionDefinition("ContextOne")] public class TestCollection : ICollectionFixture<CollectionFixture> { public TestCollection() // TestCollection is never instantiated } [Collection("ContextOne")] public class TestContainerOne : IClassFixture<ClassFixture>, IDisposable { public TestContainerOne() [Fact] public void TestOne() [Fact] public void TestTwo() public void Dispose() } [Collection("ContextOne")] public class TestContainerTwo : IDisposable { public TestContainerTwo() [Fact] public void TestOne() public void Dispose() } public class TestContainerThree { [Fact] public void TestOne() }

Чтобы увидеть в окне вывода строки, подобные приведенным ниже, тесты необходимо запускать в режиме отладки.

Хотелось бы отметить один момент по поводу практичности запуска тестов в xUnit. По умолчанию тесты выполняются синхронно в одном наборе или коллекции, если таковая имеется.

В других случаях тесты выполняются параллельно.

Вы можете узнать об этом больше читай здесь .

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



CollectionFixture : ctor ClassFixture : ctor TestContainerOne : ctor TestContainerOne : TestOne TestContainerOne : disposed TestContainerOne : ctor TestContainerOne : TestTwo TestContainerOne : disposed ClassFixture : disposed TestContainerTwo : ctor TestContainerTwo : TestOne TestContainerTwo : disposed CollectionFixture : disposed TestContainerThree : TestOne

Итак, вы можете видеть, что для группировки нескольких тестов в один контекст вы можете использовать ICollectionFixture. В то же время IClassFixture может настроить параметры среды для конкретного пакета.

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

Имеет смысл поместить код очистки для соответствующей области (теста, набора или коллекции) в Dispose.

Детали реализации

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

В моем примере используется коллекция, которая восстанавливает базу данных перед тестами и удаляет ее в Dispose().

Стоит отметить следующие проблемы данного подхода:

  • При восстановлении базы данных из резервной копии используется внутренняя информация о файлах.

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

    Для решения проблемы необходимо восстановить базу путем перемещения файлов.

    Это пример на GitHub.

  • Как правило, в базе данных может быть разное количество файлов (данных, журнала, файлового потока и т. д.).

    К этой ситуации нужно относиться правильно.

    Но для целей этой статьи я предполагаю, что нужны только данные и журнал.

    Остальные файлы игнорируются с помощью частичного восстановления ( ЧАСТИЧНЫЙ ).

Ниже приведены примеры T-SQL для восстановления и удаления базы данных.



IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = '<DBNAME>') BEGIN DECLARE @Table TABLE ( LogicalName VARCHAR(128) , [PhysicalName] VARCHAR(128) , [Type] VARCHAR , [FileGroupName] VARCHAR(128) , [Size] VARCHAR(128) , [MaxSize] VARCHAR(128) , [FileId] VARCHAR(128) , [CreateLSN] VARCHAR(128) , [DropLSN] VARCHAR(128) , [UniqueId] VARCHAR(128) , [ReadOnlyLSN] VARCHAR(128) , [ReadWriteLSN] VARCHAR(128) , [BackupSizeInBytes] VARCHAR(128) , [SourceBlockSize] VARCHAR(128) , [FileGroupId] VARCHAR(128) , [LogGroupGUID] VARCHAR(128) , [DifferentialBaseLSN] VARCHAR(128) , [DifferentialBaseGUID] VARCHAR(128) , [IsReadOnly] VARCHAR(128) , [IsPresent] VARCHAR(128) , [TDEThumbprint] VARCHAR(128) ) INSERT INTO @Table EXEC ( 'RESTORE FILELISTONLY FROM DISK = ''<PATH_TO_BACKUP_FILE>''') DECLARE @LogicalNameData varchar(128), @LogicalNameLog varchar(128) SET @LogicalNameData=(SELECT LogicalName FROM @Table WHERE Type='D') SET @LogicalNameLog=(SELECT LogicalName FROM @Table WHERE Type='L') EXEC ('RESTORE DATABASE [<DBNAME>] FROM DISK = ''<PATH_TO_BACKUP_FILE>'' WITH MOVE '''+@LogicalNameData+''' TO ''<PATH>\<DBNAME>_Data.mdf'', MOVE '''+@LogicalNameLog+''' TO ''<PATH>\<DBNAME>_Log.ldf'', REPLACE, PARTIAL' ) END



ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE [{0}]

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

Вы можете использовать класс SqlConnectionStringBuilder, чтобы определить имя базы данных в строке подключения для других сценариев, поскольку сценарии создания и удаления должны выполняться в контексте базы данных [master].

Если вам нужно удалить базу данных после определенного набора тестов, принудительно выполните это, вызвав Dispose().

Его, конечно, будет вызывать сам xUnit, но это недетерминировано и, возможно, чуть раньше ваши тесты провалятся из-за конфликта базы данных.

Теги: #tdd #xunit #.

NET #.

NET #tdd

Вместе с данным постом часто просматривают: