【问题标题】:How do I unit test a repository that uses DbContext with NSubstitute?如何对使用 DbContext 和 NSubstitute 的存储库进行单元测试?
【发布时间】:2016-07-12 15:54:59
【问题描述】:

我有一个解决方案,其中我有一个包含 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();
}

请注意,正如 Stuart Clement 在 2015 年 12 月 4 日 22:41 的评论中所建议的那样,我将上下文 TT 文件更改为使用 IDbSet

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

_dbSet = _ctx.Set<T>();

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

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

【问题讨论】:

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


    【解决方案1】:

    好吧,我发疯了,试图按照我在问题中展示的方式去做,我遇到了专为该任务设计的 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 视为一个真正的数据库,尽管它是内存中的一个临时数据库。因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。

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

    【讨论】:

    【解决方案2】:

    我创建了一个 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);
    }
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-17
      相关资源
      最近更新 更多