很多时候我们测试一个Action是否按照我们的要求进行动作,这时候就需要测试Controller。但是如果直接测试Controller的方法的话,就涉及到数据库访问,因为controller总是要用到Model中的Repository,而Repository下面就是数据库访问。
Phil Haack在他的视频“Ninja on Fire Black Belt Tips.wmv”中介绍了一种利用Moq实现脱离数据库测试的方法。其核心是利用一个“假的”Repository。他用了一个搜索来展示,我简化了一下,直接测试Index吧。Index几乎每个Controller都会有,原理是一样的。
首先要把Moq下下来,选择适用于自己的.Net版本,添加引用。
在Controller页面做一个小修改,将Repository抽象为一个接口。我假设要对音乐的种类(Genre)来做这一套测试,所以所有的类啊接口啊都是Genre开头的。
首先是Controller
{
public class GenreController : Controller
{
private readonly IGenreRepository _repository;
public GenreController() : this ( new GenreRepository())
{
}
public GenreController(IGenreRepository genreRepository)
{
_repository = genreRepository;
}
//
// GET: /Genre/
public ActionResult Index()
{
var allGenres = _repository.AllGenres;
return View(allGenres.ToList());
}
}
}
看到了吗?现在的GenreController接受一个IGenreRepository作为参数,当然,如果里面用到多个repo,参数自然要多个。但同时他也保留了缺省的无参构造函数,这个函数是为了注入真正的GenreRepository给MVC Framework用的。当然你也可以选择更高级的注入方式,这里维持简单,就在无参构造函数中指定真正给View用的那个IGenreRepository的实现。
然后是IGenreRepository。
{
IQueryable < Genre > AllGenres { get ; }
}
这里只是实例,所以我取了所有的Genre。顺便说一句,为什么要用IQueryable<Genre>,而不用IList<Genre>,这涉及到Linq2SQL的一个迟加载问题。因为每个IQueryable对象,不到真正计算时是不会访问数据库的,就像controller中的
return View(allGenres.ToList());
这两句话中,allGenres定义时不会访问数据库,而allGenres.ToList()时,就会访问数据库了,一般来说我们在repository中尽量用IQueryable。
然后是真正的实现
{
public IQueryable < Genre > AllGenres
{
get { throw new NotImplementedException(); }
}
}
这里抛了个未完成异常,因为真正的实现和本文主题无关,到这里就好。
这些都做完了,基本上给MVC前台用的一套东西就OK了。
下面是测试代码,这里测试我另外开了一个项目,用的是微软自带的测试,也可以用Nunit,没区别。
public void IndexOfAllGenresTest()
{
var repository = new Mock < IGenreRepository > ();
List < Genre > genres = new List < Genre > ()
{
new Genre() {Name = " Test1 " },
new Genre() {Name = " Test2 " }
};
repository.Setup(t => t.AllGenres).Returns(genres.AsQueryable());
GenreController controller = new GenreController(repository.Object);
var result = controller.Index() as ViewResult;
var model = result.ViewData.Model as List < Genre > ;
Assert.AreEqual(model[ 0 ], genres[ 0 ]);
Assert.AreEqual(model[ 1 ], genres[ 1 ]);
Assert.AreEqual(model.Count,genres.Count);
}
注意上面的代码,首先Mock接受IGenreRepository作为构造函数的泛型,生成了一个IGenreRepository的伪实现,然后通过Setup()和Returns()方法为这个伪实现的AllGenres属性(方法也是一样的)赋值。需要说明的是,事实上IGenreRepository.AllGenres是只读的,这里说的赋值只是一种利于理解的说法,事实上是Mock将伪实现的IGenreRepository.AllGenres的get方法代理给return中的内容了。也就是生成了类似于下面的这样一个类。
{
List < Genre > genres = new List < Genre > ()
{
new Genre() {Name = " Test1 " },
new Genre() {Name = " Test2 " }
};
public IQueryable < Genre > AllGenres
{
get { return genres.AsQueryable(); }
}
}
然后通过controller的有参构造为它注入IGenreRepository的实例。后面的逻辑就跟一般的测试没什么区别了。
其实说来说去,最核心的部分就是,测试的时候,Moq强行注入,把Repository从数据库访问给替换成了本地数据集,从而实现了测试对数据库依赖的脱离。
顺便说一下,ASP.NET/MVC上面的入门级视频(其实这个“忍者黑带”视频已经不算入门级了)很值得一看,基本上MVC的来龙去脉,基本原理都讲的很清楚了。与其在网上找来找去或者下一堆不会去看的复杂代码,还不如把这个教程walkthrough一遍,包教包会。