使用 Moq 框架进行模拟时将 DbSet 与 T 匹配

发布于 2025-01-12 17:24:44 字数 3310 浏览 0 评论 0原文

我正在使用 Entity Framework 6 和 Moq 框架。目前我正在编写一些单元测试,在每个测试中我需要为每个测试设置一个具有适当类型的数据集。 其中一个单元测试如下所示:

[TestMethod]
public async Task GetAllCaseCategories_WithEmptyDataset_ReturnsEmpty()
{
    var data = new List<DataAccessLayer.Tables.CaseCategory>(){};
    var mockContext = GetMockContextWithCaseCategoryDataSetAsync(data);
    var mockLogger = new Mock<IDatabaseContextLogging>();
    DatabaseTablesAccess databaseAccess = new DatabaseTablesAccess(mockDatabaseContext.Object, mockLogger.Object);
    List<OneCaseCategory> caseCategories = await databaseAccess.GetAllCaseCategories();
    Assert.IsTrue(caseCategories.Count().Equals(0), "caseCategories should not contain any items.");
}

生成数据库上下文模拟对象的方法包含在名为“GetMockContextWithCaseCategoryDataSetAsync”的方法中。如下:

public Mock<Context> GetMockContextWithCaseCategoryDataSetAsync(List<CaseCategory> data)
{
    var mockCaseCategorySet = new Mock<DbSet<CaseCategory>>() { };
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.Provider).Returns(new TestDbAsyncQueryProvider<CaseCategory>(data.AsQueryable().Provider));
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.Expression).Returns(data.AsQueryable().Expression);
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.ElementType).Returns(data.AsQueryable().ElementType);
    mockCaseCategorySet.As<IDbAsyncEnumerable<CaseCategory>>().Setup(x => x.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<CaseCategory>(data.AsQueryable().GetEnumerator()));
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.GetEnumerator()).Returns(data.AsQueryable().GetEnumerator());
    Mock<Context> m = new Mock<Context>();
    m.Setup(x => x.CaseCategories).Returns(mockCaseCategorySet.Object);
    return m;
}

我在数据访问对象中有多个表,并且为每个表编写了一种方法,该方法返回数据访问对象,并将适当类型的数据绑定到模拟对象中的正确表。我想概括这一点,这是我的尝试:

public Mock<Context> GetMockContextWithCaseCategoryDataSetAsync<T>(List<T> data) where T : class
{
    var mockCaseCategorySet = new Mock<DbSet<T>>() { };
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.Provider).Returns(new TestDbAsyncQueryProvider<T>(data.AsQueryable().Provider));
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.Expression).Returns(data.AsQueryable().Expression);
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.ElementType).Returns(data.AsQueryable().ElementType);
    mockCaseCategorySet.As<IDbAsyncEnumerable<T>>().Setup(x => x.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<T>(data.AsQueryable().GetEnumerator()));
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.GetEnumerator()).Returns(data.AsQueryable().GetEnumerator());
    Mock<Context> m = new Mock<Context>();
    m.Setup(x => x.?).Returns(mockCaseCategorySet.Object);
    return m;
}

一切都很好,直到我设置哪个表返回模拟对象的方法的通用版本中的倒数第二行。我不知道如何(如果可能的话)根据传入的泛型控制使用哪个表。 我想这样使用它:

var context = GetMockContextWithCaseCategoryDataSetAsync<CaseCategory>(data);

I am using Entity Framework 6 and Moq framework. Currently I am writing a few Unit Tests where in each test I need to set up a dataset with the appropriate type for each test.
One of the unit tests looks like this:

[TestMethod]
public async Task GetAllCaseCategories_WithEmptyDataset_ReturnsEmpty()
{
    var data = new List<DataAccessLayer.Tables.CaseCategory>(){};
    var mockContext = GetMockContextWithCaseCategoryDataSetAsync(data);
    var mockLogger = new Mock<IDatabaseContextLogging>();
    DatabaseTablesAccess databaseAccess = new DatabaseTablesAccess(mockDatabaseContext.Object, mockLogger.Object);
    List<OneCaseCategory> caseCategories = await databaseAccess.GetAllCaseCategories();
    Assert.IsTrue(caseCategories.Count().Equals(0), "caseCategories should not contain any items.");
}

The method that generates the database context mock object is contain in the method called "GetMockContextWithCaseCategoryDataSetAsync". Here it is:

public Mock<Context> GetMockContextWithCaseCategoryDataSetAsync(List<CaseCategory> data)
{
    var mockCaseCategorySet = new Mock<DbSet<CaseCategory>>() { };
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.Provider).Returns(new TestDbAsyncQueryProvider<CaseCategory>(data.AsQueryable().Provider));
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.Expression).Returns(data.AsQueryable().Expression);
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.ElementType).Returns(data.AsQueryable().ElementType);
    mockCaseCategorySet.As<IDbAsyncEnumerable<CaseCategory>>().Setup(x => x.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<CaseCategory>(data.AsQueryable().GetEnumerator()));
    mockCaseCategorySet.As<IQueryable<CaseCategory>>().Setup(x => x.GetEnumerator()).Returns(data.AsQueryable().GetEnumerator());
    Mock<Context> m = new Mock<Context>();
    m.Setup(x => x.CaseCategories).Returns(mockCaseCategorySet.Object);
    return m;
}

I have multiple tables in the data access object and I have written a method for each one that returns the data access object with the appropriate type of data bound to the correct table in the mock object. I would like to generalize this and here is my attempt:

public Mock<Context> GetMockContextWithCaseCategoryDataSetAsync<T>(List<T> data) where T : class
{
    var mockCaseCategorySet = new Mock<DbSet<T>>() { };
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.Provider).Returns(new TestDbAsyncQueryProvider<T>(data.AsQueryable().Provider));
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.Expression).Returns(data.AsQueryable().Expression);
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.ElementType).Returns(data.AsQueryable().ElementType);
    mockCaseCategorySet.As<IDbAsyncEnumerable<T>>().Setup(x => x.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<T>(data.AsQueryable().GetEnumerator()));
    mockCaseCategorySet.As<IQueryable<T>>().Setup(x => x.GetEnumerator()).Returns(data.AsQueryable().GetEnumerator());
    Mock<Context> m = new Mock<Context>();
    m.Setup(x => x.?).Returns(mockCaseCategorySet.Object);
    return m;
}

Everything is good up until that second last line in the generic version of the method where I am setting up which table returns the mocked object. I am lost as to how (if even possible) I control which table is used based on the generic passed in.
I would like to use it like this:

var context = GetMockContextWithCaseCategoryDataSetAsync<CaseCategory>(data);

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

冷夜 2025-01-19 17:24:44

我看不到一种方法可以完成您想要做的事情,除非您还通过通用 Set 方法(如 _dbContext .Set().Select(x => x.Id))。

我一直在使用的另一种通用设置是创建一个“MockedDbContext”类,我在访问数据库的所有测试中重用该类。这通常看起来像这样;

public class MockedDbContext
{
    public MockedDbContext() => MockContextProperties();

    public Mock<MyDbContext> ContextMock { get; } = new Mock<MyDbContext>(MockBehavior.Loose);

    public HashSet<Employee> Employees { get; } = new HashSet<Employee>();
    public HashSet<Salary> Salaries { get; } = new HashSet<Salary>();
    // etc...

    private void MockContextProperties()
    {
        SimulateGet(x => x.Employees, Employees);
        SimulateGet(x => x.Salaries, Salaries);
        // etc...
    }

    private void SimulateGet<TModel>(
        Expression<Func<MyDbContext, DbSet<TModel>>> dbSetExpression,
        HashSet<TModel> mockedData) where TModel : class
    {
        ContextMock.SetupGet(dbSetExpression).Returns(new InMemoryDbSet<TModel>(mockedData));
    }

    public void VerifyThatSaveWasCalled() => ContextMock.Verify(x => x.SaveChanges());
    public void VerifyThatSaveWasNotCalled() => ContextMock.Verify(x => x.SaveChanges(), Times.Never);
}

我这样使用它;

[TestFixture]
public class When_getting_some_data
{
    private readonly MockedDbContext _mockedDbContext = new MockedDbContext();
    private readonly Fixture _fixture = new Fixture();
    private string _organizationNumber = "yyyyyy-xxx";
    private readonly DateTime _created = new DateTime(2022, 2, 2);
    
    private SomeInformation _result;

    [OneTimeSetUp]
    public void Initialize()
    {
        var salary = _fixture.Build<Salary>().With(o => o.Created, _created).Create();
        _mockedDbContext.Salaries.Add(salary);

        _result = new SomeClass(_mockedDbContext.ContextMock.Object).GetSomeInformation(_organizationNumber);
    }

    [Test]
    public void Then_returned_data_is_as_expected()
    {
        _result.Should().NotBeNull();
        _result.Created.Should().Be(_created);
    }
}

它内部使用一个 InMemoryDbSet ,如下所示;

/// <summary>
/// The in-memory database set, taken from Microsoft's online example (http://msdn.microsoft.com/en-us/ff714955.aspx)
/// and modified to be based on DbSet instead of ObjectSet.
/// </summary>
/// <typeparam name="T">The type of DbSet.</typeparam>
public class InMemoryDbSet<T> : DbSet<T>, IQueryable<T> where T : class
{
    private static readonly HashSet<T> StaticData = new HashSet<T>();
    private readonly HashSet<T> _nonStaticData;

    /// <summary>
    /// Creates an instance of the InMemoryDbSet using the default static backing store.This means
    /// that data persists between test runs, like it would do with a database unless you
    /// cleared it down.
    /// </summary>
    public InMemoryDbSet() : this(true) { }

    /// <summary>
    /// This constructor allows you to pass in your own data store, instead of using
    /// the static backing store.
    /// </summary>
    /// <param name="data">A place to store data.</param>
    public InMemoryDbSet(HashSet<T> data) => _nonStaticData = data;

    /// <summary>
    /// Creates an instance of the InMemoryDbSet using the default static backing store.This means
    /// that data persists between test runs, like it would do with a database unless you
    /// cleared it down.
    /// </summary>
    /// <param name="clearDownExistingData">True to clear existing data</param>
    public InMemoryDbSet(bool clearDownExistingData)
    {
        if (clearDownExistingData)
            Clear();
    }

    public Func<IEnumerable<T>, object[], T> FindFunction { get; set; }
    public Type ElementType => Data.AsQueryable().ElementType;
    public Expression Expression => Data.AsQueryable().Expression;
    public IQueryProvider Provider => Data.AsQueryable().Provider;
    IQueryProvider IQueryable.Provider => Data.AsQueryable().Provider;
    public new ObservableCollection<T> Local => new ObservableCollection<T>(Data);

    /// <summary>
    /// The non static backing store data for the InMemoryDbSet.
    /// </summary>
    private HashSet<T> Data => _nonStaticData ?? StaticData;

    public void Clear() => Data.Clear();

    public override EntityEntry<T> Add(T entity)
    {
        Data.Add(entity);
        return null;
    }

    public new T Attach(T entity)
    {
        Data.Add(entity);
        return entity;
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T => Activator.CreateInstance<TDerivedEntity>();

    public T Create() => Activator.CreateInstance<T>();

    public new virtual T Find(params object[] keyValues)
    {
        if (FindFunction != null)
        {
            return FindFunction(Data, keyValues);
        }

        throw new NotImplementedException("Derive from InMemoryDbSet and override Find, or provide a FindFunction.");
    }

    public new T Remove(T entity)
    {
        Data.Remove(entity);
        return entity;
    }

    public IEnumerator<T> GetEnumerator() => Data.GetEnumerator();
}

这在编写单元测试时节省了大量工作,并且是一种一次性设置,这正是我认为您正在寻找的。

I don't see a way to do what you're trying to do unless you also access the DbSets by the generic Set<TEntity> method (like _dbContext.Set<Salary>().Select(x => x.Id)).

An alternative setup that I have been using and that is sort of generic is to create a "MockedDbContext" class that I reuse in all tests accessing the database. This usually looks something like this;

public class MockedDbContext
{
    public MockedDbContext() => MockContextProperties();

    public Mock<MyDbContext> ContextMock { get; } = new Mock<MyDbContext>(MockBehavior.Loose);

    public HashSet<Employee> Employees { get; } = new HashSet<Employee>();
    public HashSet<Salary> Salaries { get; } = new HashSet<Salary>();
    // etc...

    private void MockContextProperties()
    {
        SimulateGet(x => x.Employees, Employees);
        SimulateGet(x => x.Salaries, Salaries);
        // etc...
    }

    private void SimulateGet<TModel>(
        Expression<Func<MyDbContext, DbSet<TModel>>> dbSetExpression,
        HashSet<TModel> mockedData) where TModel : class
    {
        ContextMock.SetupGet(dbSetExpression).Returns(new InMemoryDbSet<TModel>(mockedData));
    }

    public void VerifyThatSaveWasCalled() => ContextMock.Verify(x => x.SaveChanges());
    public void VerifyThatSaveWasNotCalled() => ContextMock.Verify(x => x.SaveChanges(), Times.Never);
}

I use it like this;

[TestFixture]
public class When_getting_some_data
{
    private readonly MockedDbContext _mockedDbContext = new MockedDbContext();
    private readonly Fixture _fixture = new Fixture();
    private string _organizationNumber = "yyyyyy-xxx";
    private readonly DateTime _created = new DateTime(2022, 2, 2);
    
    private SomeInformation _result;

    [OneTimeSetUp]
    public void Initialize()
    {
        var salary = _fixture.Build<Salary>().With(o => o.Created, _created).Create();
        _mockedDbContext.Salaries.Add(salary);

        _result = new SomeClass(_mockedDbContext.ContextMock.Object).GetSomeInformation(_organizationNumber);
    }

    [Test]
    public void Then_returned_data_is_as_expected()
    {
        _result.Should().NotBeNull();
        _result.Created.Should().Be(_created);
    }
}

And It internally uses an InMemoryDbSet<T> that looks like this;

/// <summary>
/// The in-memory database set, taken from Microsoft's online example (http://msdn.microsoft.com/en-us/ff714955.aspx)
/// and modified to be based on DbSet instead of ObjectSet.
/// </summary>
/// <typeparam name="T">The type of DbSet.</typeparam>
public class InMemoryDbSet<T> : DbSet<T>, IQueryable<T> where T : class
{
    private static readonly HashSet<T> StaticData = new HashSet<T>();
    private readonly HashSet<T> _nonStaticData;

    /// <summary>
    /// Creates an instance of the InMemoryDbSet using the default static backing store.This means
    /// that data persists between test runs, like it would do with a database unless you
    /// cleared it down.
    /// </summary>
    public InMemoryDbSet() : this(true) { }

    /// <summary>
    /// This constructor allows you to pass in your own data store, instead of using
    /// the static backing store.
    /// </summary>
    /// <param name="data">A place to store data.</param>
    public InMemoryDbSet(HashSet<T> data) => _nonStaticData = data;

    /// <summary>
    /// Creates an instance of the InMemoryDbSet using the default static backing store.This means
    /// that data persists between test runs, like it would do with a database unless you
    /// cleared it down.
    /// </summary>
    /// <param name="clearDownExistingData">True to clear existing data</param>
    public InMemoryDbSet(bool clearDownExistingData)
    {
        if (clearDownExistingData)
            Clear();
    }

    public Func<IEnumerable<T>, object[], T> FindFunction { get; set; }
    public Type ElementType => Data.AsQueryable().ElementType;
    public Expression Expression => Data.AsQueryable().Expression;
    public IQueryProvider Provider => Data.AsQueryable().Provider;
    IQueryProvider IQueryable.Provider => Data.AsQueryable().Provider;
    public new ObservableCollection<T> Local => new ObservableCollection<T>(Data);

    /// <summary>
    /// The non static backing store data for the InMemoryDbSet.
    /// </summary>
    private HashSet<T> Data => _nonStaticData ?? StaticData;

    public void Clear() => Data.Clear();

    public override EntityEntry<T> Add(T entity)
    {
        Data.Add(entity);
        return null;
    }

    public new T Attach(T entity)
    {
        Data.Add(entity);
        return entity;
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T => Activator.CreateInstance<TDerivedEntity>();

    public T Create() => Activator.CreateInstance<T>();

    public new virtual T Find(params object[] keyValues)
    {
        if (FindFunction != null)
        {
            return FindFunction(Data, keyValues);
        }

        throw new NotImplementedException("Derive from InMemoryDbSet and override Find, or provide a FindFunction.");
    }

    public new T Remove(T entity)
    {
        Data.Remove(entity);
        return entity;
    }

    public IEnumerator<T> GetEnumerator() => Data.GetEnumerator();
}

This saves a lot of work when writing unit tests and is kind of a one time setup which is what I think you were looking for.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文