【问题标题】:Unit testing EF Core using in-memory database with an eager-loaded function使用带有预加载函数的内存数据库对 EF Core 进行单元测试
【发布时间】:2019-08-12 22:56:13
【问题描述】:

我正在为我的 Web API 编写单元测试,除非删除包含(从方法中急切加载),否则无法通过测试。我正在使用内存数据库提供dbcontext,但无法弄清楚它为什么不返回任何数据。提前感谢任何帮助或建设性的批评

这是我要测试的方法。
注意:如果我注释掉 .include 语句,它通过测试。

    public async Task<LibraryAsset> GetAsset(int assetId)
    {
        var asset = await _context.LibraryAssets
            .Include(p => p.Photo)
            .Include(p => p.Category)
            .Include(a => a.AssetType)
            .Include(s => s.Status)
            .Include(s => s.Author)
            .FirstOrDefaultAsync(x => x.Id == assetId);

        return asset;
    }

这是使用 inMemory DB 的基础 DbContext

    public DataContext GetDbContext()
    {
        var builder = new DbContextOptionsBuilder<DataContext>();

        if (useSqlite)
        {
            // Use Sqlite DB.
            builder.UseSqlite("DataSource=:memory:", x => { });
        }
        else
        {
            // Use In-Memory DB.
            builder.UseInMemoryDatabase(Guid.NewGuid().ToString());
        }

        var DataContext = new DataContext(builder.Options);

        if (useSqlite)
        {
            // SQLite needs to open connection to the DB.
            // Not required for in-memory-database and MS SQL.
            DataContext.Database.OpenConnection();
        }

        DataContext.Database.EnsureCreated();

        return DataContext;
    }

这是测试:

    [Fact]
    public async void GetAssetById_ExistingAsset_ReturnAsset()
    {
        using (var context = GetDbContext())
        {
            ILogger<LibraryAssetService> logger = new 
            NullLogger<LibraryAssetService>();

            var service = new LibraryAssetService(context, _logger);

            var asset = new LibraryAsset
            {
                Id = 40,
                NumberOfCopies = 20,
                Title = "",
                Year = 1992,
                Status = new Status { Id = 1 },
                AssetType = new AssetType { Id = 1 },
                Author = new Author { Id = 1 },
                Category = new Category { Id = 2 },
                Photo = new AssetPhoto { Id = 1 }
            };

            context.LibraryAssets.Attach(asset);

            context.Add(asset);
            context.SaveChanges();

            var actual = await service.GetAsset(40);
            Assert.Equal(40, actual.Id);
        }
    }

这是我第一次编写单元测试,我基本上是边学边学。请随时指出您可能注意到的任何其他错误。

【问题讨论】:

  • 您是否收到任何错误或意外结果?我对您的代码进行了测试,但未能重现您的问题。与我们分享一个可以重现您的问题的演示。

标签: c# unit-testing asp.net-core entity-framework-core xunit


【解决方案1】:

您的代码存在一些问题:

  1. 如果您的真实数据库是关系数据库,请避免使用UseInMemoryDatabase 数据库进行测试,因为它不支持关系行为。
  2. 将 Arrange 上下文与 Act 上下文分开。这意味着,创建一个新的 DataContext 用于准备测试、添加测试数据等,并为 SUT 创建另一个(在本例中为LibraryAssetService)。 DbContext 存储本地数据(在内存中),这些数据可能不存在于数据库中,并且在某些情况下可能会显示虚假的绿色测试!
  3. 添加资产时不需要Attach。这可能会使用 sqlite 创建Foreign key constraint 错误。

为了简单起见,我删除了一些导航和参数。所以让我们假设LibraryAssetService 是这样的:

public class LibraryAssetService
{
  public LibraryAssetService(DataContext context)
  {
     _context = context;
  }

  private readonly DataContext _context;

  public async Task<LibraryAsset> GetAsset(int assetId)
  {
     var asset = await _context.LibraryAssets
        .Include(p => p.Photo)
        .Include(s => s.Author)
        .FirstOrDefaultAsync(x => x.Id == assetId);

     return asset;
  }
}

测试类:

public class LibraryAssetServiceTests
{
  public LibraryAssetServiceTests()
  {
     _factory = new TestDataContextFactory();
  }

  private TestDataContextFactory _factory;

  [Fact]
  public async void GetAssetById_ExistingAsset_ReturnAsset()
  {
     // Arrange
     using (var context = _factory.Create())
     {
        var asset = new LibraryAsset
        {
           Id = 40,
           Author = new Author { Id = 1 },
           Photo = new Photo { Id = 1 }
        };

        context.Add(asset);
        context.SaveChanges();
     }

     // Act
     using (var context = _factory.Create())
     {
        var service = new LibraryAssetService(context);
        var actual = await service.GetAsset(40);

        // Assert
        Assert.Equal(40, actual.Id);
        Assert.Equal(1, actual.Author.Id);
        Assert.Equal(1, actual.Photo.Id);
     }

  }
}

最后,为您的测试准备DataContext 的小助手类。在测试类之外提取这些东西是一种很好的做法。 在使用 sqlite 内存数据库进行测试时要记住的重要事情是,您应该在测试期间保持连接打开。无论您创建多少 DbContext 实例。 xUnit 为每个测试方法创建一个测试类的实例。因此,将为每个测试创建一个 TestDataContextFactory 的实例,您可以开始了。

public class TestDataContextFactory
{
  public TestDataContextFactory()
  {
     var builder = new DbContextOptionsBuilder<DataContext>();
     var connection = new SqliteConnection("DataSource=:memory:");
     connection.Open();
     builder.UseSqlite(connection);

     using (var ctx = new DataContext(builder.Options))
     {
        ctx.Database.EnsureCreated();
     }

     _options = builder.Options;
  }

  private readonly DbContextOptions _options;

  public DataContext Create() => new DataContext(_options);
}

【讨论】:

  • 使用您的代码后出现外键限制错误。我尝试添加 suppressForeignkeyenforcement 但这仍然没有帮助
  • 创建一个github repo,我会检查它。该代码在我的机器上运行正常。确保您没有使用Attach() 方法。
  • 这是github repo的链接link
  • @lawrencefejokwu 在LibraryAssetService 的第 34 行设置断点并观察您的 asset 对象。如您所见,外键值有很多0 值,例如StatusIdAssetTypeIdAuthorId。这就是发生外键限制的原因。 ID为0的数据库中没有作者!我注释掉了LibraryAsset中的所有外键,并让测试用Sqlite运行,它是绿色的!确保使用所需的任何内容初始化资产对象,一切顺利。
  • @lawrencefejokwu 还有一件事:我检查了DataContext 类,您没有配置资产实体。为 EF 定义实体关系,这可以使您的实体更小更清晰。并注意OnModelCreating 方法中的播种。它可能会导致性能问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-10-12
  • 1970-01-01
  • 1970-01-01
  • 2020-11-01
  • 1970-01-01
  • 2017-10-20
  • 1970-01-01
相关资源
最近更新 更多