使用 xUnit 动态测试所有 Entity Framework 核心 DbContext DbSet<> 对象

Dynamically Test All Entity Framework Core DbContext DbSet<> objects with xUnit

我正在使用 xUnit 和 FluentAssertions 编写集成测试来验证我们的模型是否正确映射。我们有几十个 EF 上下文,每个上下文都有一个或多个 DbSet<> 属性,如下所示:

public class SomeContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder builder) { // ... }

    public virtual DbSet<User> Users { get; set; }
}

它们都以相同的方式布局,因此这将是使用 [MemberData] 动态获取每个上下文作为测试输入的理想选择,DbSet<> 属性,调用它,并确保它不会在执行简单查询时失败。 Container.GetInstance() 调用是对我的 DI 容器的调用,以获取实际的 DbContext 对象:

public class ContextTests
{
    public static IEnumerable<object[]> EntitiesMappedInCode =>
        typeof(DbContextInitializer).Assembly.GetTypes()
            .Where(t => t.IsSubclassOf(typeof(DbContext)) && !t.IsAbstract)
            .SelectMany(dbContextType =>
            {
                var context = (DbContext) Container.GetInstance(dbContextType);
                var entities = context.Model.GetEntityTypes();
                var dbSetProperties = context.GetType().GetProperties().Where(p =>
                    typeof(DbSet<>).IsAssignableFrom(p.PropertyType.GetGenericTypeDefinition()));
                
                return dbSetProperties.Select(dbSet => new [] {context, dbSet.GetValue(context)});
            });


    [Theory]
    [MemberData(nameof(EntitiesMappedInCode))]
    public void EntitiesDefinedInCode_ExistsInDatabase(DbContext context, object dbSetObject)
    {
        var dbSet = dbSetObject as DbSet<dynamic>; 
        dbSet.Invoking(dbSet => Queryable.FirstOrDefault<dynamic>(dbSet))
            .Should().NotThrow<SqlException>("the entity framework model should match the database");
    }
}

问题是反射无法正常返回运行时实例,并在 p.PropertyType.GetGenericTypeDefinition() 处失败并出现错误。

有没有人动态检索上下文的 DbSet<> 属性并成功调用它们的查询?

抛出异常是因为 GetGenericTypeDefinition() 不适用于非泛型类型,因此您应该在调用它之前首先检查类型是否真的是泛型:

var dbSetProps = typeof(HwContext).GetProperties().Where(c => 
        c.PropertyType.IsGenericType && 
        typeof(DbSet<>).IsAssignableFrom(c.PropertyType.GetGenericTypeDefinition()));

也不要转换为 DbSet<dynamic>,只需使用 dynamic:

foreach (var prop in dbSetProps)
{
    dynamic dbSet = prop.GetValue(context);
    // this should not throw
    Queryable.FirstOrDefault(dbSet);                
}

多亏了 Evk,我才能让它像这样工作:

public class ContextTests
{
    public static IEnumerable<object[]> EntitiesMappedInCode =>
        typeof(DbContextInitializer).Assembly.GetTypes()
            .Where(t => t.IsSubclassOf(typeof(DbContext)) && !t.IsAbstract)
            .SelectMany(dbContextType =>
            {
                var dbSetProps = dbContextType.GetProperties()
                    .Where(c => c.PropertyType.IsGenericType 
                                && typeof(DbSet<>).IsAssignableFrom(c.PropertyType.GetGenericTypeDefinition()));
                var context = Container.GetInstance(dbContextType);
                return dbSetProps.Select(dbSetProp => new [] {context, dbSetProp.GetValue(context)});
            });


    [Theory]
    [MemberData(nameof(EntitiesMappedInCode))]
    public void EntitiesDefinedInCode_ExistsInDatabase(DbContext context, dynamic dbSet)
    {
        context.Invoking(dbContext => Queryable.FirstOrDefault(dbSet))
            .Should().NotThrow<SqlException>("the entity framework model should match the database");
    }
}