-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Parallell xunit tests fail using sqlite, works when running sequentially #21633
Description
Disclamer it is not yet clear to me if my issue is with sqlite, efcore, xunit, the Microsoft.Data.Sqlite driver or my Query filters.
My goals
I want to create integration tests using xunit where I:
- Spin up a in memory Sqlite based db context per class
- Run tests in parallell
- Tests within the same class share db context and data
- Different classes gets their own db contexts and should not bother eachother
Expected behaviour
That tests between different classes do not affect each others contexts.
Actual behaviour
The behavior I am seeing is that running tests from two classes fails (either tests from class one fails or tests from class 2 fails). But if I run them sequentially one by one, they pass. Some kind of leak is happening.
The SqlLiteConnection is not thread safe according to an answer in (2) but as far as I understand xunit and class fixtures each DbFixture should get their own SqlConnection and each DbFixture should only be instantiated once per class.
I have tried
- using ClassFixtures
- using CollectionFixtures with name
- setting names on the sqlite inmemory connection (and verified that the tests indeed uses different connection strings)
But the problem remains.
To Reproduce
-
Create database fixture classes according to xunit documentation
public class DefaultDatabaseFixture : DatabaseFixture { // The default DefaultDatabaseFixture initializes a db with query filters enabled. public DefaultDatabaseFixture() : base(nameof(DefaultDatabaseFixture), true, this.TestUserContext) { } } public class DisabledQueryFilterDatabaseFixture : DatabaseFixture { // The DisabledQueryFilterDatabaseFixture fixture initializes a db with query filters disabled public DisabledQueryFilterDatabaseFixture() : base(nameof(DisabledQueryFilterDatabaseFixture), false, this.TestUserContext) { } } public abstract class DatabaseFixture : IDisposable { // Using a connection string with a named database per fixture in order to try to use Sqlites memory private const string InMemoryConnectionString = "DataSource={0};Mode=Memory;cache=shared"; private SqliteConnection connection; /// <summary> /// This db context is initialized once per class and shared between /// the tests within the same class. /// <see cref="https://xunit.net/docs/shared-context"/> /// </summary> public BokaMeraContext SharedDbContext { get; private set; } public DatabaseFixture(string databaseName, bool enableQueryFilters, IUserContext userContext) { var connectionString = string.Format(InMemoryConnectionString, databaseName); this.connection = new SqliteConnection(connectionString); this.connection.Open(); var options = new DbContextOptionsBuilder<BokaMeraContext>() .UseLazyLoadingProxies() .UseSqlite(this.connection) .Options; this.SharedDbContext = new BokaMeraContext(options, enableQueryFilters, userContext); this.SharedDbContext.Database.EnsureCreated(); } protected virtual void Dispose(bool disposing) { if (disposing) { this.SharedDbContext?.Dispose(); this.connection?.Dispose(); } } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } }
-
Create two test fixtures that uses different database fixtures
public class DisabledQueryFiltersTests : TestBase, IClassFixture<DisabledQueryFilterDatabaseFixture> { private readonly DisabledQueryFilterDatabaseFixture databaseFixture; public DisabledQueryFiltersTests(ITestOutputHelper testOutputHelper, DisabledQueryFilterDatabaseFixture databaseFixture) : base( testOutputHelper) { this.databaseFixture = databaseFixture; } [Fact] public void DbContextWithDisabledQueryFiltersShouldReturnValues() { // Given var context = this.databaseFixture.SharedDbContext; // When var owningBookingSupplier = new BookingSupplier {Id = Guid.NewGuid()}; var service = new Service {Id = Guid.NewGuid(), Owner = owningBookingSupplier}; context.Service.Add(service); context.SaveChanges(); var result = context.Service.SingleOrDefault(s => s.Id == service.Id); // Then result.Should().BeEquivalentTo(service, "the service should be returned even if the current UserContext does not own the BookingSupplier since the QueryFilters are disabled"); } }
public class SecurityQueriesTests : TestBase, IClassFixture<DefaultDatabaseFixture> { private readonly DefaultDatabaseFixture databaseFixture; public SecurityQueriesTests(ITestOutputHelper testOutputHelper, DefaultDatabaseFixture databaseFixture) : base( testOutputHelper) { this.databaseFixture = databaseFixture; } [Fact] public void DbContextWithoutUserContextShouldNotReturnValues() { // Given var context = this.databaseFixture.SharedDbContext; var owningBookingSupplier = new BookingSupplier(); // When context.Service.Add(new Service {Owner = owningBookingSupplier}); context.SaveChanges(); var result = context.Service.SingleOrDefault(s => s.Id == owningBookingSupplier.Id); // Then result.Should().BeNull(); } }
-
Run the tests in parallell on a multi core cpu. One of the tests fail. Example:
DbContextWithDisabledQueryFiltersShouldReturnValuesfails on the last assertion because it cannot find the service that was added and saved on the rows above. Re-running the test makes it pass. IF the context in this case would be a context with query filters enabled, it would have been correct that the service should not be found. Therefore I suspect that the context from the other fixture leaks into this test somehow.
Additional context
I am using a xeon cpu with 12 cores. I have noticed in other situations that if I use a machine with less cores I have less parallell issues in general (naturally). I mention it because it can be troublesome to replicate my issues if you don´t have multiple cores to run on on you machine.
BokaMeraContext
Relevant parts:
public partial class BokaMeraContext : DbContext
{
public IUserContext UserContext { get; set; }
public bool EnableQueryFilters { get; }
public BokaMeraContext(DbContextOptions<BokaMeraContext> options, bool enableQueryFilters) : base(options)
{
this.EnableQueryFilters = enableQueryFilters;
this.UserContext = DefaultUserContext;
}
public BokaMeraContext(DbContextOptions<BokaMeraContext> options, bool enableQueryFilters,
IUserContext userContext) : base(options)
{
this.EnableQueryFilters = enableQueryFilters;
this.UserContext = userContext;
}
// call AddOwnershipQueryFilter for relevant entities here
private void AddOwnershipQueryFilter<T>(EntityTypeBuilder<T> builder) where T : class, IOwnedEntity
{
// If security filters are explicitly disabled, return all rows
if (this.EnableQueryFilters)
{
// If no user context is set, no owned rows should be returned
builder.HasQueryFilter(x => this.UserContext.OwnedBookingSuppliers.Contains(x.Owner.Id) );
}
}
// ...more below
}xunit.runner.json
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"shadowCopy": false,
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": 0
}"maxParallelThreads": 0 means that xunit tries to utilize the default value (which is to use the number of cpu cores, in my case 12). See configuring xunit
Links for reading
- https://xunit.net/docs/shared-context
- https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
- https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#using-sqlite-in-memory-databases
- https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/
Component versions
| Component | Version |
|---|---|
| Microsoft.NET.Test.Sdk | 16.6.1 |
| xunit | 2.4.1 |
| xunit.runner.visualstudio | 2.4.1 |
| Microsoft.EntityFrameworkCore | 3.1.5 |
| Microsoft.EntityFrameworkCore.Proxies | 3.1.5 |
| Microsoft.EntityFrameworkCore.Tools | 3.1.5 |
| Microsoft.EntityFrameworkCore.Sqlite | 3.1.5 |
| Microsoft.EntityFrameworkCore.SqlServer | 3.1.5 |
| Microsoft.Data.Sqlite.Core | 3.1.5 |
| Target framework | netcoreapp3.1 |
| Dotnet core version | 3.1.301 |
| Operating system | Windows 10 Pro, Build 2004 |