Я запускаю модульные тесты на TFS в среде CI, сегодня произошел сбой со следующим исключением:
Error:
System.InvalidOperationException : Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
Stacktrace:
at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
at System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.GetDisplayName(Type type)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FindEntityType(Type type)
at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.EntityEqualityRewritingExpressionVisitor.RewriteEntityEquality(ExpressionType nodeType, Expression left, Expression right)
at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.EntityEqualityRewritingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at System.Linq.Expressions.BinaryExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at System.Linq.Expressions.ExpressionVisitor.VisitBinary(BinaryExpression node)
at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.EntityEqualityRewritingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at System.Linq.Expressions.BinaryExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Remotion.Linq.Clauses.WhereClause.TransformExpressions(Func`2 transformation)
at Remotion.Linq.QueryModel.TransformExpressions(Func`2 transformation)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryOptimizer.Optimize(QueryCompilationContext queryCompilationContext, QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.OptimizeQueryModel(QueryModel queryModel, Boolean asyncQuery)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateQueryExecutor[TResult](QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, IQueryModelGenerator queryModelGenerator, IDatabase database, IDiagnosticsLogger`1 logger, Type contextType)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass13_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at System.Linq.Queryable.Any[TSource](IQueryable`1 source)
at ModelNamespace.SomeRepository.TestMethod()
at TestNamespace.SomeRepositoryTest.TestMethod_Test()
После этого я снова запустил ту же сборку с той же фиксацией, и она прошла успешно.Странно, не правда ли?
Как создаются тесты
Для настройки InMemoryDatabase мы используем следующий вспомогательный класс:
public class DbContextTestHelper
{
public static DbContextOptions<CustomContext> PrepareData(Action<CustomContext> createData)
{
var options = CreateDbContextOptions();
SaveData(options, createData);
return options;
}
private static void SaveData(DbContextOptions<CustomContext> options, Action<CustomContext> createData)
{
// Insert seed data into the database using one instance of the context
using (var context = new CustomContext(options))
{
createData.Invoke(context);
context.SaveChanges();
}
}
private static DbContextOptions<CustomContext> CreateDbContextOptions()
{
return new DbContextOptionsBuilder<CustomContext>()
.UseInMemoryDatabase(Guid.NewGuid()
.ToString()) //Database with same name gets reused, so let's isolate the tests from each other...
.Options;
}
}
Так что теперь у нас есть многоТесты, которые мы создаем так:
public class SomeRepositoryTest
{
[Fact]
public async Task TestMethod_Test()
{
var options = DbContextTestHelper.PrepareData(context =>
{
// do some initialisation
});
// Use a clean instance of the context to run the test
using (var context = new CustomContext(options))
{
var testee = new SomeRepository(context);
await testee.TestMethod(); // InvalidOperationException from above
// Assert something
}
}
}
Конечно, во многих случаях мы создаем эти DbContextOptions<CustomContext>
, и, естественно, они работают параллельно из-за встроенной функциональности, которую обеспечивает xUnit
(да, конечноне в том же классе, но для многих тестовых классов).
Мой вопрос
Насколько я могу судить, существует несколько возможностей того, почему это может иногда вызывать исключение:
- GUID, сгенерированный для обеспечения разных экземпляровБазы данных InMemory являются дубликатами.Однако я бы предпочел не верить этому.
- в системе баз данных InMememory есть ошибка
- Мы неправильно используем базы данных InMemory
Знаете ли вы, что происходит?
Редактировать 1:
По запросу, вот TestMethod()
, где вызывается System.Linq.Queryable.Any[TSource](IQueryable'1 source)
.
public void TestMethod(SomeObject someObject, List<DateTime> dates, bool someOption)
{
var res = _dbContext.SomeSet.Where(o =>
o.SomeObject.Equals(someObject) && dates.Contains(o.DateTime) && o.SomeOption == someOption);
if (res.Any()) // at System.Linq.Queryable.Any[TSource](IQueryable`1 source)
{
_dbContext.SomeSet.RemoveRange(res);
}
}