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

2020-10-13  乐帮网

mvc c#

原文章: 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

写给2009年的自己。。。

公众号二维码

关注我的微信公众号
在公众号里留言交流
投稿邮箱:1052839972@qq.com

庭院深深深几许?杨柳堆烟,帘幕无重数。
玉勒雕鞍游冶处,楼高不见章台路。
雨横风狂三月暮。门掩黄昏,无计留春住。
泪眼问花花不语,乱红飞过秋千去。

欧阳修

付款二维码

如果感觉对您有帮助
欢迎向作者提供捐赠
这将是创作的最大动力