使用Entity Framework实现和测试存储库模式的教程

239 阅读10分钟

有很多关于存储库模式的博文和误解,尤其是在引入OR/M库后,如Entity Framework。在这篇文章中,我们将研究为什么这种模式仍然有用,使用它的好处是什么,以及我们如何将它与Entity Framework结合使用。尽管微软将其DbContextDbSet类定义为这种模式的替代品,但我们将看到这种模式仍然可以帮助我们使代码更加简洁。

让我们从Repository Pattern的定义开始。这种模式的最佳定义之一可以在Martin Fowler的《企业架构模式》一书中找到。

存储库在领域和数据映射层之间起中介作用, 就像一个内存中的领域对象集合。

人们可能会问,为什么这个定义这么酷。嗯,我个人喜欢它,因为它强调了存储库模式的两个非常重要的属性。第一个属性是这个模式是一个抽象的,旨在减少复杂性。第二个重要属性是,这个模式实际上是一个反映数据库表的内存集合。

因此,在这篇文章中,我们将介绍:

1.存储库模式--好处和误解

2.存储库模式概述

3.实体框架简要概述

4.实现、测试和嘲讽

1.存储库模式--好处和误解

即使我们使用了Entity Framework,我们最终可能会有很多重复的查询代码。例如,如果我们正在实现一个博客应用程序,我们想在几个地方获得浏览量最大的文章,我们可能最终会出现重复的查询逻辑,看起来就像这样:

var twentyMostViewedPosts = context.Articles
  .Where(a => a.IsPublished)
  .OrderBy(a => a.Views).Take(20);

我们最终可能会有更复杂的查询,这有可能是通过代码的重复。改变和维护这样的代码不是一件容易的事情。所以,我们仍然调用我们的存储库来帮助我们解决这个问题。我们可以把这个行为封装在Repository里面,然后像这样调用它:

var twentyMostViewedPosts = repository.GetTopTwentyArticles(context);

Coding Visual

这样就干净多了,不是吗?另外,如果我们想改变这个逻辑中的某些东西,我们只需要在一个地方做,这是一个巨大的好处。这就是使用Repository Pattern的第一个好处--没有重复的查询逻辑。

第二个明显的好处是,它将应用程序代码与持久化框架分开,并与我们正在使用的数据库分开。基本上,我们可以在我们的存储库中使用不同的OR/M,或者使用完全不同的技术,例如MongoDBPostgreSQL

看看过去十年中数据库趋势的变化,很明显我们希望有这样的灵活性。这里的有效问题是:"是的,但你到底多久会改变数据库?"最近我做了一个项目,从SQL Server到ElasticSearch再到RavenDB。

最后,但可能是使用这种模式的主要好处之一是,它简化了单元测试。然而,人们常常有一个误解,认为这种模式可以让你以更容易的方式测试数据访问层。事实并非如此,但它正在成为测试业务逻辑的重要资产。在业务逻辑中模拟资源库的实现很容易。

2.存储库模式概述

正如我们已经提到的,存储库是一个对象的内存集合,该集合需要有一个接口,我们可以使用该接口访问集合中的元素。这就是为什么 Repository 应该公开经典的 CRUD 操作。有些人选择跳过更新操作,因为从内存集合中更新一个对象本质上就是获取它并改变它的值。我就是这样的人 🙂 当然,如果你喜欢,你也可以实现这个功能。

我们将定义一个接口IRepository ,它暴露了这些功能:

  • Add - 方法,将定义的类型T的对象添加到我们的存储库中。
  • Remove - 方法将从我们的存储库中移除定义类型T的对象。
  • Get - 方法将从我们的资源库中获得一个定义类型T的对象。
  • GetAll - 方法将从我们的资源库中获得所有的对象
  • Find - 方法将从我们的资源库中找到并检索符合特定条件的对象。

注意,在我们的资源库中也没有类似保存 方法的东西。这就是另一个模式,叫做工作单元的地方。这是一个独立的组件,持有不同资源库的信息,并实现保存功能。

3.实体框架简要概述

此刻我们知道我们将使用Entity Framework,如果你对它不熟悉,你可以查看MSDN的相关文章。实质上,Entity Framework是一个对象-关系映射器(O/RM),它使.NET开发者能够使用.NET对象与数据库一起工作。它消除了开发人员通常需要编写的大部分数据访问代码的需要。简而言之,它将代码对象映射到数据库表,反之亦然。

当涉及到使用EntityFramework时,有两种方法。第一种被称为数据库优先的方法。在这种方法中,我们创建一个数据库,使用Entity Framework来创建领域对象,并在此基础上构建代码。第二种被称为代码优先的方法。这就是我们建立存储库的方法。

Data Visual

我们需要知道两个重要的类--DbContextDbSet。 DbContext是Entity Framework的一个重要类。当使用Entity Framework时,我们需要有一个类在我们的系统中派生这个类。这样,Entity Framework就会知道需要创建什么。继承这个DbContext的类为你想成为模型一部分的类型暴露了DbSet属性。

4.实现、测试和嘲弄

酷,现在我们知道了我们的Repository应该是什么样子,我们可以开始为Repository类写测试了。在我们继续之前有一个小提示,在这个例子中,我使用了xUnit单元测试框架和Moq嘲弄框架。另外,本着TDD的精神,我们将先写测试,后写实现。除此之外,本例中使用的测试类型是简单的TestClass, 下面是它的样子:

public class TestClass
{
    public int Id { get; set; }
}

很简单,对吗?它只有一个属性--Id。 让我们定义一下Repository的接口,这样我们就能更好的理解我们的实现:

public interface IReporitory<TEntity> where TEntity : class 
{
    void Add(TEntity entity);
    void Remove(TEntity entity);
    TEntity Get(int id);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
}

4.1 添加方法

好的,现在让我们看看添加 方法的测试是什么样子的:

[Fact]
public void Add_TestClassObjectPassed_ProperMethodCalled()
{
    // Arrange
    var testObject = new TestClass();

    var context = new Mock<DbContext>();
    var dbSetMock = new Mock<DbSet<TestClass>>();
    context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
    dbSetMock.Setup(x => x.Add(It.IsAny<TestClass>())).Returns(testObject);

    // Act
    var repository = new Repository<TestClass>(context.Object);
    repository.Add(testObject);

    //Assert
    context.Verify(x => x.Set<TestClass>());
    dbSetMock.Verify(x => x.Add(It.Is<TestClass>(y => y == testObject)));
}

这里有很多东西需要解释。首先,假设Repository模式将依赖于DbContext类,并且它将通过构造函数来接收它。这就是为什么在测试的Arrange 部分,我们模拟了这个类。我们也创建了一个DbSet类的模拟对象。

Programming Visual

然后我们设置了一种方式,DbSetAdd方法返回testObject, 这只是一个TestClass的对象*,DbContextSet方法返回DbSet* 模拟对象。这样做是为了以后能测试这些方法是否被调用,你可以在测试的断言部分看到这一点。

总结一下,我们模拟DbContextDbSet,然后调用资源库的Add 方法,最后从模拟对象中验证那些被调用的方法。这个方法的实现是非常直接的。这里就是:

public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
      protected readonly DbContext Context;
      protected readonly DbSet<TEntity> Entities;

      public Repository(DbContext context)
      {
          Context = context;
          Entities = Context.Set<TEntity>();
      }

      public void Add(TEntity entity)
      {
          Entities.Add(entity);
      }
}

现在你可以明白为什么我们需要模拟DbContextSet 方法了 这个方法返回一个非通用的DbSet 实例,使用它我们可以访问上下文中给定类型的实体。然后我们在方法中使用这个实例来添加另一个对象。

4.2 删除方法

这个方法的实现与添加方法的实现类似。让我们看看测试是什么样子的:

[Fact]
public void Remove_TestClassObjectPassed_ProperMethodCalled()
{
    // Arrange
    var testObject = new TestClass();

    var context = new Mock<DbContext>();
    var dbSetMock = new Mock<DbSet<TestClass>>();
    context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
    dbSetMock.Setup(x => x.Remove(It.IsAny<TestClass>())).Returns(testObject);

    // Act
    var repository = new Repository<TestClass>(context.Object);
    repository.Remove(testObject);

    //Assert
    context.Verify(x => x.Set<TestClass>());
    dbSetMock.Verify(x => x.Remove(It.Is<TestClass>(y => y == testObject)));
}

Artificial Intelligence Visual

几乎和添加方法一样,我们只需要设置DbSetRemove 方法 下面是我们添加Remove方法后Repository类的实现的样子。

public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
    protected readonly DbContext Context;
    protected readonly DbSet<TEntity> Entities;

    public Repository(DbContext context)
    {
        Context = context;
        Entities = Context.Set<TEntity>();
    }

    public void Add(TEntity entity)
    {
        Entities.Add(entity);
    }

    public void Remove(TEntity entity)
    {
        Entities.Remove(entity);
    }
}

现在,由于在实现Add 方法的过程中,我们正确地初始化了DbContextDbSet,所以我们很容易添加其他方法。

4.3 Get方法

在实现Get 方法的过程中,我们也遵循同样的原则。对这个方法的测试是这样的:

[Fact]
public void Get_TestClassObjectPassed_ProperMethodCalled()
{
    // Arrange
    var testObject = new TestClass();

    var context = new Mock<DbContext>();
    var dbSetMock = new Mock<DbSet<TestClass>>();

    context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
    dbSetMock.Setup(x => x.Find(It.IsAny<int>())).Returns(testObject);

    // Act
    var repository = new Repository<TestClass>(context.Object);
    repository.Get(1);

    // Assert
    context.Verify(x => x.Set<TestClass>());
    dbSetMock.Verify(x => x.Find(It.IsAny<int>()));
}

Programming Visual

添加了Get方法后的存储库类看起来像这样:

public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
    protected readonly DbContext Context;
    protected readonly DbSet<TEntity> Entities;

    public Repository(DbContext context)
    {
        Context = context;
        Entities = Context.Set<TEntity>();
    }

    public void Add(TEntity entity)
    {
        Entities.Add(entity);
    }

    public void Remove(TEntity entity)
    {
        Entities.Remove(entity);
    }

    public TEntity Get(int id)
    {
        return Entities.Find(id);
    }
}

4.4 GetAll方法

对于GetAll方法,我们需要对其进行一些改变。这个方法需要返回对象的列表。这意味着我们需要创建一个TestClass 对象的列表,并通过DbSet 返回 实际上,这意味着我们需要模拟DbSet 的一部分,在测试中实现IQueryableinterface。下面是它是如何完成的。

[Fact]
public void GetAll_TestClassObjectPassed_ProperMethodCalled()
{
    // Arrange
    var testObject = new TestClass() { Id = 1 };
    var testList = new List<TestClass>() { testObject };

    var dbSetMock = new Mock<DbSet<TestClass>>();
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Provider).Returns(testList.AsQueryable().Provider);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Expression).Returns(testList.AsQueryable().Expression);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.ElementType).Returns(testList.AsQueryable().ElementType);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.GetEnumerator()).Returns(testList.AsQueryable().GetEnumerator());

    var context = new Mock<DbContext>();
    context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);

    // Act
    var repository = new Repository<TestClass>(context.Object);
    var result = repository.GetAll();

    // Assert
    Assert.Equal(testList, result.ToList());
}

Data Visual

我们需要用创建的测试列表中的属性来模拟Provider、Expression、ElementTypeGetEnumerator() 。 事实证明,为这个方法写一个测试比写实现本身更有挑战性。下面是我们的Repository 类扩展了GetAll方法。

public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
  protected readonly DbContext Context;
  protected readonly DbSet<TEntity> Entities;

  public Repository(DbContext context)
  {
      Context = context;
      Entities = Context.Set<TEntity>();
  }

  public void Add(TEntity entity)
  {
      Entities.Add(entity);
  }

  public void Remove(TEntity entity)
  {
      Entities.Remove(entity);
  }

  public TEntity Get(int id)
  {
      return Entities.Find(id);
  }

  public IEnumerable<TEntity> GetAll()
  {
      return Entities.ToList();
  }
}

4.5 查找方法

在前面的例子中学习了如何模拟IQueryable之后,为Find 方法编写测试就容易多了。我们遵循我们用于GetAll方法的相同原则。测试看起来像这样:

[Fact]
public void Find_TestClassObjectPassed_ProperMethodCalled()
{
    var testObject = new TestClass(){Id = 1};
    var testList = new List<TestClass>() {testObject};

    var dbSetMock = new Mock<DbSet<TestClass>>();
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Provider).Returns(testList.AsQueryable().Provider);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Expression).Returns(testList.AsQueryable().Expression);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.ElementType).Returns(testList.AsQueryable().ElementType);
    dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.GetEnumerator()).Returns(testList.AsQueryable().GetEnumerator());

    var context = new Mock<DbContext>();
    context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);

    var repository = new Repository<TestClass>(context.Object);

    var result = repository.Find(x => x.Id == 1);

    Assert.Equal(testList, result.ToList());
}

最后,Repository类的完整实现看起来是这样的:

public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
    protected readonly DbContext Context;
    protected readonly DbSet<TEntity> Entities;

    public Repository(DbContext context)
    {
        Context = context;
        Entities = Context.Set<TEntity>();
    }

    public void Add(TEntity entity)
    {
        Entities.Add(entity);
    }

    public void Remove(TEntity entity)
    {
        Entities.Remove(entity);
    }

    public TEntity Get(int id)
    {
        return Entities.Find(id);
    }

    public IEnumerable<TEntity> GetAll()
    {
        return Entities.ToList();
    }

    public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
    {
        return Entities.Where(predicate);
    }
}

结论

很多人认为,如果我们要使用DbContextDbSet,就不需要实现Repository Pattern和Unit of Work。如果你问我是不是这样,我会说,这取决于问题的类型。在这篇文章中,你有机会看到如何使用Entity Framework构建和测试一个通用的Repository。如果你选择使用DbContextDbSet,本文中仍有一些有用的提示,比如如何模拟这些类。 谢谢你的阅读!