如何测试ASP.NET中的Controller

很多时候我们测试一个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

namespace MvcMusicStore.Controllers
{
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。

public interface IGenreRepository
{
IQueryable
<Genre> AllGenres { get; }
}

 

 

这里只是实例,所以我取了所有的Genre。顺便说一句,为什么要用IQueryable<Genre>,而不用IList<Genre>,这涉及到Linq2SQL的一个迟加载问题。因为每个IQueryable对象,不到真正计算时是不会访问数据库的就像controller中的

var allGenres = _repository.AllGenres;
return View(allGenres.ToList());

 

这两句话中,allGenres定义时不会访问数据库,而allGenres.ToList()时,就会访问数据库了,一般来说我们在repository中尽量用IQueryable。

然后是真正的实现

internal class GenreRepository : IGenreRepository
{
public IQueryable<Genre> AllGenres
{
get { throw new NotImplementedException(); }
}
}


这里抛了个未完成异常,因为真正的实现和本文主题无关,到这里就好。

这些都做完了,基本上给MVC前台用的一套东西就OK了。

 

下面是测试代码,这里测试我另外开了一个项目,用的是微软自带的测试,也可以用Nunit,没区别。

Test
[TestMethod]
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中的内容了。也就是生成了类似于下面的这样一个类。

MockGenreRepository
internal class MockGenreRepository : IGenreRepository
{
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一遍,包教包会。

posted @ 2010-07-20 21:52  彭小bo  阅读(1184)  评论(2编辑  收藏  举报