使用Moq进行单元测试EF存储库模式

时间:2011-09-02 09:51:29

标签: unit-testing entity-framework moq repository-pattern

我决定开始在我们的应用程序中编写单元测试。它使用Entity Framework和存储库模式。

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

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中设置的正确Calendar实体。但这当然不起作用。由于我没有设置我的模拟对象的Find方法,所以我没有得到任何实体。

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

如果我必须使用当前的解决方案,那么这不是世界末日,但如果财产年变成不可空的int会怎么样?当然,我将不得不在逻辑类中更改我的实现,但我还必须更改测试。我想尽量避免这种情况。

2 个答案:

答案 0 :(得分:12)

我可以看到两种方式:

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();
    }
}

这是最简单,也是我首选的方式。 Moq不是一切的锤子;)

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

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类上执行,而GenericRepository类又将被Moq覆盖,以提供固定的实体集。

我明确地设置了CallBase = true,以防万一你碰巧发现了Find虚拟,所以我们确保它总是被调用。如果Find不是虚拟的,则技术上不需要,因为它总是在mock继承/模拟的实际类上调用。

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

答案 1 :(得分:0)

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

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

template <typename C>
typename C::iterator my_begin(C& ctr) { return ctr.begin(); }

template <typename C>
typename C::const_iterator my_begin(const C& ctr) { return ctr.begin(); }

template <typename C, size_t sz>
C* my_begin(C (&ctr)[sz]) { return &ctr[0]; } 

template <typename C, size_t sz>
const C* my_begin(const C (&ctr)[sz]) { return &ctr[0]; } 

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

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

PM> Install-Package Effort.EF6

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

    /// 
    /// 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();
    }
}

EffortDatabaseStrategy的位置是:

[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());
    }

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