简体   繁体   中英

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. I split the entities into a separate Entities project, and have a Repositories project that references them both.

I have added a BaseRepository with some common methods, and want to unit test it. 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...

[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

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 = _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.

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. 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. I made a mistake at first and installed Effort (without the EF6 bit), and had all sorts of problems. If you're using EF6 (or EF5 I think), make sure you install this version.

*) Modified the MyModel.Context.tt file to include an extra constructor that took a DbConnection... public MyEntities(DbConnection connection) : base(connection, true) { }

*) Added the connection string to the test project's App.Config file. 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. 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.

*) 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. Instead, you have to think of Effort as a real database, albeit a temporary one in memory. 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 . 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 .

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. 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 ):

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM