如何在xUnit中Mock数据库?DbContext模拟失败求助
我的DbContext代码
public class OrganizerDbContext : IdentityDbContext<AppUser> { public OrganizerDbContext(DbContextOptions<OrganizerDbContext> options) : base(options) { } // list of dbSet<> here protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<AppUser>() .HasOne(a => a.Accesses) .WithOne(a => a.User) .HasForeignKey<UserAccess>(a => a.UserId); builder.Entity<AppUser>() .HasOne(a=>a.EmploymentStatus) .WithOne(a=>a.User) .HasForeignKey<EmploymentStatus>(a => a.UserId); builder.Entity<EmploymentStatus>() .HasOne(a=>a.Facility) .WithMany(a=>a.Employments) .HasForeignKey(a => a.FacilityId); } }
测试代码(xUnit + FakeItEasy)
[Fact] public async Task AnnouncerIndex_Test_1() { // Arrange var fakeContext = A.Fake<OrganizerDbContext>(); var fakeUserManager = A.Fake<UserManager<AppUser>>(); var controller = new AnnouncementsController(fakeContext, fakeUserManager); // Act var result = await controller.IsUserBlockedFromAccesingAnnouncer(); // Assert Assert.False(result); }
第一次测试失败报错
Message:
FakeItEasy.Core.FakeCreationException :
Failed to create fake of type Organizer3.Data.OrganizerDbContext: Below is a list of reasons for failure per attempted constructor:
Constructor with signature (Microsoft.EntityFrameworkCore.DbContextOptions1[Organizer3.Data.OrganizerDbContext]) failed: No constructor matches the passed arguments for constructor. An exception of type System.InvalidOperationException was caught during this call. Its message was: The DbContextOptions passed to the OrganizerDbContextProxy constructor must be a DbContextOptions<OrganizerDbContextProxy>. When registering multiple DbContext types, make sure that the constructor for each context type has a DbContextOptions<TContext> parameter rather than a non-generic DbContextOptions parameter. at Microsoft.EntityFrameworkCore.DbContext..ctor(DbContextOptions options) at Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserContext5..ctor(DbContextOptions options)
at Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext8..ctor(DbContextOptions options) at Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext3..ctor(DbContextOptions options)
at Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext1..ctor(DbContextOptions options) at Organizer3.Data.OrganizerDbContext..ctor(DbContextOptions1 options) in C:\Users\Smokiel\Desktop\Organizer3\Organizer3\Areas\Identity\Data\OrganizerDbContext.cs:line 15
at Castle.Proxies.OrganizerDbContextProxy..ctor(IInterceptor[] , DbContextOptions`1 options)Stack Trace:
FailedCreationResult.get_Result() line 82
FakeAndDummyManager.CreateFake(Type typeOfFake, Action`1 optionsBuilder, LoopDetectingResolutionContext resolutionContext) line 42
FakeAndDummyManager.CreateFake(Type typeOfFake, LoopDetectingResolutionContext resolutionContext) line 28
A.FakeT line 31
AnnouncementsControllerTests.AnnouncerIndex_Test_1() line 28
--- End of stack trace from previous location ---
添加无参构造函数后的报错
Message: System.ArgumentException : Object 'Organizer3.Controllers.AnnouncementsController' of type Organizer3.Controllers.AnnouncementsController is not recognized as a fake object. Stack Trace: DefaultFakeManagerAccessor.GetFakeManager(Object proxy) line 28 Fake.GetFakeManager(Object fakedObject) line 28 FakeConfigurationManager.GuardAgainstNonFake(Object target) line 83 FakeConfigurationManager.CallTo[T](Expression
1 callSpecification) line 45 A.CallTo[T](Expression1 callSpecification) line 161 AnnouncementsControllerTests.AnnouncerIndex_Test_1() line 34 --- End of stack trace from previous location ---
解决方案
方案1:使用内存数据库创建真实DbContext(推荐,无代理问题)
如果只是需要一个不执行真实数据库操作的dummy上下文,用EF Core内存数据库创建真实实例是最简单的方案,完全满足需求且不会有FakeItEasy的代理类型冲突问题:
[Fact] public async Task AnnouncerIndex_Test_1() { // Arrange // 配置内存数据库选项,用唯一名称避免测试间干扰 var dbOptions = new DbContextOptionsBuilder<OrganizerDbContext>() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; // 创建真实的DbContext实例(内存数据库,不会影响真实数据) var context = new OrganizerDbContext(dbOptions); var fakeUserManager = A.Fake<UserManager<AppUser>>(); var controller = new AnnouncementsController(context, fakeUserManager); // Act var result = await controller.IsUserBlockedFromAccesingAnnouncer(); // Assert Assert.False(result); }
方案2:正确配置FakeItEasy创建DbContext假对象
如果一定要用FakeItEasy,需要手动传入匹配的DbContextOptions<OrganizerDbContext>作为构造参数,解决代理类型不匹配的问题:
[Fact] public async Task AnnouncerIndex_Test_1() { // Arrange var dbOptions = new DbContextOptionsBuilder<OrganizerDbContext>().Options; // 传入构造参数创建假对象 var fakeContext = A.Fake<OrganizerDbContext>(dbOptions); var fakeUserManager = A.Fake<UserManager<AppUser>>(); var controller = new AnnouncementsController(fakeContext, fakeUserManager); // Act var result = await controller.IsUserBlockedFromAccesingAnnouncer(); // Assert Assert.False(result); }
错误原因说明
- 第一次报错:FakeItEasy创建DbContext代理时会生成子类
OrganizerDbContextProxy,但EF Core要求DbContextOptions<T>的泛型参数必须和上下文类型严格匹配,导致传入的DbContextOptions<OrganizerDbContext>不被代理类接受。 - 添加无参构造函数后的报错:大概率是测试代码中存在
A.CallTo(controller)...这类针对控制器的FakeItEasy调用,但控制器不是假对象,所以报错。如果只是需要dummy上下文,方案1的内存数据库实例完全够用,无需创建假对象。
内容的提问来源于stack exchange,提问作者Samo

