How to mock an EF6 context and/or the underlying database when the values to be tested reside in the database?

c# entity-framework-6 unit-testing

Question

I'm trying to add unit testing to my projects that use EF6. The TDD approach works fine for simple methods that taken an input and return some output, but I'm not clear on how to make it work for methods that do read/write operations on the database. More specifically, I'm not entirely sure about how to create an in-memory representation of the database (for the test data) and then "mock" the context to point to that in-memory representation.

As an example, please consider the below class (not representative of the actual production code but demonstrates the problem) that reads a file line by line, references a bunch of tables to do some validation and then stores results to two separate database tables -

class Importer { 
    private repository;
    private bool IsValid(string line) {
        //Refer to a bunch of database tables and return true or false. Below is just a random code that demonstrates this
        if(repository.Context.SomeTable1.Count(t => t.Prop1 == line[2]) > 0 &&
            repository.Context.SomeTable2.First(t => t.Prop2 == line[3]).Prop3 != null &&
            ... 
            repository.Context.SomeTable10.Last(t => t.Prop5 == line[5]).Prop9 != null)
            return true;
        return false;
    }
    public Importer(IRepository repository) { this.repository = repository; }
    public void Process(string fileName) {
        ImportLog log = new ImportLog();
        foreach(var line in GetNextLine(fileName) { //GetNextLine reads the file and yield returns lines
            if(IsValid(line)) { //IsValid refers to a bunch of tables and returns true/false
                log.Imported++;
                FileTable fileTable = new fileTable();
                fileTable.Line = line;
                repository.Context.FileTables.Add(fileTable);
                repository.Context.Entry(fileTable).State = EntityState.Added;
                repository.Context.SaveChanges(); //Must save here, can't buffer because the file and lines are too large
            }
            else { log.Rejected++; }
        }
        repository.Context.ImportLogs.Add(log);
        repository.Context.Entry(log).State = EntityState.Added;
        repository.Context.SaveChanges();
    }
}

Now, the only real test that verifies that the class is working is to run the Process method and then check the database to ensure that ImportLog and FileTable tables contain correct values for the given file.

This brings us to my question - How do I create an in-memory representation of the database (that contains ImportLog, FileTable, SomeTable1 to 10 tables and then point the repository.Context to that in-memory representation? Or am I going about it in a completely wrong manner?

Note: I suppose I could create mock CRUDs in the repository instead of just using the DBContext but that would be a momentous effort because the database has close to 100 tables. Just pointing the DBContext to a mocked database solves the problem most efficiently. Alternatively, I could create a real test database but I'm keeping that option only for if an in-memory database solution is not possible.

1
1
2/2/2017 4:31:26 PM

Popular Answer

Your IRepository could be generic and not expose Context directly. By mocking both repositories, you could verify the Add method and only test what Process method is doing.

public interface IRepository<T> : where T : class
{
     void Add(T item);
     IQueryable<T> Query();
     void SaveChanges(bool unitOfWork);
}

public class Repository<T> : IRepository<T>
{
    ...
    public void Add(T item)
    {
        _dbContext.Entry(item).State = EntityState.Added;
    }
    public IQueryable<T> Query()
    {
        return _dbContext.Set<T>().AsQueryable();
    }
    public void SaveChanges(bool unitofWork = false)
    {
        if (!unitofWork)
        {
             _dbContext.SaveChanges();
        }
    }
}

and finally your Importer might look like this...

public class Importer
    {
        private readonly IRepository<FileTable> _fileRepository;
        private readonly IRepository<ImportLog> _importRepo;

        private bool IsValid(string line)
        {
            //Refer to a bunch of database tables and return true or false. Below is just a random code that demonstrates this
            //if (_fileRepository.Query().Count(t => t.Prop1 == line[2]) > 0 &&
            //    _importRepo.Query().First(t => t.Prop2 == line[3]).Prop3 != null

            return false;
        }

        public Importer(IRepository<FileTable> fileRepository, IRepository<ImportLog> importRepo, ILogParser logFile)
        {
            //use DI...
            //var dbContext = new FusionContext();
            //fileRepository = new Repository<FileTable>(dbContext);
            //importRepo = new Repository<ImportLog>(dbContext);

            _fileRepository = fileRepository;
            _importRepo = importRepo;
        }
        public void Process(string fileName)
        {
            var log = new ImportLog();

            //I would use and interface to get logfile
            foreach (var line in _logParser.GetLinesFrom(fileName) { //GetNextLine reads the file and yield returns lines
                if (IsValid(line))
                { //IsValid refers to a bunch of tables and returns true/false
                    log.Imported++;
                    FileTable fileTable = new  FileTable();
                    fileTable.Line = line;
                    _fileRepository.Add(fileTable, true);
                }
                else { log.Rejected++; }
            }

            _importRepo.Add(log, true);

            _importRepo.SaveChanges();

            //because importRepo and fileRepo are using same dbContext instance, they will be saved in one transaction
        }
    }

(Update) Unit test... below example uses moq framework for mocking

public void Should_Add_Logs()
{

    //arrange
    var fileRepoMock = new Mock<IRepository<FileTable>>();

    var importer = new Importer(fileRepoMock.Object,...);

    //action
    importer.Process("path");

    //assert
    fileRepoMock.Verify(r=>r.Add(It.IsAny<FileTable>(),Times.AtMostOnce());
}

Hope this helps

2
2/2/2017 9:55:48 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow