Как выполнить модульное тестирование репозитория, использующего DbContext с 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, как это было предложено Стюартом Клементом в его комментарии от 04 декабря 2015, в 22:41.

Однако, когда я запускаю этот тест, он дает исключение нулевой ссылки, так как строка в конструкторе базового репозитория, которая устанавливает _dbSet, оставляет его нулевым...

_dbSet = _ctx.Set<T>();

Я предполагаю, что мне нужно добавить еще одну строку, когда я настрою свой фиктивный контекст, но я не уверен, что именно. Я думал, что приведенного выше кода должно быть достаточно для заполнения DbSet.

Кто-нибудь может объяснить, что я пропустил или сделал неправильно?


person Avrohom Yisroel    schedule 12.07.2016    source источник


Ответы (2)


Что ж, сведя себя с ума, пытаясь сделать так, как я показал в своем вопросе, я наткнулся на Усилие, которое было разработано для этой задачи и сопровождалось этот урок, который меня вдохновил. У меня было несколько проблем с его кодом, о которых я объясню ниже.

Короче, что я сделал...

*) Установите 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 как о реальной базе данных, хотя и временной в памяти. Поэтому в инициализации я настроил список фиктивных пациентов, добавил их в базу данных и только потом проводил реальное тестирование.

Надеюсь, это поможет кому-то. Это оказалось намного проще, чем то, как я пытался сделать это изначально.

person Avrohom Yisroel    schedule 12.07.2016
comment
Усилия перемещены сюда: entityframework-effort.net/?z=codeplex - person Matt Becker; 14.07.2020
comment
@MattBecker Спасибо за обновление, Мэтт, я изменил свой ответ, чтобы использовать новую ссылку. - person Avrohom Yisroel; 15.07.2020

Я создал расширение NSubstitute, чтобы помочь модульному тестированию уровня репозитория, вы можете найти его на GitHub DbContextMockForUnitTests. Основной файл, на который вы хотите сослаться, — это DbContextMockForUnitTests/MockHelpers/MockExtension.cs (у него есть 3 зависимых файла кода в той же папке, которая использовалась для тестирования с помощью async), скопируйте и вставьте все 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)

person Igor    schedule 12.07.2016