You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Xunit与Moq时无法实例化EntityEntry代理类的问题

单元测试中EntityEntry实例化错误排查与解决

错误信息

System.ArgumentException: '无法实例化类的代理:Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry`1[[Main.Domain.Contact, PublicKeyToken=null]]。找不到无参数构造函数。(参数 'constructorArguments')'

错误提示指向富领域类Contact,但该类已包含无参和带参两个公共构造函数,且拥有Update方法。尝试过网络上的Mock方案,均未解决问题。

相关代码

Contact类带参构造函数

public Contact(
    Guid franchiseId,
    string firstName,
    string? middleName,
    string lastName,
    bool doNotContact,
    string? emailAddress,
    IEnumerable<ContactGroupContact>? contactGroupContacts = null,
    ICollection<ContactNote> contactNotes = null
)

Contact类无参构造函数

public Contact()
{
    ContactGroupContacts = new HashSet<ContactGroupContact>();
    ContactTags = new HashSet<ContactTag>();
    Messages = new HashSet<Message>();
}

Contact类Update方法

public override void Update(Contact entity)
{
    Initialize(this);

    this.FirstName = entity.FirstName;
    this.LastName = entity.LastName;
    this.CalledName = entity.CalledName;
    this.DoNotContact = entity.DoNotContact;
    this.EmailAddress = entity.EmailAddress;
    this.MiddleName = entity.MiddleName;
    this.MobileNumber = entity.MobileNumber;
    // 省略其他属性赋值逻辑
}

Contact类还包含ContactNotes和ContactGroup的ICollection属性,推测与阴影属性、级联删除相关。

原单元测试代码

[Fact]
public void Update_ShouldCallModifyUpdatedEntity()
{
    //Arrange
    Contact contact = A.Fake<Contact>();
    contact.UniqueId = Guid.NewGuid();
    contact.LastModifiedDate = DateTime.MinValue;

    Contact contactTbu = A.Fake<Contact>();
    contactTbu.UniqueId = contact.UniqueId;
    contactTbu.Address1 = "Test Address";
    #region Utils
    var internalEntityEntry = new InternalEntityEntry(
        new Mock<IStateManager>().Object,
        new RuntimeEntityType("T", typeof(Contact), false, null, null, null, ChangeTrackingStrategy.Snapshot, null, false),
        contact);
    var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
    optionsBuilder.UseInMemoryDatabase("InMemoryDbForTests");
    #endregion

    var _dbContext = new Mock<DbContext>(optionsBuilder.Options);
    var _dbSet = new Mock<DbSet<Contact>>();


    _dbContext.Setup(x => x.Set<Contact>().AsQueryable()).Returns(_dbSet.Object);
    _dbContext.Setup(x => x.Set<Contact>()).Returns(_dbSet.Object);
    _dbContext.Setup(x => x.Set<Contact>().Find(contact.UniqueId)).Returns(contact);
    _dbContext.Setup(x => x.Entry(It.IsAny<Contact>())).Returns(new EntityEntry<Contact>(internalEntityEntry));
    _dbContext.Setup(x => x.Entry(contact).CurrentValues.SetValues(contactTbu));//出错位置
    _dbSet.Setup(x => x.Find(It.IsAny<Contact>()))
        .Returns(contact);

    //Act
    _sut = new Repository<Contact>(_dbContext.Object);
    _sut.Update(contact);

    //Assert
    _dbContext.Verify(x=>x.Entry(contact));
    _dbContext.Verify(x => x.Entry(It.IsAny<Contact>()).CurrentValues.SetValues(contact));
}

问题分析

错误的根源不是Contact类,而是EntityEntry<T>本身没有公开的无参构造函数,且该类的实例化依赖EF Core内部的InternalEntityEntry等组件。手动通过new EntityEntry<Contact>(internalEntityEntry)创建实例后,Moq在处理Mock设置时会尝试为该对象创建代理,由于EntityEntry<T>的构造限制,导致代理创建失败,抛出找不到无参构造函数的错误。

解决方案

通过Moq直接MockEntityEntry<Contact>和PropertyValues对象,避免手动实例化EF内部类,调整后的单元测试代码如下:

[Fact]
public void Update_ShouldCallModifyUpdatedEntity()
{
    //Arrange
    var contact = A.Fake<Contact>();
    contact.UniqueId = Guid.NewGuid();
    contact.LastModifiedDate = DateTime.MinValue;

    var contactTbu = A.Fake<Contact>();
    contactTbu.UniqueId = contact.UniqueId;
    contactTbu.Address1 = "Test Address";

    // Mock EntityEntry及其CurrentValues属性
    var mockEntityEntry = new Mock<EntityEntry<Contact>>();
    var mockPropertyValues = new Mock<PropertyValues>();
    mockEntityEntry.Setup(entry => entry.CurrentValues).Returns(mockPropertyValues.Object);

    var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
    optionsBuilder.UseInMemoryDatabase("InMemoryDbForTests");

    var _dbContext = new Mock<DbContext>(optionsBuilder.Options);
    var _dbSet = new Mock<DbSet<Contact>>();

    // 基础DbSet相关设置
    _dbContext.Setup(x => x.Set<Contact>()).Returns(_dbSet.Object);
    _dbContext.Setup(x => x.Set<Contact>().Find(contact.UniqueId)).Returns(contact);
    // 让DbContext的Entry方法返回Mock的EntityEntry
    _dbContext.Setup(x => x.Entry(It.IsAny<Contact>())).Returns(mockEntityEntry.Object);

    //Act
    _sut = new Repository<Contact>(_dbContext.Object);
    _sut.Update(contact);

    //Assert
    // 验证Entry方法被调用
    _dbContext.Verify(x => x.Entry(contact), Times.Once);
    // 验证SetValues方法被调用
    mockPropertyValues.Verify(values => values.SetValues(contact), Times.Once);
}

关键调整点

  1. 移除手动创建InternalEntityEntry和EntityEntry<Contact>的代码,改用Moq创建Mock对象,规避EF内部类的构造限制。
  2. 单独MockPropertyValues并关联到EntityEntry的CurrentValues属性,避免直接操作EF内部组件。
  3. 调整验证逻辑,直接验证PropertyValues的SetValues方法是否被调用,而非通过DbContext的链式调用验证。

内容的提问来源于stack exchange,提问作者Cenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 05:36:22