如何对使用DbContext和NSubstitute的存储库进行单元测试?

时间:2016-07-12 15:55:00

标签: c# entity-framework unit-testing dbcontext nsubstitute

我有一个解决方案,其中我有一个Data项目,其中包含从现有数据库生成的EF6 .edmx文件。我将实体拆分为一个单独的实体项目,并有一个存储库项目,它们都引用它们。

我添加了一个带有一些常用方法的BaseRepository,并希望对它进行单元测试。班级的顶端看起来像这样......

public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
  private readonly MyEntities _ctx;
  private readonly DbSet<T> _dbSet;

  public BaseRepository(MyEntities ctx) {
    _ctx = ctx;
    _dbSet = _ctx.Set<T>();
  }

  public IEnumerable<T> GetAll() {
    return _dbSet;
  }

  //...
}

按照我在https://stackoverflow.com/a/21074664/706346找到的代码,我尝试了以下内容......

[TestMethod]
public void BaseRepository_GetAll() {
  IDbSet<Patient> mockDbSet = Substitute.For<IDbSet<Patient>>();
  mockDbSet.Provider.Returns(GetPatients().Provider);
  mockDbSet.Expression.Returns(GetPatients().Expression);
  mockDbSet.ElementType.Returns(GetPatients().ElementType);
  mockDbSet.GetEnumerator().Returns(GetPatients().GetEnumerator());
  MyEntities mockContext = Substitute.For<MyEntities>();
  mockContext.Patients.Returns(mockDbSet);

  BaseRepositoryInterface<Patient> patientsRepository 
                          = new BaseRepository<Patient>(mockContext);
  List<Patient> patients = patientsRepository.GetAll().ToList();
  Assert.AreEqual(GetPatients().Count(), patients.Count);
}

private IQueryable<Patient> GetPatients() {
  return new List<Patient> {
    new Patient {
      ID = 1,
      FirstName = "Fred",
      Surname = "Ferret"
    }
  }
    .AsQueryable();
}

请注意,我更改了上下文TT文件以使用IDbSet,正如Stuart Clement在12月4日15日在22:41发表的评论中所建议的

但是,当我运行此测试时,它会给出一个空引用异常,因为基础知识库构造函数中设置_dbSet的行会使其为空...

_dbSet = _ctx.Set<T>();

我猜我在设置模拟上下文时需要添加另一行,但我不确定是什么。我认为上面的代码应该足以填充DbSet。

任何人都能解释我错过了什么或做错了什么?

2 个答案:

答案 0 :(得分:5)

好吧,我试图按照我在问题中展示的方式疯狂地试图去做,我遇到Effort,这是专为此任务而设计的,然后跟着this tutorial,让我走了。我的代码存在一些问题,我将在下面解释。

简单地说,我所做的是......

*)在测试项目中安装Effort.EF6。我一开始犯了一个错误并安装了Effort(没有EF6位),并且遇到了各种各样的问题。如果您正在使用EF6(或我认为的EF5),请确保安装此版本。

*)修改了MyModel.Context.tt文件以包含一个带有DbConnection的额外构造函数... public MyEntities(DbConnection connection) : base(connection, true) { }

*)将连接字符串添加到测试项目的App.Config文件中。我从数据项目中逐字复制了这个。

*)为测试类添加了初始化方法以设置上下文...

private MyEntities _ctx;
private BaseRepository<Patient> _patientsRepository;
private List<Patient> _patients;

[TestInitialize]
public void Initialize() {
  string connStr = ConfigurationManager.ConnectionStrings["MyEntities"].ConnectionString;
  DbConnection connection = EntityConnectionFactory.CreateTransient(connStr);
  _ctx = new MyEntities(connection);
  _patientsRepository = new BaseRepository<Patient>(_ctx);
  _patients = GetPatients();
}

重要 - 在链接文章中,他使用DbConnectionFactory.CreateTransient(),当我尝试运行测试时,它会出现异常。这似乎是针对Code First的,因为我使用Model First,我不得不将其更改为使用EntityConnectionFactory.CreateTransient()

*)实际测试相当简单。我添加了一些辅助方法来尝试整理它,并使其更具可重用性。在我完成之前,我可能会再进行几轮重构,但这样做有效,而且现在已经足够干净......

[TestMethod]
public void BaseRepository_Update() {
  AddAllPatients();
  Assert.AreEqual(_patients.Count, _patientsRepository.GetAll().Count());
}

#region Helper methods

private List<Patient> GetPatients() {
  return Enumerable.Range(1, 10).Select(CreatePatient).ToList();
}

private static Patient CreatePatient(int id) {
  return new Patient {
    ID = id,
    FirstName = "FirstName_" + id,
    Surname = "Surname_" + id,
    Address1 = "Address1_" + id,
    City = "City_" + id,
    Postcode = "PC_" + id,
    Telephone = "Telephone_" + id
  };
}

private void AddAllPatients() {
  _patients.ForEach(p => _patientsRepository.Update(p));
}

#endregion

在这里需要思想转变的一点是,与Effort不同,与其他模拟不同,你不会告诉模拟框架为特定参数返回什么。相反,你必须把Effort想象成一个真正的数据库,尽管它是一个临时存储器。因此,我在初始化时建立了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。

希望这有助于某人。事实证明,这比我最初尝试的方式要容易得多。

答案 1 :(得分:1)

我创建了一个NSubstitute扩展来帮助对存储库层进行单元测试,您可以在GitHub DbContextMockForUnitTests上找到它。您要引用的主文件是DbContextMockForUnitTests/MockHelpers/MockExtension.cs它在用于使用--Parents whose children contain a subset of children --setup create table #parent ( id int ) create table #child ( parent_id int, foo varchar(32) ) insert into #parent (id) values (1) insert into #parent (id) values (2) insert into #parent (id) values (3) insert into #child (parent_id, foo) values (1, 'buzz') insert into #child (parent_id, foo) values (1, 'buzz') insert into #child (parent_id, foo) values (1, 'fizz') insert into #child (parent_id, foo) values (2, 'buzz') insert into #child (parent_id, foo) values (2, 'fizz') insert into #child (parent_id, foo) values (2, 'bang') insert into #child (parent_id, foo) values (3, 'buzz') --create in calling procedure declare @tblTargets table (strTarget varchar(10)) insert into @tblTargets (strTarget) values ('fizz') insert into @tblTargets (strTarget) values ('buzz') --select query to be called in procedure; -- pass @tblTargets in as TVP, or create from delimited string via splitter function select #parent.id --returns 1 and 2 from #parent inner join #child on #parent.id = #child.parent_id where #child.foo in (select strTarget from @tblTargets) group by #parent.id having count(distinct #child.foo) = (select COUNT(*) from @tblTargets) --cleanup drop table #parent drop table #child 进行测试的同一文件夹中有3个相关代码文件),将所有4个文件复制并粘贴到您的文件中项目。您可以看到此单元测试,其中显示了如何使用它DbContextMockForUnitTests/DbSetTests.cs

为了使其与您的代码相关,我们假设您已复制主文件并在async语句中引用了正确的命名空间。您的代码将是这样的(如果using未被密封,您将不需要更改它,但我仍然认为编码的一般规则是尝试接受可能的最不具体的类型) :

MyEntities

单元测试代码:

// Slight change to BaseRepository, see comments
public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
    private readonly DbContext _ctx; // replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
    private readonly DbSet<T> _dbSet;

    // replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
    public BaseRepository(DbContext ctx) {
        _ctx = ctx;
        _dbSet = _ctx.Set<T>();
    }

    public IEnumerable<T> GetAll() {
        return _dbSet;
    }

    //...
}

免责声明 - 我是上述存储库的作者,但部分基于Testing with Your Own Test Doubles (EF6 onwards)