简体   繁体   English

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

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

I have a solution in which I have a Data project that contains an EF6 .edmx file, generated from an existing database.我有一个解决方案,其中我有一个包含从现有数据库生成的 EF6 .edmx 文件的数据项目。 I split the entities into a separate Entities project, and have a Repositories project that references them both.我将实体拆分为一个单独的 Entities 项目,并有一个引用它们的 Repositories 项目。

I have added a BaseRepository with some common methods, and want to unit test it.我添加了一个带有一些常用方法的 BaseRepository,并希望对其进行单元测试。 The top of the class looks like this...班级的顶部看起来像这样......

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

  //...
}

Following the code I found at https://stackoverflow.com/a/21074664/706346 , I tried the following...按照我在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();
}

Note that I changed the context TT file to use IDbSet, as suggested by Stuart Clement in his comment on Dec 4 '15 at 22:41请注意,我将上下文 TT 文件更改为使用 IDbSet,正如 Stuart Clement 在 2015 年 12 月 4 日 22:41 发表的评论中所建议的

However, when I run this test, it gives a null reference exception, as the line in the base repository constructor that sets _dbSet leaves it null...但是,当我运行此测试时,它给出了一个空引用异常,因为设置_dbSet的基本存储库构造函数中的_dbSet_dbSet为空...

_dbSet = _ctx.Set<T>();

I would guess I need to add another line when I set up my mock context, but I'm not sure what.我想在设置模拟上下文时需要添加另一行,但我不确定是什么。 I thought the code above should be enough to populate the DbSet.我认为上面的代码应该足以填充 DbSet。

Anyone able to explain what I've missed or done wrong?任何人都能够解释我错过了什么或做错了什么?

Well, having driven myself mad trying to do it the way I showed in my question, I came across Effort , which was designed for the task, and followed this tutorial , which got me going.好吧,在尝试按照我在问题中展示的方式让自己发疯之后,我遇到了专为该任务设计的Effort ,并遵循了本教程,这让我继续前进。 I had a few problems with his code, which I'l explain below.我对他的代码有一些问题,我将在下面解释。

Briefly, what I did was...简而言之,我所做的是......

*) Install Effort.EF6 in the test project. *) 在测试项目中安装 Effort.EF6。 I made a mistake at first and installed Effort (without the EF6 bit), and had all sorts of problems.我一开始犯了一个错误,安装了 Effort(没有 EF6 位),但遇到了各种各样的问题。 If you're using EF6 (or EF5 I think), make sure you install this version.如果您使用的是 EF6(或我认为是 EF5),请确保安装此版本。

*) Modified the MyModel.Context.tt file to include an extra constructor that took a DbConnection... public MyEntities(DbConnection connection) : base(connection, true) { } *) 修改 MyModel.Context.tt 文件以包含一个额外的构造函数,该构造函数采用 DbConnection... public MyEntities(DbConnection connection) : base(connection, true) { }

*) Added the connection string to the test project's App.Config file. *) 将连接字符串添加到测试项目的 App.Config 文件中。 I copied this verbatim from the data project.我从数据项目中逐字复制了这个。

*) Added an initialisation method to the test class to set up the context... *) 向测试类添加了初始化方法以设置上下文...

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

Important - In the linked article, he uses DbConnectionFactory.CreateTransient() , which gave an exception when I tried to run the tests.重要- 在链接的文章中,他使用了DbConnectionFactory.CreateTransient() ,当我尝试运行测试时它给出了一个异常。 It seems that this is for Code First, and as I'm using Model First, I had to change it to use EntityConnectionFactory.CreateTransient() instead.似乎这是针对 Code First 的,因为我使用的是 Model First,所以我不得不将其更改为使用EntityConnectionFactory.CreateTransient()

*) The actual test was fairly simple. *) 实际测试相当简单。 I added some helper methods to try and tidy it up, and make it more reusable.我添加了一些辅助方法来尝试整理它,并使其更具可重用性。 I'll probably do several more rounds of refactoring before I'm done, but this works, and is clean enough for now...在我完成之前,我可能会再做几轮重构,但这有效,并且现在足够干净......

[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

The bit that needed a mind-shift here was that with Effort, unlike other mocking, you don't tell the mocking framework what to return for a particular argument.这里需要转变思路的是,Effort 与其他模拟不同,您不会告诉模拟框架针对特定参数返回什么。 Instead, you have to think of Effort as a real database, albeit a temporary one in memory.相反,您必须将 Effort 视为一个真正的数据库,尽管它是内存中的临时数据库。 Therefore, I set up a list of mock patients in the initialisation, added them to the database, and only then did the actual testing.因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。

Hope this helps someone.希望这可以帮助某人。 It turned out to be a lot easier than the way I was trying to do it originally.结果证明它比我最初尝试的方式要容易得多。

I created a NSubstitute extension to help unit test the repository layer, you can find it on GitHub DbContextMockForUnitTests .我创建了一个 NSubstitute 扩展来帮助对存储库层进行单元测试,您可以在GitHub DbContextMockForUnitTests上找到它。 The main file you want to reference is DbContextMockForUnitTests/MockHelpers/MockExtension.cs ( it has 3 dependent code files in that same folder used for testing with async ) , copy and paste all 4 files into your project.您要引用的主要文件是DbContextMockForUnitTests/MockHelpers/MockExtension.cs它在用于测试async同一文件夹中有 3 个相关代码文件),将所有 4 个文件复制并粘贴到您的项目中。 You can see this unit test that shows how to use it DbContextMockForUnitTests/DbSetTests.cs .你可以看到这个单元测试,它展示了如何使用它DbContextMockForUnitTests/DbSetTests.cs

To make that relevant to your code lets assume that you have copied the main file and referenced the correct namespace in your using statements.为了使其与您的代码相关,假设您已复制主文件并在using语句中引用了正确的命名空间。 Your code would be something like this ( If MyEntities is not sealed you would not need to change it but I still would as a general rule of coding is try accept the least specific type possible ):您的代码将是这样的(如果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 code:单元测试代码:

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

Disclaimer - I am the author of the aforementioned repository but it was partially based on Testing with Your Own Test Doubles (EF6 onwards)免责声明 - 我是上述存储库的作者,但它部分基于使用您自己的测试替身进行测试(EF6 以上)

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

相关问题 如何正确地对与Entity Framework / DbContext耦合的存储库模式方法进行单元测试? - How do I properly unit test a repository-pattern method, that has coupling to Entity Framework / DbContext? 如何使用NSubstitute模拟DbContext,然后添加/删除数据 - How do I mock DbContext using NSubstitute and then add/remove data 使用NUnit和NSubstitute对通用存储库进行单元测试 - Unit test of generic repository using NUnit and NSubstitute 如何对使用DbContext.Database.SqlQuery的方法进行单元测试<T> - How to unit test method that uses DbContext.Database.SqlQuery<T> 如何编写使用HttpContext的服务器端单元测试? - How do I write a server side unit test that uses a HttpContext? 如何对使用Fluent界面的代码进行单元测试? - How do I unit test code that uses a Fluent interface? 使用 NSubstitute 单元测试 Hangfire - Unit test Hangfire with NSubstitute 如何模拟 DbContext 以在存储库的单元测试中使用? - How can I mock a DbContext to use within unit tests of a repository? 如何对使用Entity Framework的存储库模式进行单元测试? - How to unit test a repository pattern that uses Entity Framework? Mocking DbContext 与 NSubstitute 和 Repository 为空或 NotImplementedException - Mocking DbContext with NSubstitute and Repository is empty or NotImplementedException
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM