📜 ⬆️ ⬇️

Testing with a database in .NET


A common approach in .NET for testing applications that work with a database is dependency injection (Dependency Injection). It is proposed to separate the code working with the base from the main logic by creating an abstraction, which can later be replaced in tests. This is a very powerful and flexible approach, which nevertheless has some drawbacks - increasing complexity, separating logic, explosive growth in the number of types. More in the previous article Something is not right with testing in .NET (Java, etc.) or in Wiki / Dependency Injection .


There is a simpler approach, widespread in the world of dynamic languages. Instead of creating an abstraction that can be controlled in tests, this approach suggests controlling the base itself. The test framework provides a clean base for each test, and you can create a test script in it. It is easier and gives more confidence in the tests.



Example


As the previous article showed, the example is very important. If it is unsuccessful, the example itself is criticized, not the approach. Here I gave him more attention, but of course he is not perfect either:

There is some kind of application for inventory accounting of goods. Goods can be moved between warehouses using movement documents. A method is needed that allows to obtain balances at a specified warehouse at a specified point in time.

To do this, we introduce the following method (and it will need to be tested):

public class ReminesService { RemineItem[] GetReminesFor(Storage storage, DateTime time) { ... } } 

The article will not implement this method, but it is in the repository on the githaba.
')

Test database


We will need a database for testing. For simple projects you can use SQLite, this is a good compromise between the speed of tests and their reliability. For more complex cases, it is better to use the same database as in the development. In most cases, this is not a problem - MySql and PostgreSql are lightweight, for SQLServer there is a LocalDb mode.

If you are working with SQLServer, it is convenient to use the LocalDb mode for the test database - it is much easier and faster than the full database, while fully functional. To do this, you need to configure App.config in the test project:

Configuration for SQLServer LocalDb
 <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="MSSQLLocalDB" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration> 


Framework


Since this approach is very rare in .NET, there are almost no ready-made libraries for its implementation. Therefore, I designed the work in this area in a small library DbTest . You can view the sources and examples on the githab or install into the project via nuget. The project is in the preliminary version and the API may change - so be careful.

Initial data


In a real system, there are many relationships between models; in order to insert at least one row in the target table, you need to fill in a set of related tables. For example, a product (Good) may refer to a manufacturer (Manufacturer), which in turn refers to a country (Country).

To simplify the further creation of test scripts, you need to create a minimum set of common data for the system.

To make it a bit more fun, let's take whiskey bottles as goods. Let's start with the model that has no dependencies - the country of the manufacturer (Country):

 public class Countries : IModelFixture<Country> { public string TableName => "Countries"; public static Country Scotland => new Country { Id = 1, Name = "Scotland", IsDeleted = false }; public static Country USA => new Country { Id = 2, Name = "USA", IsDeleted = false }; } 

In order for the framework to understand that this is a description of the initial data, the class must implement the IModelFixture<T> interface. Instances of models are declared static to provide access to them from other fixtures and tests. You must explicitly specify the primary keys ( Id ) and follow their uniqueness within the same model.

Now you can create manufacturers:

 class Manufacturers : IModelFixture<Manufacturer> { public string TableName => "Manufacturers"; public static Manufacturer BrownForman => new Manufacturer { Id = 1, Name = "Brown-Forman", CountryId = Countries.USA.Id, IsDeleted = false }; public static Manufacturer TheEdringtonGroup => new Manufacturer { Id = 2, Name = "The Edrington Group", CountryId = Countries.Scotland.Id, IsDeleted = false }; } 

And the goods:

 public class Goods : IModelFixture<Good> { public string TableName => "Goods"; public static Good JackDaniels => new Good { Id = 1, Name = "Jack Daniels, 0.5l", ManufacturerId = Manufacturers.BrownForman.Id, IsDeleted = false }; public static Good FamousGrouseFinest => new Good { Id = 2, Name = "The Famous Grouse Finest, 0.5l", ManufacturerId = Manufacturers.TheEdringtonGroup.Id, IsDeleted = false }; } 

Pay attention to foreign keys - they are not specified explicitly, but refer to another fixture.

Such an approach has many advantages over sql files or json fixtures files:


Important! This approach has a drawback - every time a static property is accessed, an instance of the model and all its dependent models are created (and their dependencies, too). If you experience performance problems or circular references, you can fix this with lazy initialization Lazy <T>.

 private static Good _famousGrouseFinest = new Lazy<Good>(() => new Good { Id = 2, Name = "The Famous Grouse Finest, 0.5l", ManufacturerId = Manufacturers.TheEdringtonGroup.Id, IsDeleted = false }; public static Good FamousGrouseFinest => _famousGrouseFinest.Value; 

Environment preparation


The test environment is primarily a database, it can also be singletons and static variables (for example, HttpContext can be set in asp.net). It is better to collect all these operations in one place and run before each test. We have called such a place - World. To prepare the database, you need to call the ResetWithFixtures method and pass a list of initial fixtures there.

 static class World { public static void InitDatabase() { using (var context = new MyContext()) { var dbTest = new EFTestDatabase<MyContext>(context); dbTest.ResetWithFixtures( new Countries(), new Manufacturers(), new Goods() ); } } public static void InitContextWithUser() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://your-domain.com", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new GenericPrincipal( new GenericIdentity("root"), new string[0] ); } } 

The ability to set static variables and singletons is especially important when testing legacy code, where it is not so easy to change the architecture - but there is an urgent need for testing. Separating the setting of the environment into several methods allows you to prepare the environment for each individual test. For example, in unit tests the base is not used and there is no point in clearing the base for them. Or you may need to prepare a different environment for different system states (authorized and unauthorized user).

Creating a test script


In tests, we have to do a lot of preparatory work, the Arrange phase of the test is the most crucial and difficult. Therefore, it is desirable to create helpers that simplify this process, make the code easier to read. One convenient mechanism is to create a ModelBuilder, which creates entities, saves them to the database and returns instances for further use:

 public class ModelBuilder { public MoveDocument CreateDocument(string time, Storage source, Storage dest) { var document = new MoveDocument { Number = "#", SourceStorageId = source.Id, DestStorageId = dest.Id, Time = ParseTime(time), IsDeleted = false }; using (var db = new MyContext()) { db.MoveDocuments.Add(document); db.SaveChanges(); } return document; } public MoveDocumentItem AddGood(MoveDocument document, Good good, decimal count) { var item = new MoveDocumentItem { MoveDocumentId = document.Id, GoodId = good.Id, Count = count }; using (var db = new MyContext()) { db.MoveDocumentItems.Add(item); db.SaveChanges(); } return item; } } 

We are testing


It's time to put everything together and see what happened:

 [SetUp] public void SetUp() { World.InitDatabase(); //      } [Test] public void CalculateRemainsForMoveDocuments() { /// ARRANGE -    var builder = new ModelBuilder(); //      var doc1 = builder.CreateDocument("15.01.2016 10:00:00", Storages.MainStorage, Storages.RemoteStorage); builder.AddGood(doc1, Goods.JackDaniels, 10); builder.AddGood(doc1, Goods.FamousGrouseFinest, 15); //      var doc2 = builder.CreateDocument("16.01.2016 20:00:00", Storages.RemoteStorage, Storages.MainStorage); builder.AddGood(doc2, Goods.FamousGrouseFinest, 7); /// ACT -    var remains = RemainsService.GetRemainFor(Storages.RemoteStorage, new DateTime(2016, 02, 01)); /// ASSERT -   Assert.AreEqual(2, remains.Count); Assert.AreEqual(10, remains.Single(x => x.GoodId == Goods.JackDaniels.Id).Count); Assert.AreEqual(8, remains.Single(x => x.GoodId == Goods.FamousGrouseFinest.Id).Count); } 

Note the use of initial fixtures in the test code.
Storages.MainStorage , Goods.JackDaniels , Goods.FamousGrouseFinest , etc.

It is very convenient that you have all the objects at hand that already exist in the database and can be used in any phase of the test.

Summary


This approach is unfairly bypassed in the world of strongly-typed languages, while it is very widespread in dynamic languages. This is not a silver bullet and is not a substitute for DI, but it is a very convenient and appropriate approach in many cases.

Compared to DI, testing with this database has the following advantages:


The biggest fly in the ointment with integration tests is the run time, they are much slower, but this is a solvable problem. At least server time is much cheaper than developer time.

DI is a very good and my favorite technique, any self-respecting programmer should be able to use it. However, in the field of testing there is a very good alternative, which has a different set of advantages and disadvantages. I am in favor of having a large set of methods and approaches in the arsenal and each was applied according to the situation.

useful links


DbTest (repository with test framework and examples from the article)
Smocks (mock for static system methods)

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


All Articles