3

I am going though the Apress ASP.NET MVC 3 book and trying to ensure I create Unit Tests for everything possible but after spending a good part of a day trying to work out why edit's wouldn't save (see this SO question) I wanted to create a unit test for this.

I have worked out that I need to create a unit test for the following class:

public class EFProductRepository : IProductRepository {
    private EFDbContext context = new EFDbContext();

    public IQueryable<Product> Products {
        get { return context.Products; }
    }

    public void SaveProduct(Product product) {
        if (product.ProductID == 0) {
            context.Products.Add(product);
        }
        context.SaveChanges();
    }

    public void DeleteProduct(Product product) {
        context.Products.Remove(product);
        context.SaveChanges();
    }
}

public class EFDbContext : DbContext {
    public DbSet<Product> Products { get; set; }
}

I am using Ninject.MVC3 and Moq and have created several unit tests before (while working though the previously mentioned book) so am slowly getting my head around it. I have already (hopefully correctly) created a constructor method to enable me to pass in _context:

public class EFProductRepository : IProductRepository {
    private EFDbContext _context;

    // constructor
    public EFProductRepository(EFDbContext context) {
        _context = context;
    }

    public IQueryable<Product> Products {
        get { return _context.Products; }
    }

    public void SaveProduct(Product product) {
        if (product.ProductID == 0) {
            _context.Products.Add(product);
        } else {
            _context.Entry(product).State = EntityState.Modified;
        }
        _context.SaveChanges();
    }

    public void DeleteProduct(Product product) {
        _context.Products.Remove(product);
        _context.SaveChanges();
    }
}

BUT this is where I start to have trouble... I believe I need to create an Interface for EFDbContext (see below) so I can replace it with a mock repo for the tests BUT it is built on the class DbContext:

public class EFDbContext : DbContext {
    public DbSet<Product> Products { get; set; }
}

from System.Data.Entity and I can't for the life of me work out how to create an interface for it... If I create the following interface I get errors due to lack of the method .SaveChanges() which is from the DbContext class and I can't build the interface using "DbContext" like the `EFDbContext is as it's a class not an interface...

using System;
using System.Data.Entity;
using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Concrete {
    interface IEFDbContext {
        DbSet<Product> Products { get; set; }
    }
}

The original Source can be got from the "Source Code/Downloads" on this page encase I have missed something in the above code fragments (or just ask and I will add it).

I have hit the limit of what I understand and no mater what I search for or read I can't seem to work out how I get past this. Please help!

Community
  • 1
  • 1
GazB
  • 3,510
  • 1
  • 36
  • 42

2 Answers2

6

The problem here is that you have not abstracted enough. The point of abstractions/interfaces is to define a contract that exposes behavior in a technology-agnostic way.

In other words, it is a good first step that you created an interface for the EFDbContext, but that interface is still tied to the concrete implementation - DbSet (DbSet).

The quick fix for this is to expose this property as IDbSet instead of DbSet. Ideally you expose something even more abstract like IQueryable (though this doesn't give you the Add() methods, etc.). The more abstract, the easier it is to mock.

Then, you're left with fulfilling the rest of the "contract" that you rely on - namely the SaveChanges() method.

Your updated code would look like this:

public class EFProductRepository : IProductRepository {
    private IEFDbContext context;

    public EFProductRepository(IEFDbContext context) {
        this.context = context;
    }
    ...
}

public interface IEFDbContext {
   IDbSet<Product> Products { get; set; }
   void SaveChanges();
}

BUT... the main question you have to ask is: what are you trying to test (conversely, what are you trying to mock out/avoid testing)? In other words: are you trying to validate how your application works when something is saved, or are you testing the actual saving.

If you're just testing how your application works and don't care about actually saving to the database, I'd consider mocking at a higher level - the IProductRepository. Then you're not hitting the database at all.

If you want to make sure that your objects actually get persisted to the database, then you should be hitting the DbContext and don't want to mock that part after all.

Personally, I consider both of those scenarios to be different - and equally important - and I write separate tests for each of them: one to test that my application does what it's supposed to do, and another to test that the database interaction works.

Jess Chadwick
  • 2,373
  • 2
  • 21
  • 24
  • I have (or at least the book has) already got a unit test for IProductRepository and that tests how the application manages saves but it didn't go to a level to check that EFProductRepostiory worked (and the example in the book doesn't!) so I wanted to add something for that. Testing that the data is saved to the db it's self seems to me like another level of testing and again I am not sure the correct way to go about this as I guess if I'm not using a mock repo I would need to be using scripts to create / wipe db's... Is that level of testing normal? Isn't that testing MySQL and not the app!? – GazB Sep 16 '11 at 20:40
  • @Zasurus: Testing that the database layer works is an important step which falls under either *integration* or *system* testing. There are a lot of problems that system tests can catch that unit tests can't (bad database connection strings, EF models out of sync with the database schema, etc). On the other hand, your repository code appears to be so simple, many people ([*cough* Joel Spolsky]) would argue that by unit testing it you're introducing more maintenance overhead with very little potential for actually catching an error. – StriplingWarrior Sep 16 '11 at 21:17
  • As it's only one table for that application I agree it is simple but the aim of this is project is to learn it's not going to ever be a live application. Although I am using what I learn to create the start of an application that will become live. Which will hopefully grow to a decent size and therefore I want to ensure I at least get my head around the correct way to do these on this app so I can apply it to the real one. :) The current version of the live app is over 150 tables (although mess hence the rewrite in MVC) so I am sure Unit testing will help there! Will try above shortly. – GazB Sep 16 '11 at 21:40
  • 1
    @Zasurus: *SHAMELESS SELF-PROMOTION*: I did a talk about Unit testing (versus Integration testing) that I recorded. You can check it out here: http://vimeo.com/21785812 – Jess Chadwick Sep 19 '11 at 01:30
  • @Jess Chadwick: Thanks the start of the video was useful but really someone needs to buy you a new camera! ;) Once you got into the code that you couldn't read I got completely lost :( Guess you don't have that in HD do you? Or at least with the projected output superimposed on the top right of the screen as your explanation was quite easy to follow. :) Thanks anyway for your help. Didn't get the unit test to work in the end but did get past this problem thanks to your's and Jeroen help. Time restraints on current projects mean I will have to come back to this after xmas. – GazB Sep 19 '11 at 08:39
2

I guess your current code looks something like this (I put in the interface):

public class EFProductRepository : IProductRepository {
    private IEFDbContext _context;

    // constructor
    public EFProductRepository(IEFDbContext context) {
        _context = context;
    }

    public IQueryable<Product> Products {
        get { return _context.Products; }
    }

    public void SaveProduct(Product product) {
        if (product.ProductID == 0) {
            _context.Products.Add(product);
        } else {
            _context.Entry(product).State = EntityState.Modified;
        }
        **_context.SaveChanges();**
    }

    public void DeleteProduct(Product product) {
        _context.Products.Remove(product);
        **_context.SaveChanges();**
    }
}

public class EFDbContext : DbContext, IEFDbContext {
    public DbSet<Product> Products { get; set; }
}

public interface IEFDbContext {
    DbSet<Product> Products { get; set; }
}

The problem is EFProductRepository now expects an object implementing the IEFDbContext interface, but this interface does not define the SaveChanges method used at the lines I put between the asteriskes so the compiler starts complaining.

Defining the SaveChanges method on the IEFDbContext interface solves your problem:

public interface IEFDbContext {
    DbSet<Product> Products { get; set; }

    void SaveChanges();
}
Jeroen
  • 1,246
  • 11
  • 23
  • I see... I did consider this but thought there must be another way of doing it! :) I will give it a go and see how it goes... Will get back to you. Thanks :) – GazB Sep 16 '11 at 20:23