MVC单元测试,使用Repository模式、Ninject、Moq
本篇使用Repository设计MVC项目,使用Ninject作为DI容器,借助Moq进行单元测试。
模型和EF上下文
模型很简单:
public class Foo { public int Id { get; set; } public string Name { get; set; } }
EF上下文为:
using System.Data.Entity; namespace MvcApplication1.Models { public class FooBarContext : DbContext { public DbSet<Foo> Foos { get; set; } } }
Repository相关
为了避免在IXXXRepository中有关增删改查等的重复代码,有必要创建一个所有IXXXRepository的基接口:
using System; using System.Linq; using System.Linq.Expressions; namespace MvcApplication1.Repository { public interface IBaseRepository<T> where T : class { IQueryable<T> GetAll(); IQueryable<T> FindBy(Expression<Func<T, bool>> predicate); void Add(T entity); void Edit(T entity); void Delete(T entity); void Save(); } }
IFooRepository,也可以有自己的接口方法:
using MvcApplication1.Models; namespace MvcApplication1.Repository { public interface IFooRepository : IBaseRepository<Foo> { Foo GetSingle(int fooId); } }
BaseRepository是一个抽象类,提供了所有XXXRepository的泛型基类实现,并实现 IBaseRepository接口:
using System.Data.Entity; using System.Linq; namespace MvcApplication1.Repository { public abstract class BaseRepository<C,T> : IBaseRepository<T> where T : class where C : DbContext,new() { private C _db = new C(); public C Db { get { return _db; } set { _db = value; } } public System.Linq.IQueryable<T> GetAll() { IQueryable<T> query = _db.Set<T>(); return query; } public System.Linq.IQueryable<T> FindBy(System.Linq.Expressions.Expression<System.Func<T, bool>> predicate) { IQueryable<T> query = _db.Set<T>().Where(predicate); return query; } public void Add(T entity) { _db.Set<T>().Add(entity); } public void Edit(T entity) { _db.Entry(entity).State = EntityState.Modified; } public void Delete(T entity) { _db.Set<T>().Remove(entity); } public void Save() { _db.SaveChanges(); } } }
FooRepository不仅派生于BaseRepository<FooBarContext, Foo>,还需要实现IFooRepository约定的接口方法:
using System.Linq; using MvcApplication1.Models; namespace MvcApplication1.Repository { public class FooRepository : BaseRepository<FooBarContext, Foo>,IFooRepository { public Foo GetSingle(int fooId) { var query = GetAll().FirstOrDefault(x => x.Id == fooId); return query; } } }
Ninject控制器工厂
通过GuGet安装Ninjct,创建Ninject控制器工厂:
using System.Web.Mvc; using MvcApplication1.Repository; using Ninject; namespace MvcApplication1.Extension { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); } protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, System.Type controllerType) { return controllerType == null ? null : (IController) ninjectKernel.Get(controllerType); } private void AddBindings() { ninjectKernel.Bind<IFooRepository>().To<FooRepository>(); ninjectKernel.Bind<IBarRepository>().To<BarRepository>(); } } }
并在全局注册:
protected void Application_Start() { ...... ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); }
创建FooController,包含增删改查
using System; using System.Web.Mvc; using MvcApplication1.Models; using MvcApplication1.Repository; namespace MvcApplication1.Controllers { public class FooController : Controller { private readonly IFooRepository _fooRepository; public FooController(IFooRepository fooRepository) { _fooRepository = fooRepository; } public ViewResult Index() { var model = _fooRepository.GetAll(); return View(model); } public ActionResult Details(int id) { var model = _fooRepository.GetSingle(id); if (model == null) { return HttpNotFound(); } return View(model); } public ActionResult Edit(int id) { var model = _fooRepository.GetSingle(id); if (model == null) { return HttpNotFound(); } return View(model); } [ActionName("Edit"), HttpPost] public ActionResult Eidt_Post(Foo foo) { if (ModelState.IsValid) { try { _fooRepository.Edit(foo); _fooRepository.Save(); return RedirectToAction("Details", new { id = foo.Id }); } catch (Exception ex) { ModelState.AddModelError(string.Empty, "出错了:" + ex.Message); } } return View(foo); } public ActionResult Create() { return View(); } [ActionName("Create"), HttpPost] public ActionResult Create_Post(Foo foo) { if (ModelState.IsValid) { try { _fooRepository.Add(foo); _fooRepository.Save(); return RedirectToAction("Details", new {id = foo.Id}); } catch (Exception ex) { ModelState.AddModelError(string.Empty, "出错了:"+ex.Message); } } return View(foo); } public ActionResult Delete(int id) { var model = _fooRepository.GetSingle(id); if (model == null) { return HttpNotFound(); } return View(model); } [ActionName("Delete"),HttpPost] public ActionResult Delete_Post(int id) { var model = _fooRepository.GetSingle(id); if (model == null) { return HttpNotFound(); } _fooRepository.Delete(model); _fooRepository.Save(); return RedirectToAction("Index"); } } }
单元测试
通过NuGet安装Moq,借助Moq来模拟接口方法的返回值。
→初始化
private IFooRepository fooRepository; [TestInitialize] public void Initialize() { Mock<IFooRepository> mock = new Mock<IFooRepository>(); mock.Setup(m => m.GetAll()).Returns(new Foo[] { new Foo(){Id = 1, Name = "Fake Foo 1"}, new Foo(){Id = 2, Name = "Fake Foo 2"}, new Foo(){Id = 3, Name = "Fake Foo 3"}, new Foo(){Id = 4, Name = "Fake Foo 4"} }.AsQueryable()); mock.Setup(m => m.GetSingle(It.Is<int>(i =>i == 1 || i == 2 || i == 3 || i == 4))).Returns<int>(r => new Foo { Id = r, Name = string.Format("Fake Foo {0}", r) }); fooRepository = mock.Object; }
→测试返回类型
[TestMethod] public void is_index_return_model_type_of_iqueryable_foo() { //Arragne FooController fooController = new FooController(fooRepository); //Act var indexModel = fooController.Index().Model; //Assert Assert.IsInstanceOfType(indexModel, typeof(IQueryable<Foo>)); } [TestMethod] public void is_details_returns_type_of_ViewResult() { //Arrange FooController fooController = new FooController(fooRepository); //Act var detailsResult = fooController.Details(1); //Assert Assert.IsInstanceOfType(detailsResult, typeof(ViewResult)); } [TestMethod] public void is_details_returns_type_of_HttpNotFoundResult() { //Arrange FooController fooController = new FooController(fooRepository); //Act var detailsResult = fooController.Details(5); //Assert Assert.IsInstanceOfType(detailsResult, typeof(HttpNotFoundResult)); }
→测试返回集合类型Model的数量
结果:
参考资料:
How to Work With Generic Repositories on ASP.NET MVC and Unit Testing Them By Mocking