📜 ⬆️ ⬇️

Creating a test DB context in tests using xUnit

In cases where your application has a non-trivial data scheme (people, products, orders, prices, volumes, states depending on a heap of parameters, etc.), it is easier to have some data dump recreated on the test environment, or taken from production, and use it for tests. In this case, you may need several data dumps, for each of the cases that automated tests should be able to roll and roll back to the test environment. In this article I will try to show how this can be done using the fixtures and collections of the xUnit framework. The entire solution is based on xUnit version 2.0 dated March 16, 2015 .

Test execution script in context


The simplest scenario for data-driven tests might look like this:
  1. create a database for test collection
  2. update the database to the latest version (optional)
  3. run automatic tests
  4. delete DB

Now I do not want to dwell on cases when it is necessary to update the database, since These technical details are not important to this article. But I would like to note that ADO.NET does not allow the execution of scripts in which there is a GO. If you want to automatically roll scripts, build your system so that you can roll each script separately. Even the SQL Server Management Objects (SMO) library breaks scripts by GO and runs the pieces individually (tested). Thus, the 2nd paragraph will not be considered in the article. XUnit will help us with the rest. A description of the general concept of xUnit contexts can be found in their documentation .

Fixture is a class created before running tests from one suite. I call a suite a class with tests in order to avoid tautology. Collection is a class that describes a group of suites, but is never created during the execution of tests. It is for description only. Below it will be shown in the examples.

Life cycle of Fixtures and Collections


Examples you can find on github . Beginning with xUnit 2.0, the authors replaced IUseFixture with ICollectionFixture and IClassFixture.
')
To demonstrate how xUnit creates instances of classes, I created three suites. Two of them must run in the same context.

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() } 

To see the lines in the output window as below, tests must be run in debug mode. I want to note one thing about the papallism of the tests in xUnit. By default, tests are performed synchronously within one suite, or collection, if there is one. In other cases, tests are performed in parallel. More details about this can be found here . Therefore, in reality, the output may be slightly different on your computer, but I sorted it for greater visibility.

 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 

Thus, you can see to group several tests into one context you can use ICollectionFixture. At the same time, IClassFixture can adjust the environment settings for a specific suite. And it is important to note that the designer of the suite is called for each individual test, no matter how many. In Dispose, it is reasonable to locate the cleanup code of the appropriate scop (test, suite or collection).

Implementation details


Now it should be obvious that you can create a class that executes the script described above and attach it to the tests using ICollectionFixture or IClassFixture, depending on the specific tasks. In my example, I used a collection that restores the database before the tests, and in Dispose () it drops it.

It is worth noting about the following problems of this approach:

Below are examples of T-SQL for restoring and deleting databases.

 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}] 

Some comments for example on GitHub. To make fixture with a universal database context, you need to pass the connection string and the path to the backup file to the constructor. You can use the SqlConnectionStringBuilder class to define the database name in the connection string for other scripts, since create and delete scripts should be run in the context of the [master] database. If you need to remove the database after some test suite, do it forcibly by calling Dispose (). It will, of course, be called by xUnit itself, but this is non-deterministic and, perhaps, a little earlier your tests will fall down due to database conflicts.

Source: https://habr.com/ru/post/265501/


All Articles