XUnit Execute Code Before and After Each Test

Recently I am exploring XUnit some internal information. There are some test framework that has specific method that execute before and after each test method. This is also known as Setup and Teardown.

Is it possible with XUnit ?

Answer: Yes.

In order to achieve this, It is required to implement IAsyncLifetime interface in your test class.

Scenario

  1. We have one DbContext and It has one DbSet called Person.

  2. As part of Unit Test or Integration Test, It is required that DbContext properly Set for each method. This is most of the time case with Unit Test. Many will suggest that use In Memory database for unit test and that is upto certain extend true and some might suggest that use repository pattern but if you have some portable database engine which is similar to your main provider then those test are quite useful as It gives proper behavior internal to provider.

  3. In my case Database is SQL Server so I am using LocalDb for unit test ( or call it integration test.)

  4. For each method we need DbContext in proper clean state. (There are some good solution and but it is not part of this article as this will focus on XUnit part only.)

Solution

DbContext and Entity

public class SampleContext : DbContext
    {
        public SampleContext(DbContextOptions<SampleContext> dbContextOptions)
            : base(dbContextOptions)
        {

        }
        public DbSet<Person> Persons { get; set; }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

Unit Test With IAsyncLifetime

public class SampleDbContextTests
    { 
        [Fact]
        public async Task Add_One_Two_Return_Three()
        {
            var context = await CreateContext();
            context.Persons.Add(new Person() { Name = "Test" });
            context.SaveChanges();

            var count = context.Persons.Count();
            Assert.Equal(1,context.Persons.Count());
            await DeleteContext(context);
        }

        [Fact]
        public async Task Add_One_MinusOne_Return_Zero()
        {
            var context = await CreateContext();
            context.Persons.Add(new Person() { Name = "Test" });
            context.Persons.Add(new Person() { Name = "Test2" });
            context.SaveChanges();

            Assert.Equal(2, context.Persons.Count());
            await DeleteContext(context);
        }

        private async Task<SampleContext> CreateContext(
            [CallerMemberName] string methodName = "default")
        {
            DbContextOptionsBuilder<SampleContext> dbContextOptionsBuilder
                = new DbContextOptionsBuilder<SampleContext>();

            _ = dbContextOptionsBuilder.UseSqlServer($"Server=(localdb)\\MSSQLLocalDB;Database={methodName};Trusted_Connection=True");
            var context = new SampleContext(dbContextOptionsBuilder.Options);
            await context.Database.EnsureDeletedAsync();
            await context.Database.EnsureCreatedAsync();
            return context;
        }

        private async Task DeleteContext(SampleContext context)
        {
            await context.Database.EnsureDeletedAsync();            
        }
}
  • So if you look at above sample code than you can see that each create database and later it delete it.

  • Probably it works fine too but you have to explicitly delete database otherwise nexttime test created at that time it may create problem.

  • Also code is less clear as setup db and tear it down required for each test then It is step to remember and also no clarity over setup and cleanup step.

  • Also here we have to create new db as if same dbname used then it may problem as no clarity when test will execute.

UnitTest with LifeTime methods.

    public class SampleDbContextWithLifeTimeTests : IAsyncLifetime
    {
        SampleContext context = default!;

        [Fact]
        public async Task Add_One_Two_Return_Three()
        {
            context.Persons.Add(new Person() { Name = "Test" });
            context.SaveChanges();

            var count = context.Persons.Count();
            Assert.Equal(1, context.Persons.Count());
        }

        [Fact]
        public async Task Add_One_MinusOne_Return_Zero()
        {
            context.Persons.Add(new Person() { Name = "Test" });
            context.Persons.Add(new Person() { Name = "Test2" });
            context.SaveChanges();

            Assert.Equal(2, context.Persons.Count());

        }
        public async Task InitializeAsync()
        {
            DbContextOptionsBuilder<SampleContext> dbContextOptionsBuilder
                = new DbContextOptionsBuilder<SampleContext>();

            _ = dbContextOptionsBuilder.UseSqlServer($"Server=(localdb)\\MSSQLLocalDB;Database=unittestdb;Trusted_Connection=True");
            context = new SampleContext(dbContextOptionsBuilder.Options);
            await context.Database.EnsureDeletedAsync();
            await context.Database.EnsureCreatedAsync();

            System.Diagnostics.Debug.WriteLine("InitilizeAsync Called");
        }

        public async Task DisposeAsync()
        {
            await context.Database.EnsureDeletedAsync();
            System.Diagnostics.Debug.WriteLine("DisposeAsync Called");
        }

    }
  • Now if you look above test , It is more clear.

  • Surely It will not execute in parallel but more clear. Also there will not too many database.

  • InitializeAsync will get called before each test and it create new db.

  • Once test complete it called DisposeAsync and which delete db so all things properly clean up.

Conclusion

By using IAsyncLifetime, It is possible to setup state of each test method and it gives more isolation for test rather test data get corrupted or not in proper state due to order or some other issue.

There is always chance to improvement and will discuss about effectively cleaning db in next article.