设计一个可以单元测试的类

时间:2011-09-16 16:13:44

标签: asp.net asp.net-mvc unit-testing asp.net-mvc-3 ninject

我正在通过Apress ASP.NET MVC 3 book并尝试确保我为所有可能的事情创建单元测试但是在花了一天的时间试图弄清楚为什么编辑不会保存(参见this SO question我想为此创建一个单元测试。

我已经知道我需要为以下类创建一个单元测试:

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; }
}

我正在使用Ninject.MVC3和Moq并且之前已经创建了几个单元测试(虽然正在处理前面提到过的书),所以我慢慢地了解它。我已经(希望正确地)创建了一个构造函数方法,使我能够传入_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();
    }
}

但这是我开始遇到麻烦的地方......我相信我需要为EFDbContext创建一个接口(见下文),所以我可以用模拟回购替换它来进行测试但它建立在班级DbContext

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

来自System.Data.Entity我不能为我的生活找出如何为它创建一个接口...如果我创建了以下接口,我会因缺少方法{{1}而得到错误来自.SaveChanges()类,我无法使用“DbContext”构建接口,就像`EFDbContext一样,因为它是一个类而不是接口...

DbContext

可以从this page封装上的“源代码/下载”获取原始源代码我在上面的代码片段中遗漏了一些内容(或者只是询问并且我将添加它)。

我达到了我理解的极限,无论我搜索或阅读的内容,我似乎无法弄清楚我是如何理解的。 请帮忙!

2 个答案:

答案 0 :(得分:6)

这里的问题是你没有足够的抽象。抽象/接口的要点是定义一个以技术无关的方式暴露行为的契约。

换句话说,为EFDbContext创建一个接口是一个很好的第一步,但该接口仍然依赖于具体实现--DbSet(DbSet)。

快速修复此问题是将此属性公开为IDbSet而不是DbSet。理想情况下,你会暴露出比IQueryable更抽象的东西(虽然这不会给你Add()方法等)。越抽象,越容易模拟。

然后,您将完成您所依赖的“合同”的其余部分 - 即SaveChanges()方法。

您的更新代码如下所示:

public class EFProductRepository : IProductRepository {
    private IEFDbContext context;

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

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

但是...... 你要问的主要问题是:你想测试什么(相反,你想要嘲笑/避免测试的是什么)?换句话说:您是在尝试验证应用程序在保存某些内容时的工作方式,还是在测试实际保存

如果您只是测试应用程序的工作方式而不关心实际保存到数据库,我会考虑在更高级别进行模拟 - IProductRepository。然后你根本就没有进入数据库。

如果你想确保你的对象实际上被持久化到数据库,那么你应该点击DbContext并且不想在那之后模拟那个部分。

就我个人而言,我认为这两种情况都是不同的 - 同样重要 - 我为每种情况编写了单独的测试:一种测试我的应用程序执行了它应该做的事情,并且另一个测试数据库交互是否有效。

答案 1 :(得分:2)

我猜你当前的代码看起来像这样(我放入界面):

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; }
}

问题是EFProductRepository现在需要一个实现IEFDbContext接口的对象,但是这个接口没有定义我在星号之间放置的行使用的SaveChanges方法,所以编译器启动侵犯。

SaveChanges界面上定义IEFDbContext方法可以解决您的问题:

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

    void SaveChanges();
}