带 Moq 的单元测试 EF 存储库模式

发布于 2024-12-02 21:42:14 字数 3497 浏览 1 评论 0原文

我决定开始在我们的应用程序中编写单元测试。它使用带有存储库模式的实体框架。

现在我想开始测试使用存储库的逻辑类。我在这里提供一个简单的例子。

我在类 GenericRepository 中的三个方法:

public class GenericRepository : IRepository
{
    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

一个简单的逻辑类,按降序从日历表中返回不同的年份(是的,我知道日历这个词在我们的代码中拼写错误):

public class GetDistinctYearsFromCalendar
{
    private readonly IRepository _repository;

    public GetDistinctYearsFromCalendar()
    {
        _repository = new GenericRepository();
    }

    internal GetDistinctYearsFromCalendar(IRepository repository)
    {
        _repository = repository;
    }

    public int[] Get()
    {
        return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray();
    }
}

这是我的第一个测试:

[TestFixture]
public class GetDistinctYearsFromCalendarTest
{
    [Test]
    public void ReturnsDistinctDatesInCorrectOrder()
    {
        var repositoryMock = new Mock<IRepository>();

        repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl>
        {
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 1, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 2, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2011, 1, 1),
                  Year = 2011
              }
        }.AsQueryable());

        var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get();

        Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct.");
        Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first.");
        Assert.AreEqual(2010, getDistinct[1], "Wrong year.");


    }
}

这工作正常。但这实际上并不是我想做的。由于我必须在模拟对象上设置 Find 方法,所以我还需要知道如何在我的逻辑类中调用它。如果我想做 TDD,我不想介意这个。我想知道的是我的存储库应该提供哪些日历实体。我想设置 GetQuery 方法。 像这样:

repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl>
{
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 1, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 2, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2011, 1, 1),
          Year = 2011
      }
}.AsQueryable());

因此,当 Find 在 GenericRepository 类内部调用 GetQuery 时,它应该获得我在 GetQuery 中设置的正确日历实体。但这当然行不通。由于我尚未设置模拟对象的 Find 方法,因此我没有获得任何实体。

那么该怎么办呢?当然,我可能可以使用 Moles 或其他一些可以模拟一切的框架,但我不想这样做。我可以在课程或测试的设计中做些什么来解决这个问题吗?

如果我必须采用当前的解决方案,那并不是世界末日,但是如果财产年份变成不可为空的整数怎么办?当然,我将不得不更改逻辑类中的实现,但我也必须更改测试。我想尽量避免这种情况。

I decided to start writing unit tests in our application. It uses Entity Framework with a repository pattern.

Now I want to start testing logic classes which are using the repositories. I provide a simple example here.

Three of my methods in the class GenericRepository:

public class GenericRepository : IRepository
{
    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

A simple logic class returning distinct years from a calendar table in descending order (yes I know the word calendar is misspelled in our code):

public class GetDistinctYearsFromCalendar
{
    private readonly IRepository _repository;

    public GetDistinctYearsFromCalendar()
    {
        _repository = new GenericRepository();
    }

    internal GetDistinctYearsFromCalendar(IRepository repository)
    {
        _repository = repository;
    }

    public int[] Get()
    {
        return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray();
    }
}

And here is my first test:

[TestFixture]
public class GetDistinctYearsFromCalendarTest
{
    [Test]
    public void ReturnsDistinctDatesInCorrectOrder()
    {
        var repositoryMock = new Mock<IRepository>();

        repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl>
        {
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 1, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 2, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2011, 1, 1),
                  Year = 2011
              }
        }.AsQueryable());

        var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get();

        Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct.");
        Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first.");
        Assert.AreEqual(2010, getDistinct[1], "Wrong year.");


    }
}

This is working fine. But this is not actually what I want to do. Since I have to setup the method Find on the mock object I also need to know how it is going to be called in my logic class. If I would like to do TDD I don't want to mind about this. All I want to know is which Calendar entities my repository should provide. I would like to setup the GetQuery method.
Like this:

repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl>
{
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 1, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 2, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2011, 1, 1),
          Year = 2011
      }
}.AsQueryable());

So when Find is calling GetQuery internally in the GenericRepository class it should get the correct Calendar entities that I setup in GetQuery. But this is not working of course. Since I haven't setup the Find method of my mock object I don't get any entities.

So what to do? Of course I could probably use Moles or some other framework which mocks everything but I don't want to do that. Is there anything I can do in the design of the class or test to solve the issue?

It is not the end of the world if I have to go with my current solution but what if the property year turns into a not nullable int? Then of course I will have to change my implementation in the logic class but I would also have to change the test. I would like to try to avoid this.

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

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

发布评论

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

评论(2

不必在意 2024-12-09 21:42:14

我可以看到两种方法:

public class MockRepository : IRepository
{
    private List<object> entities;
    public MockRepository(params object[] entitites)
    {
      this.entities = entities.ToList();
    }

    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        return this.entities.OfType<TEntity>().AsQueryable();
    }

    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

这是最简单的也是我首选的方法。起订量并不是万能的锤子;)

或者,如果您真的坚持使用起订量(我很受宠若惊,但在这种情况下这是非常不必要的,因为您可以对返回的实体进行基于状态的测试),您可以这样做:

public class GenericRepository : IRepository
{
    public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

然后使用 Moq 覆盖 GetQuery 的行为:

var repository = new Mock<GenericRepository> { CallBase = true };

repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable());

将会发生的是,Find 方法将在 GenericRepository 类上执行,该方法又会被 Moq 覆盖以提供固定的实体集的 GetQuery。

我明确设置 CallBase = true 以防万一您碰巧也将 Find 设为虚拟,以便我们确保它始终被调用。如果 Find 不是虚拟的,则在技术上不需要,因为它将始终在模拟继承/模拟的实际类上调用。

我会选择第一个选项,更容易理解正在发生的事情,并且它可以在单个特定测试的上下文之外重用(只需传递您需要的任何实体,它将适用于所有情况)。

I can see two ways:

public class MockRepository : IRepository
{
    private List<object> entities;
    public MockRepository(params object[] entitites)
    {
      this.entities = entities.ToList();
    }

    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        return this.entities.OfType<TEntity>().AsQueryable();
    }

    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

That's the easiest and my preferred way. Moq isn't the hammer for everything ;)

Alternatively, if you really insist on using Moq (I'm flattered, but it's very much unnecessary in this case, as you can do state based testing on the returned entities), you can do:

public class GenericRepository : IRepository
{
    public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

And then use Moq to override the behavior of GetQuery:

var repository = new Mock<GenericRepository> { CallBase = true };

repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable());

What will happen is that the Find method will get executed on the GenericRepository class, which will in turn the GetQuery, which has been overwritten by Moq to provide the fixed set of entities.

I set CallBase = true explicitly just in case you happen to make Find virtual too, so that we ensure it's always called. Not technically needed if the Find isn't virtual, as it will always be invoked on the actual class the mock is inheriting/mocking from.

I'd go for the first option, much simpler to understand what's going on, and it can be reused outside of the context of a single particular test (just pass any entities you need and it will work for everything).

筱果果 2024-12-09 21:42:14

最近,针对 EF 6+ 推出了一个名为 Effort 的新工具,我发现它对于针对假 DB 进行单元测试非常有帮助。请参阅 http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=首页

使用此包管理器控制台命令添加它:

PM> Install-Package Effort.EF6

然后为 DbContext 添加一个接口,例如,如果您使用的是 AdventureWorks 数据库(请参阅 https://sql2012kitdb.codeplex.com/):

然后更新您的 DbContext 以添加两个新的参数化构造函数:

    /// 
    /// Create a new context based on database name or connection string.
    /// 
    /// Database name or connection string
    public AdventureWorksEntities(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

    public AdventureWorksEntities(DbConnection connection)
        : base(connection, true)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

添加一个将接口带入存储库的构造函数:

    private IAdventureWorksDbContext _dbContext;

    public ProductRepository(IAdventureWorksDbContext dbContext)
    {
        dbContext.Configuration.AutoDetectChangesEnabled = false;
        this._dbContext = dbContext;
    }

然后将一个接口添加到您的单元测试项目并关联类:

public interface ITestDatabase : IDisposable
{
    IAdventureWorksDbContext CreateContext();

    void Dispose(IAdventureWorksDbContext context);
}

向您的单元测试项目添加一些假数据:

public class ProductsTestData
{
    public static void AddTestData(IAdventureWorksDbContext dbContext)
    {
        dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." });
        dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now });
        dbContext.SaveChanges();
    }
}

现在设置您的单元测试类:

[TestClass]
public class ProductsTest
{
    private ITestDatabase _testDatabaseStrategy;
    private ProductRepository _productRepository;
    private IAdventureWorksDbContext _context;

    [TestInitialize]
    public void SetupTest()
    {
        // create the test strategy.  This will initialise a new database
        _testDatabaseStrategy = CreateTestStrategy();

        // add test data to the database instance
        using (_context = _testDatabaseStrategy.CreateContext())
        {
            ProductsTestData.AddTestData(_context);
            _context.SaveChanges();
        }

        // initialise the repository we are testing
        _context = _testDatabaseStrategy.CreateContext();
        _productRepository = new ProductRepository(_context);
    }

    protected ITestDatabase CreateTestStrategy()
    {
        return new EffortDatabaseStrategy();
    }

    [TestCleanup]
    public void CleanupTest()
    {
        // dispose of the database and connection
        _testDatabaseStrategy.Dispose(_context);
        _context = null;
    }

    [TestMethod]
    public void GetProductsByTagName()
    {
        IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false);
        Assert.AreEqual(1, products.Count());
    }

EffortDatabaseStrategy 所在位置:

public class EffortDatabaseStrategy : ITestDatabase
{
    public EffortDatabaseStrategy()
    {
    }

    private DbConnection _connection;

    public IAdventureWorksDbContext CreateContext()
    {
        if (_connection == null)
        {
            _connection = Effort.DbConnectionFactory.CreateTransient();
        }
        var context = new AdventureWorksDbContext(_connection);

        return context;
    }

    public void Dispose(IAdventureWorksDbContext context)
    {
        if (context != null)
        {
            context.Dispose();
        }
    }

    public void Dispose()
    {
    }
}

有关完整详细信息,请参阅 http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx

Recently, a new tool called Effort has come out for EF 6+ that I found to be tremendously helpful for unit testing against a fake DB. See http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=Home.

Add it by using this package manager console command:

PM> Install-Package Effort.EF6

Then add an interface for your DbContext, say, if you are using the AdventureWorks database (see https://sql2012kitdb.codeplex.com/):

Then update your DbContext to add two new parameterized constructors:

    /// 
    /// Create a new context based on database name or connection string.
    /// 
    /// Database name or connection string
    public AdventureWorksEntities(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

    public AdventureWorksEntities(DbConnection connection)
        : base(connection, true)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

Add a constructor that takes the interface to your repository:

    private IAdventureWorksDbContext _dbContext;

    public ProductRepository(IAdventureWorksDbContext dbContext)
    {
        dbContext.Configuration.AutoDetectChangesEnabled = false;
        this._dbContext = dbContext;
    }

Then add an interface to your unit testing project and associated class:

public interface ITestDatabase : IDisposable
{
    IAdventureWorksDbContext CreateContext();

    void Dispose(IAdventureWorksDbContext context);
}

Add some fake data to your unit testing project:

public class ProductsTestData
{
    public static void AddTestData(IAdventureWorksDbContext dbContext)
    {
        dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." });
        dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now });
        dbContext.SaveChanges();
    }
}

Now setup your unit testing class:

[TestClass]
public class ProductsTest
{
    private ITestDatabase _testDatabaseStrategy;
    private ProductRepository _productRepository;
    private IAdventureWorksDbContext _context;

    [TestInitialize]
    public void SetupTest()
    {
        // create the test strategy.  This will initialise a new database
        _testDatabaseStrategy = CreateTestStrategy();

        // add test data to the database instance
        using (_context = _testDatabaseStrategy.CreateContext())
        {
            ProductsTestData.AddTestData(_context);
            _context.SaveChanges();
        }

        // initialise the repository we are testing
        _context = _testDatabaseStrategy.CreateContext();
        _productRepository = new ProductRepository(_context);
    }

    protected ITestDatabase CreateTestStrategy()
    {
        return new EffortDatabaseStrategy();
    }

    [TestCleanup]
    public void CleanupTest()
    {
        // dispose of the database and connection
        _testDatabaseStrategy.Dispose(_context);
        _context = null;
    }

    [TestMethod]
    public void GetProductsByTagName()
    {
        IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false);
        Assert.AreEqual(1, products.Count());
    }

Where EffortDatabaseStrategy is:

public class EffortDatabaseStrategy : ITestDatabase
{
    public EffortDatabaseStrategy()
    {
    }

    private DbConnection _connection;

    public IAdventureWorksDbContext CreateContext()
    {
        if (_connection == null)
        {
            _connection = Effort.DbConnectionFactory.CreateTransient();
        }
        var context = new AdventureWorksDbContext(_connection);

        return context;
    }

    public void Dispose(IAdventureWorksDbContext context)
    {
        if (context != null)
        {
            context.Dispose();
        }
    }

    public void Dispose()
    {
    }
}

For full details, please see http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.

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