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

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

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

我添加了一个带有一些常用方法的 BaseRepository,想对其进行单元测试。 class 的顶部看起来像这样...

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

  //...
}

根据我在 找到的代码,我尝试了以下...

[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 = _ctx.Set<T>();

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

谁能解释我错过了什么或做错了什么?

我创建了一个 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. You can see this unit test that shows how to use it 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);
}

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

好吧,在试图按照我在问题中展示的方式去做这件事时,我把自己逼疯了,我遇到了 Effort, which was designed for the task, and followed this tutorial,这让我继续前进。我在他的代码中遇到了一些问题,我将在下面解释。

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

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

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

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

*) 在测试中添加了一个初始化方法 class 来设置上下文...

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 时,您不需要告诉模拟框架 return 特定参数的内容。相反,您必须将 Effort 视为一个真实的数据库,尽管它是内存中的一个临时数据库。因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。

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