首页 > 解决方案 > 无法跟踪 Entity Framework Core 单元测试实体

问题描述

我正在尝试编写一个使用实体框架核心的 inmemorydbcontext 的单元测试:

InMemoryDbContext 的设置:

public abstract class ServiceTestsBase<TContextInterface, TContext>
        where TContextInterface : IDbContext
        where TContext : DbContext, TContextInterface
    {
        protected TContextInterface InMemoryContext { get; private set; }

        public ServiceTestsBase()
        {
            this.InitializeInMemoryContext();
        }

        protected void InitializeInMemoryContext()
        {
            var httpContextAccessorFake = new Mock<IHttpContextAccessor>();
            var randomAuditDatabaseName = $"{nameof(IAuditingContext)}_{Guid.NewGuid()}";
            var auditOptions = new DbContextOptionsBuilder<AuditDbContext>().UseInMemoryDatabase(randomAuditDatabaseName).Options;
            var auditContext = new AuditDbContext(auditOptions);

            var randomDatabaseName = $"{nameof(TContextInterface)}_{Guid.NewGuid()}";
            var options = new DbContextOptionsBuilder<TContext>().UseInMemoryDatabase(randomDatabaseName).Options;
            this.InMemoryContext = (TContext) Activator.CreateInstance(typeof(TContext), options, httpContextAccessorFake.Object, auditContext);
        }

        protected int AddToInMemoryContext<TEntity>(ICollection<TEntity> entities) where TEntity : BaseEntity
        {
            foreach (var entity in entities)
            {
                this.InMemoryContext.Add(entity);
            }

            return this.InMemoryContext.SaveChangesAsync().Result;
        } 

我的单元测试:

[TestMethod]
    public void SaveProjektStatus_WhenOldPhaseStatusWasAbgebrochenAndNewStatusIsAbgebrochen_ThenShouldNotSendMail()
    {
        var ppmProjektId = new Guid("0A1D9BCF-01BA-418E-81D8-FD5EFFD79046");
        var projektStatusId = new Guid("4600DEEF-09DB-4CB3-99CF-4D3BCBA22712");

        var ppmProjekt = new PPMProjekt
        {
            Id = ppmProjektId
        };
        var oldProjektStatus = new PPMProjektStatus
        {
            Id = projektStatusId,
            PPMProjektId = ppmProjektId,
            PhaseStatusId = PPMCodeConst.PPM_PHASESTATUS_ABGEBROCHEN.Id
        };
        this.AddToInMemoryContext(new[] { ppmProjekt });
        this.AddToInMemoryContext(new[] { oldProjektStatus });

        var newProjektStatus = new PPMProjektStatus
        {
            Id = projektStatusId,
            PPMProjektId = ppmProjektId,
            PhaseStatusId = PPMCodeConst.PPM_PHASESTATUS_ABGEBROCHEN.Id
        };

        emailServiceFake.Setup(x => x.GetEmailTemplateById(PPMEmailVorlageConst.PPM_PROJECT_STATUS_CHANGED.Id))
            .Returns(new EmailVorlage
            {
                SubjectDt = "{0}{1}",
                BodyDt = "{0}{1}",
                DefaultEmpfaenger = "testmail@test.com"
            });

        this.testee.SaveProjektStatus(newProjektStatus);

        emailServiceFake.Verify(x => x.SendEmail(It.IsAny<EMail>()), Times.Never());
    }

待测方法:

public void SaveProjektStatus(PPMProjektStatus projektStatus)
        {
            var oldProjektStatus = db.PPMProjektStatus.AsNoTracking().SingleOrDefault(x => x.Id == projektStatus.Id);

            var personalInternTagessatz = db.PPMProjekt.Single(x => x.Id == projektStatus.PPMProjektId).PersonalInternTagessatz_CHF;

            projektStatus.PersonalInternFachIST_CHF = personalInternTagessatz * projektStatus.PersonalInternFachIST_PT;
            projektStatus.PersonalInternFachHochrechnung_CHF = personalInternTagessatz * projektStatus.PersonalInternFachHochrechnung_PT;
            projektStatus.PersonalInternInformatikIST_CHF = personalInternTagessatz * projektStatus.PersonalInternInformatikIST_PT;
            projektStatus.PersonalInternInformatikHochrechnung_CHF = personalInternTagessatz * projektStatus.PersonalInternInformatikHochrechnung_PT;

            db.AddOrUpdate(projektStatus);
            db.SaveChanges();

            if (projektStatus.PhaseStatusId.Equals(PPMCodeConst.PPM_PHASESTATUS_ABGEBROCHEN.Id)
                && (oldProjektStatus == null || !oldProjektStatus.PhaseStatusId.Equals(PPMCodeConst.PPM_PHASESTATUS_ABGEBROCHEN.Id)))
            {                
                NotifyProjectStatusAbgebrochen(projektStatus.PPMProjekt.Nummer, projektStatus.PPMProjekt.Bezeichnung, projektStatus.GeaendertAm);
            }

            if (projektStatus.PhaseStatusId.Equals(PPMCodeConst.PPM_PHASESTATUS_UEBERFUERTIN.Id))
            {
                var projektUeberfuertIn = db.PPMProjekt.Where(x => x.Id == projektStatus.ProjektUeberfuertIn).FirstOrDefault();
                NotifyProjectStatusUeberfuertIn(projektStatus.PPMProjekt.Nummer, projektStatus.PPMProjekt.Bezeichnung, projektStatus.GeaendertAm, projektUeberfuertIn.Nummer, projektUeberfuertIn.Bezeichnung);
            }

            searchService.RebuildProjekteSearchIndex(projektStatus.PPMProjektId);
        }

测试在以下行失败:db.AddOrUpdate(projektStatus); 与此消息:

System.InvalidOperationException:无法跟踪实体类型“PPMProjektStatus”的实例,因为已经在跟踪具有相同键值 {'Id'} 的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

为什么会抛出这个异常?它只在单元测试中失败,当我手动运行程序时它可以工作。我明确地使用 .AsNoTracking() 来避免这个问题,但似乎 InMemoryDatabase 并不关心它......

提前致谢

编辑:我也尝试只加载 ID 而不是像这样的整个对象

            var oldProjektStatus = db.PPMProjektStatus.SingleOrDefault(x => x.Id == projektStatus.Id)?.PhaseStatusId;

但我仍然得到同样的错误......

标签: .netentity-frameworkunit-testing.net-coreentity-framework-core

解决方案


推荐阅读