1

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.

Achilles
  • 1,099
  • 12
  • 29
  • Don't you also have a dependency on the file system that you would need mock out for unit testing? – Fran Feb 02 '17 at 17:07
  • Abstract all the things. This sounds more like integration testing than unit testing.unites should be tested in isolation with any dependencies mocked/faked. – Nkosi Feb 02 '17 at 17:11
  • @Fran Yes, but that's easier to manage by hard coding a few things in the tests. It's the mocking of the database/context that trumps me because creating and maintaining a fake repository for so many tables becomes a long term maintenance problem (aside from the immediate development/test effort). – Achilles Feb 02 '17 at 17:28
  • @Nkosi Good point about the integration vs unit test but in this class the smallest testable "unit" is the `Process` method. In what ways would integration testing process differ though in this case? – Achilles Feb 02 '17 at 17:31
  • @Achilles, The `Process` method is too tightly coupled to implementation concerns and should be refactored to depend more on abstractions that would allow it to be easier to isolate the method for testing. – Nkosi Feb 02 '17 at 18:33
  • @Achilles The main question is - when you mock your database, what are you trying to test and WHY? *Hint: you can't test database layer without a database.* – trailmax Feb 02 '17 at 22:09
  • @trailmax In this scenario, we're looking to test that the Importer class is able to import the file and log the import results correctly. Yes, I see what you mean about testing the database and I'm very much inclined to just create a test database, but apparently it's considered bad for unit tests to hit databases which is making me do this research. – Achilles Feb 02 '17 at 23:14
  • @Achilles Call them integration tests and use a test database. Here is how I do it: http://tech.trailmax.info/2014/03/how-we-do-database-integration-tests-with-entity-framework-migrations/ – trailmax Feb 03 '17 at 10:00
  • @Achilles and if you getting into nitty-gritty of definition of Unit test, if you are reading a file, then it is not a unit test already. – trailmax Feb 03 '17 at 10:01

2 Answers2

2

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

praveen
  • 86
  • 3
  • Thank you. How to create a generic enough mock repository though that doesn't have much long term maintenance overhead (so that we don't end up using a lot of time testing the repository created for unit-testing)? Also, an unrelated question - Does `_dbContext.Set().AsQueryable()` retrieve all of the table data and then make it `AsQueryable()` or it loads as needed by a predicate? – Achilles Feb 02 '17 at 18:42
  • You don't have to create repository for unit testing, instead you just have to mock them. Look at the unit test example I have added to Answer. And about `.AsQueryable()`, please refer to this answer [Using IQueryable with Linq](http://stackoverflow.com/questions/1578778/using-iqueryable-with-linq) – praveen Feb 02 '17 at 22:05
  • Thank you, this is very helpful. I'm not familiar with Moq yet, I'll try it out now. – Achilles Feb 02 '17 at 23:19
0

For this you can implement the "Repository" pattern. At high level, for your DB related logic you can create an interface that contains methods you need to use on your DB entities. Then, you will have 2 repository classes, both implement the interface. One will work with the actual EF context and access the DB, and the other will work with in memory collections of data. You client code will be able to work with either implementation for testing or running the logic against the DB. Look here for a walk through. Also, this pattern has been recommended by the Asp.Net team.

artist_dev
  • 163
  • 2
  • 11
  • Thank you. Sure, but as I mentioned creating CRUDs and other operations for all tables would be a lot of work and I'm worried that maintaining that fake repository would end up becoming a long term future maintenance nightmare (testing the fake repository, keeping it in sync with the database). So, I'm hoping if there's something out there that can fully/partially automate all this and preferably works without changing the existing by code (that is, just pointing the DBContext to s fake database). – Achilles Feb 02 '17 at 17:15
  • 1
    Great. In that case you might want to take a look at some mocking tool, like [Moq](http://www.nuget.org/packages/Moq/). – artist_dev Feb 02 '17 at 17:53