繁体   English   中英

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

[英]How do I unit test a repository that uses DbContext with NSubstitute?

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

我添加了一个带有一些常用方法的 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 在 2015 年 12 月 4 日 22:41 发表的评论中所建议的

但是,当我运行此测试时,它给出了一个空引用异常,因为设置_dbSet的基本存储库构造函数中的_dbSet_dbSet为空...

_dbSet = _ctx.Set<T>();

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

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

好吧,在尝试按照我在问题中展示的方式让自己发疯之后,我遇到了专为该任务设计的Effort ,并遵循了本教程,这让我继续前进。 我对他的代码有一些问题,我将在下面解释。

简而言之,我所做的是......

*) 在测试项目中安装 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 视为一个真正的数据库,尽管它是内存中的临时数据库。 因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。

希望这可以帮助某人。 结果证明它比我最初尝试的方式要容易得多。

我创建了一个 NSubstitute 扩展来帮助对存储库层进行单元测试,您可以在GitHub DbContextMockForUnitTests上找到它。 您要引用的主要文件是DbContextMockForUnitTests/MockHelpers/MockExtension.cs它在用于测试async同一文件夹中有 3 个相关代码文件),将所有 4 个文件复制并粘贴到您的项目中。 你可以看到这个单元测试,它展示了如何使用它DbContextMockForUnitTests/DbSetTests.cs

为了使其与您的代码相关,假设您已复制主文件并在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;
    }

    //...
}

单元测试代码:

// unit test
[TestMethod]
public void BaseRepository_GetAll() {
    // arrange

    // this is the mocked data contained in your mocked DbContext
    var patients = new List<Patient>(){
      new Patient(){/*set properties for mocked patient 1*/},
      new Patient(){/*set properties for mocked patient 2*/},
      new Patient(){/*set properties for mocked patient 3*/},
      new Patient(){/*set properties for mocked patient 4*/},
      /*and more if needed*/
    };
    // Create a fake/Mocked DbContext
    var mockedContext = NSubstitute.Substitute.For<DbContext>();
    // call to extension method which mocks the DbSet and adds it to the DbContext
    mockedContext.AddToDbSet(patients);

    // create your repository that you want to test and pass in the fake DbContext
    var repo = new BaseRepository<Patient>(mockedContext);

    // act
    var results = repo.GetAll();

    // assert
    Assert.AreEqual(results.Count(), patients.Count);
}

免责声明 - 我是上述存储库的作者,但它部分基于使用您自己的测试替身进行测试(EF6 以上)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM