ASP.NET MVC + MVC Contrib + Unit Testing MVC 单元测试

来源:互联网 发布:阿里云邮箱容量超限 编辑:程序博客网 时间:2024/04/28 22:26

最近在写单元测试看了一点文章就记下来了,我的英语不好,也是第一次翻译,也没有太多时间经理还等着呢!!希望大家不要扔板砖!

原文章http://srtsolutions.com/blogs/patricksteele/archive/2009/08/23/asp-net-mvc-mvc-contrib-unit-testing.aspx
ASP.NET MVC + MVC Contrib + Unit Testing
Mvc 模式的一个最大的好处就是它的分层使代码更容易测试,微软意识到这一点的重要性所以可以在它的软件中可以自动

创建一个单独的测试项目。这就一个良好的开始,在此基础上你可以自由扩展。虽然单独的一个功能(可能是方法)在

MSTest中很容易被引用,但是它在使用时要有很多Objtects 做为基础,请求(查询字符串,参数,等等),返回

(cookies,内容的模式,headers,等等),Session等等,在真实的环境中这些Objects 是由ISS处理的结果,但是在测试

环境中你的测试是分开的,不可以把这些引用过来。
我们可以用自己伪造这些基本的Objects,但是这样需要伪造很多,这个最终是MVC Contrib项目要解决的一个重点.(这个

不会翻译)
MVC Contrib Test Helper
MVC模式得到很多赞誉因为它的各种优点-更易控制的界面,容易使用的数据模型,单独的控制区,除了这些还有它还有更

加方便的测试功能,用Rhino.Mocks类库,在MVC Contrib中你可以很方便的初始化以下 对象;

    * HttpRequest
    * HttpResponse
    * HttpSession
    * Form
    * HttpContext
    * and more!
在这一章我将创建一个完整的ASP.NET MVC下测试Controller 的例子,用这几个简单的例子展示整个过程,也加入详细的

说明。
Scenario(整体说明)
我们首先创建一个ASP.NET MVC 项目,里面有一些用户提交的数据,我们要使用"wizard-like"的模式.用户可能首先要填

写的是他的个人信息(姓名),然后就要提交了,最终将返回一个页面。虽然有可能用户可能提交的比这更多的信息但是

这里我就以这种简单的方式开始。
Design(设计)
第一步我们假设只收集这些最基本的个人信息(姓和名),在用户提交后我们将要保存这些基本信息到Cook中,在登录后

他的姓和名很多时间被引用到。用户要执行的动作就是Controller 中这个Cook中用户的代码.再下一步就是利用它获得更

详细的信息。
Set Up(准备工作)
准备好一个新的项目这样你可以在里面创建单元测试,默认创建MVC项目时里面就有一个Home 控制器。为了理简单的说明

测试过程我们就用它来写这上测试。
首先我们不创建一个类来存用户的基本信息,虽然它只用两条信息,但是在实际环境中就不只这两条了,所以我们才创建

类开方便存取信息。


public class SpeakerInfo

{

    public string FirstName { get; set; }

    public string LastName { get; set; }

}

Test#1
首先我们写一个写一个通不过测试,可以看到测试出错的地方,然后我们把它调正确,对比一下看一看测试结果。


[TestMethod]

public void Speaker_WithoutSessionData_Returns_EmptyModel()

{

    var controller = new HomeController();

 

    var result = (ViewResult)controller.Speaker();

    var info = (SpeakerInfo)result.ViewData.Model;

 

    Assert.IsNull(info.FirstName);

    Assert.IsNull(info.LastName);

}
我们要做的就是创建一个Controller 然后调用 提交的的方法。这里我们并没有收集到用户提交的信息:


public ActionResult Speaker()

{

    return View();

}

然后运行我的测试,我们可以看到一个红色的叉,因为我们没有写视图的model,让我们修改一下这个方法:


public ActionResult Speaker()

{

    return View(new SpeakerInfo());

}

运行这个例子,这时候我们可以通过了,好现在我们来改进一下代码,

我们来把用户的基本信息存到Cook当中去,这样以后可以直接取他的信息还可以来判断权限(他们登录后不可能每次提交

都重新填写他的个人信息).如果没有登录的话,我们取这个信息时将返回一个空的值,让我们把它写成一个属性:


private SpeakerInfo SpeakerInfo

{

    get

    {

        var info = Session[SessionKeys.SpeakerInfoKey] as SpeakerInfo;

        if (info == null)

        {

            info = new SpeakerInfo();

        }

 

        return info;

    }

    set

    {

        Session[SessionKeys.SpeakerInfoKey] = value;

    }

}
看代码:你会注意到里面有一个object “SessionKeys”,我不喜欢直接写莫名奇妙的字符串常量,所以我常用常常写一

些静态常量放到一个特定的文件夹下,这样做对以后的扩展性也有很大好处。代码如下:


public static class SessionKeys

{

    public const string SpeakerInfoKey = "SI";

}
现在我们先前写的可以这样写了,不用new 一个 SpeakerInfo 了:


public ActionResult Speaker()

{

    return View(this.SpeakerInfo);

}
现在用运行测试,发现竟然还有一个错误,为什么呢?就是因为这个Session 是空的。我没有运行ASP.NET服务,我们是

单独测试的当然没有Cook对象了,(哈哈总算到了重点了,我想知道的也是这一部分)MVC Contrib 来救命了!!因为我们

会常常用到这个,所以这里写成一个方法了。来仿造ASP.NET中基本的Object:


private static HomeController CreateController()

{

    TestControllerBuilder builder = new TestControllerBuilder();

    return builder.CreateController<HomeController>();

}
这就可以了(这么神奇吗,我不相信),这个HomeController 就是我们仿造出来的方法里面当然有

(Session,Request,Response,……)下面让我们改一下我们的测试代码:


[TestMethod]

public void Speaker_WithoutSessionData_Returns_EmptyModel()

{

    var controller = CreateController();

 

    var result = (ViewResult)controller.Speaker();

    var info = (SpeakerInfo)result.ViewData.Model;

 

    Assert.IsNull(info.FirstName);

    Assert.IsNull(info.LastName);

}
运行这个测试,我们发现成功了!好的开始下一个测试!
(好奇怪啊,我还是有点不明白……!!难道那个TestContrrollerBuiler 就是moq中一员)
#Test #2
这个测试不检查是否我们把用户的基本信息保存到Cook ,并且返回这个用户Object 。这个测试意思是在我登录成功后在

以后的操作中取信息时是否正确。让我们开始写吧,记住在这里一旦我们用上边的方法写入了伪Cook 就可以像真实的对

象一样爽快地用它。


[TestMethod]

public void Speaker_WithSessionData_Returns_PopulatedModel()

{

    var controller = CreateController();

    controller.Session[SessionKeys.SpeakerInfoKey] = new SpeakerInfo {FirstName = "Bob", LastName =

"Smith"};

 

    var result = (ViewResult)controller.Speaker();

    var info = (SpeakerInfo)result.ViewData.Model;

 

    Assert.AreEqual("Bob", info.FirstName);

    Assert.AreEqual("Smith", info.LastName);

}
如果运行一下的话,你会发现依然是正确的,因为我们在上面已经把用户的信息写入到了Session中了。
Test#3
这个测试来检测一下用户是否只输入了姓,而没有输入名,看我们怎么写这个测试:


[TestMethod]

public void Data_Posted_Without_LastName_Returns_Error()

{

    var controller = CreateController();

    var result = (RedirectToRouteResult)controller.Speaker("jim", "");

    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];

 

    Assert.AreEqual("jim", info.FirstName);

    Assert.AreEqual("", info.LastName);

    Assert.AreEqual(1, controller.ModelState.Count);

    Assert.IsTrue(controller.ModelState.ContainsKey("lastName"));

    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());

}
瞧一下最后行代码,这个就是扩展方法了,是用lambdas模式的Nmock(呵,我喜欢看到lambdas,它为我们提供了一个扩

展方法来实现 控制器中的 RedirectToRouteResult ,本来代码应该是这样的(不用mock的helpers):
Assert.AreEqual("Speaker", result.RouteValues["action"]);

Assert.AreEqual("Home", result.RouteValues["controller"]);
(老外的撒娇挺有意思不翻译了,呵呵)
我们还没有写方法来收集用户信息呢,让我们先写一个简单的方法来代替一下:


[AcceptVerbs(HttpVerbs.Post)]

public ActionResult Speaker(string firstName, string lastName)

{

    return null;

}
看到了我加了代码 AccepVerbs 属性是因为我们希望只有用户按<Form>中的Post方法提交时才执行。也看到了,里面有两

个参数一个是"firsName",另一个是"lastName"那是ASP.NET 提供的固定传参模式。
现在我们来完备我们的代码,虽然我们用Cook来存储用户的基本信息,但是和它配套的另一个扩展属性还没有伪造出来,就是那个ModelState 中Error,应该写代码如下:


[AcceptVerbs(HttpVerbs.Post)]

public ActionResult Speaker(string firstName, string lastName)

{

    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };

    if (String.IsNullOrEmpty(lastName))

    {

        ModelState.AddModelError("lastName", "Last Name is Reqiured.");

    }

 

    return this.RedirectToAction(c => c.Speaker());

}
运行测试,我们通过了,现在我们成功写了五个方法中的三个了.
(怎么搞的老外有点直白了,现在对这个有点晕.不加那个ModelState error 不行吗,莫非生成的默认的项目中这个一定要)
Test #4
这个和上一个有点像,是测他输入的姓不能为空,记住我们一开始说的!


[TestMethod]

public void Data_Posted_Without_FirstName_Returns_Error()

{

    var controller = CreateController();

    var result = (RedirectToRouteResult)controller.Speaker("", "jones");

    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];

 

    Assert.AreEqual("", info.FirstName);

    Assert.AreEqual("jones", info.LastName);

    Assert.AreEqual(1, controller.ModelState.Count);

    Assert.IsTrue(controller.ModelState.ContainsKey("firstName"));

    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());

}
这一段代码好像不错唉,但是我们的话就会发现有错误。我们逻辑上并没有犯任何错误啊,让我们再看一看我们的那个Speaker(string,string)方法:(少了一个ModelState error要add的)


[AcceptVerbs(HttpVerbs.Post)]

public ActionResult Speaker(string firstName, string lastName)

{

    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };

    if (String.IsNullOrEmpty(lastName))

    {

        ModelState.AddModelError("lastName", "Last Name is Reqiured.");

    }

    if (String.IsNullOrEmpty(firstName))

    {

        ModelState.AddModelError("firstName", "First Name is Reqiured.");

    }

 

    return this.RedirectToAction(c => c.Speaker());

}
我们完善了代码并且测试都通过了。
再返回来看一看我们的代码,好像还少一个了,如果这两个参数都为空呢。有好多这种情况就被忽视了.下面我来增加这个测试,写这个测试好像是多余的,但是以后代码有可以修改,你不能保证它那时没有用。

我们写的#3和#4是很相似的,除了那个"first","last" name 单词不一样,我们都写了两个 ModelState Error:
public void Data_Posted_Blank_Returns_Error()

{

    var controller = CreateController();

    var result = (RedirectToRouteResult)controller.Speaker("", "");

    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];

 

    Assert.AreEqual("", info.FirstName);

    Assert.AreEqual("", info.LastName);

    Assert.AreEqual(2, controller.ModelState.Count);

    Assert.IsTrue(controller.ModelState.ContainsKey("firstName"));

    Assert.IsTrue(controller.ModelState.ContainsKey("lastName"));

    result.AssertActionRedirect().ToAction<HomeController>(c => c.Speaker());

}
运行测试,是成功的!现在只有一个要写了!
Test #5
这个测试是测在姓和名都不为空时我们要保存这个信息到Cook当中去并且进行下一阶段的工作:


[TestMethod]

public void Data_Posted_To_Speaker_Saves_To_Session_and_Redirects()

{

    var controller = CreateController();

 

    var result = (RedirectToRouteResult)controller.Speaker("jon", "jones");

    result.AssertActionRedirect().ToAction<HomeController>(c => c.SessionDetails());

 

    var info = (SpeakerInfo)controller.Session[SessionKeys.SpeakerInfoKey];

    Assert.AreEqual("jon", info.FirstName);

    Assert.AreEqual("jones", info.LastName);

}
写到这里你也许看到那个SessionDetails()方法,下面我们来写它:


public ActionResult SessionDetails()

{

    return View();

}

完成这一步运行测试,我们发现又有错误了,注意到最后一行speaker(string,string)已经直接将那个Speaker(用户类)返回了没有错误判断,到现在为至我们还没有用到SessionDetails方法,现在是用它的时候了,如果输入信息有误的话将重新回到用户信息speaker,如果正确的话就会调用SessionDetails:


[AcceptVerbs(HttpVerbs.Post)]

public ActionResult Speaker(string firstName, string lastName)

{

    this.SpeakerInfo = new SpeakerInfo { FirstName = firstName, LastName = lastName };

    if (String.IsNullOrEmpty(firstName))

    {

        ModelState.AddModelError("firstName", "First Name is Reqiured.");

    }

    if (String.IsNullOrEmpty(lastName))

    {

        ModelState.AddModelError("lastName", "Last Name is Reqiured.");

    }

 

    if (ModelState.Count != 0)

    {

        return this.RedirectToAction(c => c.Speaker());

    }

 

    return this.RedirectToAction(c => c.SessionDetails());

}
运行测试,通过了,现在我们的所有的测试都通过了。
Conclusion(结尾)
--================================================================================
--==关于MVC Contrib 的dll下载http://www.codeplex.com/MVCContrib

--==
--===================================================================================