Skip to content

Latest commit

 

History

History
700 lines (606 loc) · 21.5 KB

File metadata and controls

700 lines (606 loc) · 21.5 KB

EntityFramework Core TUnit Usage

Combines EfLocalDb, TUnit, Verify.TUnit, and Verify.EntityFramework into a test base class that provides an isolated database per test with Arrange-Act-Assert phase enforcement.

EfLocalDb.TUnit package NuGet Status

https://nuget.org/packages/EfLocalDb.TUnit/

Schema and data

The snippets use a DbContext of the following form:

public class TheDbContext(DbContextOptions options) : DbContext(options)
{
    public DbSet<Company> Companies { get; set; } = null!;
    public DbSet<Employee> Employees { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder builder)
    {
        var company = builder.Entity<Company>();
        company.HasKey(_ => _.Id);
        company.HasMany(_ => _.Employees).WithOne(_ => _.Company).IsRequired();

        var employee = builder.Entity<Employee>();
        employee.HasKey(_ => _.Id);
        employee.HasMany(_ => _.Vehicles).WithOne(_ => _.Employee).IsRequired();

        var vehicle = builder.Entity<Vehicle>();
        vehicle.HasKey(_ => _.Id);
    }
}

snippet source | anchor

public class Company
{
    public required Guid Id { get; init; }
    public required string Name { get; set; }
    public List<Employee> Employees { get; set; } = [];
}

snippet source | anchor

public class Employee
{
    public required Guid CompanyId { get; set; }
    public Company? Company { get; init; }
    public required Guid Id { get; init; }
    public required string Name { get; set; }
    public List<Vehicle> Vehicles { get; set; } = [];
}

snippet source | anchor

Initialize

LocalDbTestBase<T>.Initialize needs to be called once. This is best done in a ModuleInitializer:

public static class ModuleInitializer
{
    [ModuleInitializer]
    public static void Initialize()
    {
        VerifyDiffPlex.Initialize(OutputType.Compact);
        VerifierSettings.InitializePlugins();
        LocalDbLogging.EnableVerbose();
        LocalDbSettings.ConnectionBuilder(_ => _.ConnectTimeout = 300);
        LocalDbTestBase<TheDbContext>.Initialize();
    }
}

snippet source | anchor

Usage in a Test

Inherit from LocalDbTestBase<T> and use the ArrangeData, ActData, and AssertData properties. These enforce phase ordering: accessing ActData transitions from Arrange to Act, and accessing AssertData transitions to Assert. Accessing a phase out of order throws an exception.

public class Tests :
    LocalDbTestBase<TheDbContext>
{
    [Test]
    public async Task Simple()
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "value"
            });
        await ArrangeData.SaveChangesAsync();

        var entity = await ActData.Companies.SingleAsync();
        entity.Name = "value2";
        await ActData.SaveChangesAsync();

        var result = await AssertData.Companies.SingleAsync();
        await Verify(result);
    }

    [Test]
    public async Task StaticInstance()
    {
        Instance.ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "value"
            });
        await Instance.ArrangeData.SaveChangesAsync();

        var entity = await Instance.ActData.Companies.SingleAsync();
        entity.Name = "value2";
        await Instance.ActData.SaveChangesAsync();

        var result = await Instance.AssertData.Companies.SingleAsync();
        await Verify(result);
    }

    [Test]
    public Task Combinations()
    {
        string[] inputs = ["value1", "value2"];
        return Combination()
            .Verify(Run, inputs);

        async Task<Company> Run(string input)
        {
            ArrangeData.Companies.Add(
                new()
                {
                    Id = Guid.NewGuid(),
                    Name = "value"
                });
            await ArrangeData.SaveChangesAsync();

            var entity = await ActData.Companies.SingleAsync();
            entity.Name = input;
            await ActData.SaveChangesAsync();

            return await AssertData.Companies.SingleAsync();
        }
    }

    [Test]
    public Task Name() =>
        Verify(new
        {
            Database.Name,
            Database.Connection.DataSource
        });

    [Test]
    [Arguments("case")]
    public Task NameWithParams(string caseName) =>
        Verify(new
        {
            Database.Name,
            Database.Connection.DataSource
        });

    [Test]
    public Task ThrowForRedundantIgnoreQueryFilters() =>
        ThrowsTask(
                () =>
                {
                    var entities = AssertData.Companies;
                    return entities.IgnoreQueryFilters().SingleAsync();
                })
            .IgnoreStackTrace();

    [Test]
    public async Task IgnoreQueryFiltersAllowedOnArrangeAndAct()
    {
        await ArrangeData.Companies.IgnoreQueryFilters().ToListAsync();
        await ActData.Companies.IgnoreQueryFilters().ToListAsync();
    }

    [Test]
    public async Task ActInAsync()
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "value"
            });
        await ArrangeData.SaveChangesAsync();

        await AsyncMethod();

        var result = await AssertData.Companies.SingleAsync();
        await Verify(result);

        async Task AsyncMethod()
        {
            await Task.Delay(1);
            var entity = await ActData.Companies.SingleAsync();
            entity.Name = "value2";
            await ActData.SaveChangesAsync();
        }
    }

    [Test]
    public async Task VerifyEntityById()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        await VerifyEntity<Company>(company.Id);
    }

    [Test]
    public async Task VerifyEntityByIdNull() =>
        await VerifyEntity<Company>(Guid.NewGuid());

    [Test]
    public async Task VerifyEntityWithInclude()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "the Company"
        };
        var employee = new Employee
        {
            Id = Guid.NewGuid(),
            CompanyId = company.Id,
            Name = "the Employee"
        };
        ArrangeData.AddRange(company, employee);
        await ArrangeData.SaveChangesAsync();
        await VerifyEntity<Company>(company.Id)
            .Include(_ => _.Employees);
    }

    [Test]
    public async Task VerifyEntityWithThenInclude()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "the Company"
        };
        var employee = new Employee
        {
            Id = Guid.NewGuid(),
            CompanyId = company.Id,
            Name = "the Employee"
        };
        var vehicle = new Vehicle
        {
            Id = Guid.NewGuid(),
            EmployeeId = employee.Id,
            Model = "the Vehicle"
        };
        ArrangeData.AddRange(company, employee, vehicle);
        await ArrangeData.SaveChangesAsync();
        await VerifyEntity<Company>(company.Id)
            .Include(_ => _.Employees)
            .ThenInclude(_ => _.Vehicles);
    }

    [Test]
    public async Task VerifyEntities_DbSet()
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "value"
            });
        await ArrangeData.SaveChangesAsync();
        await VerifyEntities(AssertData.Companies);
    }

    [Test]
    public async Task VerifyEntities_Queryable()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        await VerifyEntities(AssertData.Companies.Where(_ => _.Id == company.Id));
    }

    [Test]
    public async Task VerifyEntity_Queryable()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        await VerifyEntity(AssertData.Companies.Where(_ => _.Id == company.Id));
    }

    [Test]
    public async Task ArrangeQueryableAfterAct()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        var queryable = ArrangeData.Companies.Where(_ => _.Id == company.Id);
        // ReSharper disable once UnusedVariable
        var act = ActData;
        await ThrowsTask(() => VerifyEntities(queryable))
            .IgnoreStackTrace()
            .DisableRequireUniquePrefix();
    }

    [Test]
    public Task AccessActAfterAssert()
    {
        // ReSharper disable once UnusedVariable
        var assert = AssertData;
        return Throws(() => ActData)
            .IgnoreStackTrace();
    }

    [Test]
    public async Task ActQueryableAfterAssert()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        var queryable = ActData.Companies.Where(_ => _.Id == company.Id);
        // ReSharper disable once UnusedVariable
        var assert = AssertData;
        await ThrowsTask(() => VerifyEntities(queryable))
            .IgnoreStackTrace()
            .DisableRequireUniquePrefix();
    }

    [Test]
    public async Task ArrangeQueryableAfterAssert()
    {
        var company = new Company
        {
            Id = Guid.NewGuid(),
            Name = "value"
        };
        ArrangeData.Companies.Add(company);
        await ArrangeData.SaveChangesAsync();
        var queryable = ArrangeData.Companies.Where(_ => _.Id == company.Id);
        // ReSharper disable once UnusedVariable
        var assert = AssertData;
        await ThrowsTask(() => VerifyEntities(queryable))
            .IgnoreStackTrace()
            .DisableRequireUniquePrefix();
    }

    [Test]
    public Task AccessArrangeAfterAssert()
    {
        // ReSharper disable once UnusedVariable
        var assert = AssertData;
        return Throws(() => ArrangeData)
            .IgnoreStackTrace();
    }

    [Test]
    public Task AccessArrangeAfterAct()
    {
        // ReSharper disable once UnusedVariable
        var act = ActData;
        return Throws(() => ArrangeData)
            .IgnoreStackTrace();
    }
}

snippet source | anchor

Static Instance

The current test instance can be accessed via LocalDbTestBase<T>.Instance. This is useful when test helpers need to access the database outside the test class:

[Test]
public async Task StaticInstance()
{
    Instance.ArrangeData.Companies.Add(
        new()
        {
            Id = Guid.NewGuid(),
            Name = "value"
        });
    await Instance.ArrangeData.SaveChangesAsync();

    var entity = await Instance.ActData.Companies.SingleAsync();
    entity.Name = "value2";
    await Instance.ActData.SaveChangesAsync();

    var result = await Instance.AssertData.Companies.SingleAsync();
    await Verify(result);
}

snippet source | anchor

Combinations

Verify Combinations are supported. The database is reset for each combination:

[Test]
public Task Combinations()
{
    string[] inputs = ["value1", "value2"];
    return Combination()
        .Verify(Run, inputs);

    async Task<Company> Run(string input)
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "value"
            });
        await ArrangeData.SaveChangesAsync();

        var entity = await ActData.Companies.SingleAsync();
        entity.Name = input;
        await ActData.SaveChangesAsync();

        return await AssertData.Companies.SingleAsync();
    }
}

snippet source | anchor

VerifyEntity

Helpers for verifying entities by primary key, with optional Include/ThenInclude:

[Test]
public async Task VerifyEntityById()
{
    var company = new Company
    {
        Id = Guid.NewGuid(),
        Name = "value"
    };
    ArrangeData.Companies.Add(company);
    await ArrangeData.SaveChangesAsync();
    await VerifyEntity<Company>(company.Id);
}

snippet source | anchor

[Test]
public async Task VerifyEntityWithInclude()
{
    var company = new Company
    {
        Id = Guid.NewGuid(),
        Name = "the Company"
    };
    var employee = new Employee
    {
        Id = Guid.NewGuid(),
        CompanyId = company.Id,
        Name = "the Employee"
    };
    ArrangeData.AddRange(company, employee);
    await ArrangeData.SaveChangesAsync();
    await VerifyEntity<Company>(company.Id)
        .Include(_ => _.Employees);
}

snippet source | anchor

[Test]
public async Task VerifyEntityWithThenInclude()
{
    var company = new Company
    {
        Id = Guid.NewGuid(),
        Name = "the Company"
    };
    var employee = new Employee
    {
        Id = Guid.NewGuid(),
        CompanyId = company.Id,
        Name = "the Employee"
    };
    var vehicle = new Vehicle
    {
        Id = Guid.NewGuid(),
        EmployeeId = employee.Id,
        Model = "the Vehicle"
    };
    ArrangeData.AddRange(company, employee, vehicle);
    await ArrangeData.SaveChangesAsync();
    await VerifyEntity<Company>(company.Id)
        .Include(_ => _.Employees)
        .ThenInclude(_ => _.Vehicles);
}

snippet source | anchor

VerifyEntities

Verify a collection of entities from a DbSet or IQueryable:

[Test]
public async Task VerifyEntities_DbSet()
{
    ArrangeData.Companies.Add(
        new()
        {
            Id = Guid.NewGuid(),
            Name = "value"
        });
    await ArrangeData.SaveChangesAsync();
    await VerifyEntities(AssertData.Companies);
}

snippet source | anchor

[Test]
public async Task VerifyEntity_Queryable()
{
    var company = new Company
    {
        Id = Guid.NewGuid(),
        Name = "value"
    };
    ArrangeData.Companies.Add(company);
    await ArrangeData.SaveChangesAsync();
    await VerifyEntity(AssertData.Companies.Where(_ => _.Id == company.Id));
}

snippet source | anchor

SharedDb

Mark test methods with [SharedDb] to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data.

Use [SharedDbWithTransaction] instead when tests need to write data. Each test runs inside an auto-rolling-back transaction, ensuring test isolation while still sharing the database instance.

Note: [SharedDbWithTransaction] means that on test failure the resulting database cannot be inspected (since the transaction is rolled back). A workaround when debugging a failure is to temporarily remove the attribute.

Both attributes can be mixed in the same test fixture:

public class SharedDbTests : LocalDbTestBase<TheDbContext>
{
    [Test]
    [SharedDb]
    public async Task ReadFromSharedDb()
    {
        var count = await ActData.Companies.CountAsync();
        await Assert.That(count).IsEqualTo(0);
    }

    [Test]
    [SharedDbWithTransaction]
    public async Task CanReadAndWrite()
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "SharedDbWithTransaction Company"
            });
        await ArrangeData.SaveChangesAsync();

        var entity = await ActData.Companies.SingleAsync();
        await Assert.That(entity.Name)
            .IsEqualTo("SharedDbWithTransaction Company");
    }

    [Test]
    [SharedDbWithTransaction]
    public async Task DataIsRolledBack()
    {
        ArrangeData.Companies.Add(
            new()
            {
                Id = Guid.NewGuid(),
                Name = "Should Not Persist"
            });
        await ArrangeData.SaveChangesAsync();

        var count = await ActData.Companies.CountAsync();
        await Assert.That(count).IsEqualTo(1);
    }

    [Test]
    [SharedDbWithTransaction]
    public async Task StartsWithEmptyDatabase()
    {
        var count = await ActData.Companies.CountAsync();
        await Assert.That(count).IsEqualTo(0);
    }
}

snippet source | anchor

Parallel Execution

To run tests in parallel, configure parallelism at the assembly level:

using TUnit.Core.Interfaces;

[assembly: ParallelLimiter<ParallelLimit2>]

public class ParallelLimit2 : IParallelLimit
{
    public int Limit => 2;
}

snippet source | anchor