【问题标题】:Moq - Mock DbSet<T>.AddAsync throws no invocations performedMoq - 模拟 DbSet<T>.AddAsync 不引发任何调用
【发布时间】:2020-07-06 10:26:51
【问题描述】:

我有一个单元测试,它基本上是在测试 EF Core 的行为。我要测试的类如下所示:

namespace MusicPortal.Repository.Repository
{
    public class ArtistRepository : IArtistRepository
    {
        private readonly MusicPortalDbContext _context;

        public ArtistRepository(MusicPortalDbContext context)
        {
            _context = context;
        }

        public async Task<MusicPortalDatabaseResponse<bool>> AddNewArtist(Artist artist)
        {
            try
            {
                await _context.Artists.AddAsync(new Artist
                {
                    ArtistType = ArtistTypes.Band,
                    City = artist.City,
                    Country = artist.Country,
                    Genre = artist.Genre,
                    Name = artist.Name,
                    ProfileImageUrl = artist.ProfileImageUrl
                });

                _context.SaveChanges();
                return new MusicPortalDatabaseResponse<bool>
                {
                    HasError = false,
                    Exception = null,
                    Response = true
                };
            }
            catch (Exception e)
            {
                return new MusicPortalDatabaseResponse<bool>
                {
                    HasError = true,
                    Exception = e,
                    Response = false
                };
            }
        }
    }
}

我使用 Moq 对其进行了以下单元测试

namespace MusicPortal.Tests.Repository.ArtistRepository.AddNewArtist
{
    [TestFixture]
    public class GivenAddingANewArtistToADatabaseFails
    {
        private Mock<DbSet<Artist>> _mockArtistDbSet;
        private Mock<MusicPortalDbContext> _mockContext;

        private IArtistRepository _artistRepository;
        private MusicPortalDatabaseResponse<bool> _addArtistToDbResponse;

        [OneTimeSetUp]
        public async Task Setup()
        {
            _mockArtistDbSet = new Mock<DbSet<Artist>>();
            _mockContext = new Mock<MusicPortalDbContext>();

            _mockArtistDbSet
                .Setup(x => x.AddAsync(It.IsAny<Artist>(), It.IsAny<CancellationToken>()))
                .Callback((Artist artist, CancellationToken token) => { })
                .ReturnsAsync(It.IsAny<EntityEntry<Artist>>());
            _mockContext
                .Setup(x => x.SaveChanges())
                .Throws(new Exception("Cannot save new Artist to Database"));

            _artistRepository = new MusicPortal.Repository.Repository.ArtistRepository(_mockContext.Object);
            _addArtistToDbResponse = await _artistRepository.AddNewArtist(It.IsAny<Artist>());
        }

        [Test]
        public void ThenANegativeResultIsReturned() // pass
        {
            Assert.IsFalse(_addArtistToDbResponse.Response);
            Assert.IsTrue(_addArtistToDbResponse.HasError);
            Assert.IsInstanceOf<Exception>(_addArtistToDbResponse.Exception);
        }

        [Test]
        public void ThenTheArtistContextAddMethodIsCalledOnce() //fail
        {
            _mockArtistDbSet.Verify(x => x.AddAsync(It.IsAny<Artist>(), It.IsAny<CancellationToken>()), Times.Once);
        }

        [Test]
        public void ThenTheArtistsContextSaveMethodIsNeverCalled() //pass
        {
            _mockContext.Verify(x => x.SaveChanges(), Times.Never);
        }
    }
}

第一个和最后一个断言通过但ThenTheArtistContextAddMethodIsCalledOnce() 失败,因为以下错误:

MusicPortal.Tests.Repository.ArtistRepository.AddNewArtist.GivenAddingANewArtistToADatabaseFails.TheArtistContextAddMethodIsCalledOnce

Moq.MockException : 预期在模拟上调用一次,但为 0 次:x => x.AddAsync(It.IsAny(), It.IsAny())

执行的调用:

模拟:1> (x): 未执行任何调用。

在 Moq.Mock.Verify(Mock mock, LambdaExpression 表达式, Times times, String failMessage) 在 Moq.Mock1.Verify[TResult](Expression1 表达式,Func`1 次) 在 MusicPortal.Tests\Repository\ArtistRepository\AddNewArtist\GivenAddingANewArtistToADatabaseFails.cs:line 53 中的 MusicPortal.Tests.Repository.ArtistRepository.AddNewArtist.GivenAddingANewArtistToADatabaseFails.ThenTheArtistContextAddMethodIsCalledOnce()

我了解到问题代码是c# _mockArtistDbSet .Setup(x => x.AddAsync(It.IsAny<Artist>(), It.IsAny<CancellationToken>())) .Callback((Artist artist, CancellationToken token) => { }) .ReturnsAsync(It.IsAny<EntityEntry<Artist>>());

而且我知道问题很可能是由于异步问题造成的,但我不知道为什么,或者实际问题是什么。有什么建议和解决方案吗?

【问题讨论】:

  • 你最后一次测试不应该也失败吗?看起来应该调用 SaveChanges。
  • 该设置代码不应该真的存在,但不存在,Save 方法永远不会被调用,因为添加到 dbSet 的代码会引发错误并会延迟到 catch 块。

标签: c# unit-testing moq ef-core-2.2


【解决方案1】:

因此,使用 SaveChangesAsyncAddAsync 等异步任务似乎不太容易测试 EF Core。最后,我按照 MS 指南测试 EF 核心并创建了一个模拟上下文。唯一的缺点是我只能测试快乐的路径。尽管错误路径是由使用存储库的服务测试的,但我希望在存储库层上有更多的测试覆盖率。

无论如何,这是规范

using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MusicPortal.Core.Context;
using MusicPortal.Core.Repository;
using MusicPortal.Tests.Repository.ArtistRepository.TestHelpers;
using MusicPortal.Tests.Repository.ArtistRepository.TestHelpers.MockDB;
using NUnit.Framework;
using MockArtistRepository = MusicPortal.Repository.Repository.ArtistRepository;

namespace MusicPortal.Tests.Repository.ArtistRepository.AddNewArtist 
{
    [TestFixture]
    public class GivenANewArtistToInsertIntoTheDb
    {
        private DbContextOptions<MusicPortalDbContext> _options;
        private MusicPortalDatabaseResponse<bool> _mockResponse;

        [OneTimeSetUp]
        public async Task Setup()
        {
            _options = new MockDbFactory("MusicPortalDB").Options;

            using (var context = new MusicPortalDbContext(_options))
            {
                var artistRepository = new MockArtistRepository(context);
                _mockResponse = await artistRepository.AddNewArtist(MockRepositoryData.Artist);
            }
        }

        [Test]
        public void AndThenAPositiveResultIsReturned()
        {
            Assert.Null(_mockResponse.Exception);
            Assert.IsTrue(_mockResponse.Response);
            Assert.IsFalse(_mockResponse.HasError);
        }

        [Test]
        public void ThenTheArtistShouldBeSavedWithNoProblem()
        {
            using (var context = new MusicPortalDbContext(_options))
            {
                Assert.AreEqual(1, context.Artists.Count());
            }
        } 
    }
}

和模拟数据库:

using System;
using Microsoft.EntityFrameworkCore;
using MusicPortal.Core.Context;
using MusicPortal.Core.DBModels;

namespace MusicPortal.Tests.Repository.ArtistRepository.TestHelpers.MockDB
{
    public sealed class MockDbFactory
    {
        public DbContextOptions<MusicPortalDbContext> Options { get; }

        public MockDbFactory(string dbName)
        {
            Options = new DbContextOptionsBuilder<MusicPortalDbContext>()
                .UseInMemoryDatabase(dbName)
                .Options;
        }

        public void AddArtistsToContext()
        {
            using (var context = new MusicPortalDbContext(Options))
            {
                context.Artists.Add(new Artist
                {
                    City = "Orange County",
                    Country = "USA",
                    Events = null,
                    Genre = "Pop Punk",
                    Id = Guid.Parse("8a07504b-8152-4d8b-8e21-74bf64322ebc"),
                    Merchandise = null,
                    Name = "A Day To Remember",
                    ArtistType = "Band",
                    ProfileImageUrl = "https://placehold.it/30x30"
                });

                context.SaveChanges();
            }
        }    
    }
}

我希望这可以帮助任何关注相同问题的人。吸取的教训是,除非绝对必须,否则不要使用异步。

【讨论】:

    【解决方案2】:

    您声明并设置了 _mockArtistDbSet,但您不使用/将其附加到 _mockContext。我认为您需要添加以下内容:

    _mockContext.Setup(m => m.Artist).Returns(_mockArtistDbSet.Object);
    

    【讨论】:

    • 不幸的是,我遇到了同样的问题。我正在考虑重构以对数据库进行阻塞调用。可能会对我的某些服务造成性能影响,但我宁愿拥有可测试的代码,也不愿在我的存储库上不覆盖
    猜你喜欢
    • 2019-05-01
    • 1970-01-01
    • 2018-08-15
    • 2017-04-24
    • 1970-01-01
    • 2012-04-29
    • 1970-01-01
    • 2021-04-07
    • 1970-01-01
    相关资源
    最近更新 更多