簡體   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