当前位置:首页 > [翻译] ASP.NET MVC Tip #12 – 仿制控制器上下文

[翻译] ASP.NET MVC Tip #12 – 仿制控制器上下文

点击次数:1691  更新日期:2010-12-31
\n

  原文地址:http://weblogs.asp.net/stephenwalther/archive/2008/06/30/asp-net-mvc-tip-12-faking-the-controller-context.aspx
  
  翻译:Anders Liu
  
  摘要:在这个Tip中,Stephen Walther介绍了在为ASP.NET MVC应用程序创建单元测试时,如何深入ASP.NET内部进行测试。Stephen Walther介绍了如何创建一组标准的仿制对象(Fake Object)来模仿当前用户、当前用户角色、请求参数、会话状态和Cookie。
  
  ASP.NET MVC用程序比ASP.NET Web Forms应用程序更加可测试。ASP.NET MVC的每个特性从设计伊始就一直注意可测试性。然而,ASP.NET MVC应用那个程序中还是有一些方面是难以测试的。尤其你会发现,在ASP.NET MVC中测试ASP.NET内部仍然是一个挑战。
  
  我所说的“ASP.NET内部”是什么意思呢?就是指那些出现在HttpContext中的东西。也就是这些对象:
  
  Request.Forms–POST到一个页面的表单参数。
  
  Request.QueryString–传递到一个页面的查询字符串参数。
  
  User–发起页面请求的当前用户。
  
  Reqest.Cookies–传递到页面的浏览器Cookie。
  
  Session–会话状态对象。
  
  例如,假设你想对一个特定的控制器–其实是一个特定的会话状态条目–进行测试,你需要创建类似下面这样的单元测试:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestSessionState()
  
  {
  
  // Arrange
  
  var controller = new HomeController();
  
  // Act
  
  var result = controller.TestSession() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“wow!”, controller.HttpContext.Session["item1"]);
  
  }
  
  [TestMethod]
  
  public void TestSessionState()
  
  {
  
  // Arrange
  
  var controller = new HomeController();
  
  // Act
  
  var result = controller.TestSession() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“wow!”, controller.HttpContext.Session["item1"]);
  
  }
  
  该测试检查名为TestSession()的控制器操作是否向会话状态中添加了一个新的名叫item1的条目、其值是否为”wow!”。
  
  下面的控制器操作可以通过这一测试:
  
  view plaincopy to clipboardprint?
  
  public ViewResult TestSession()
  
  {
  
  Session["item1"] = “wow!”;
  
  return View();
  
  }
  
  public ViewResult TestSession()
  
  {
  
  Session["item1"] = “wow!”;
  
  return View();
  
  }
  
  该控制操作向会话状态中插入了一个具有期望值的条目。
  
  不幸的是,如果你运行该单元测试,测试会失败。失败的原因是出现了一个NullReferenceException异常。此处的问题在于在单元测试的上下文中,会话状态并不存在。事实上,在一个测试方法中,任何ASP.NET内部的东西都不存在。这意味着你无法测试Cookies、表单参数、查询字符串参数和用户实体或用户角色。
  
  Mocking VS Stubbing
  
  如果你需要编写一个用户到了ASP.NET内部对象的单元测试,那么你必须做出选择。你有两种选择,一是使用Mock Ojbect Framework,或者是使用一组仿制类(Fake Class)。
  
  第一个选择是伪造(mock)ASP.NET内部对象,这可以使用Moq、Typemock Isolator或Rhino Mocks这样的Mock Ojbect Framework来完成。使用这些框架中的任何一种都可以生成假扮ASP.NET内部对象的对象。如果你想更多地了解这些框架,可以看我之前针对这三种Mock Ojbect Framework撰写的博客:
  
  http://weblogs.asp.net/stephenwalther/archive/2008/06/11/tdd-introduction-to-moq.aspx
  
  http://weblogs.asp.net/stephenwalther/archive/2008/03/22/tdd-introduction-to-rhino-mocks.aspx
  
  http://weblogs.asp.net/stephenwalther/archive/2008/03/16/tdd-introduction-to-typemock-isolator.aspx
  
  另外一种选择就是创建一组类,用于模拟ASP.NET内部对象。这组类可以只创建一次,然后在将来所有的ASP.NET MVC项目中使用它们。
  
  在这个Tip中,我将介绍第二种方法。我将向你展示如何通过创建一组标准的ASP.NET内部对象仿制类来简单地测试ASP.NET内部对象,而无需使用Mock Object Framework。
  
  创建仿制控制器上下文
  
  在该Tip的结尾,你可以下载到这些仿制类。我创建了一组ASP.NET内部对象仿制类,名字分别是:
  
  FakeControllerContext
  
  FakeHttpContext
  
  FakeHttpRequest
  
  FakeHttpSessionState
  
  FakeIdentity
  
  FakePrincipal
  
  在单元测试中创建FakeControllerContext的实例,并将其赋值给控制器的ControllerContext属性,就可以开始使用这些仿制类了。例如,下面展示了如何利用FakeControllerContext类来在单元测试中仿制一个特定的用户。
  
  view plaincopy to clipboardprint?
  
  var controller = new HomeController();
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”);
  
  var controller = new HomeController();
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”);
  
  当把FakeControllerContext赋给控制器后,在单元测试的其余部分,控制器将会使用这个上下文。让我们来通过不同的例子看一看如何使用FakeControllerContext来模拟不同的ASP.NET内部对象。
  
  测试表单参数
  
  假设你希望向操作传递不同的表单参数来测试控制器操作的行为。另外,假设控制器操作象下面这样直接访问Reques.Form:
  
  view plaincopy to clipboardprint?
  
  public ActionResult Insert()
  
  {
  
  ViewData["firstname"] = Request.Form["firstName"];
  
  ViewData["lastName"] = Request.Form["lastName"];
  
  return View();
  
  }
  
  public ActionResult Insert()
  
  {
  
  ViewData["firstname"] = Request.Form["firstName"];
  
  ViewData["lastName"] = Request.Form["lastName"];
  
  return View();
  
  }
  
  如何测试控制器操作呢?对于这种情况,你可以使用接受一组表单参数的FakeControllerContext构造器。下面的测试检查了firstName和lastName表单参数是否被保存到了视图数据中:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestFakeFormParams()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake controller context
  
  var formParams = new NameValueCollection { { “firstName”, “Stephen” }, {“lastName”, “Walther”} };
  
  controller.ControllerContext = new FakeControllerContext(controller, formParams);
  
  // Act
  
  var result = controller.Insert() as ViewResult;
  
  Assert.AreEqual(“Stephen”, result.ViewData["firstName"]);
  
  Assert.AreEqual(“Walther”, result.ViewData["lastName"]);
  
  }
  
  [TestMethod]
  
  public void TestFakeFormParams()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake controller context
  
  var formParams = new NameValueCollection { { “firstName”, “Stephen” }, {“lastName”, “Walther”} };
  
  controller.ControllerContext = new FakeControllerContext(controller, formParams);
  
  // Act
  
  var result = controller.Insert() as ViewResult;
  
  Assert.AreEqual(“Stephen”, result.ViewData["firstName"]);
  
  Assert.AreEqual(“Walther”, result.ViewData["lastName"]);
  
  }
  
  FakeControllerContext的表单参数是通过一个NameValueCollection创建的。表单参数的仿制集合被传递给FakeControllerContext的构造器。
  
  测试查询字符串参数
  
  假设你需要测试查询字符串参数是否被传递到一个视图中。查询字符串可以直接通过Request.QueryString集合访问。例如,控制器操作看起来可能是下面这样:
  
  view plaincopy to clipboardprint?
  
  public ViewResult Details()
  
  {
  
  ViewData["key1"] = Request.QueryString["key1"];
  
  ViewData["key2"] = Request.QueryString["key2"];
  
  ViewData["count"] = Request.QueryString.Count;
  
  return View();
  
  }
  
  public ViewResult Details()
  
  {
  
  ViewData["key1"] = Request.QueryString["key1"];
  
  ViewData["key2"] = Request.QueryString["key2"];
  
  ViewData["count"] = Request.QueryString.Count;
  
  return View();
  
  }
  
  在这种情况下,你可以通过将一个NameValueCollection传递给FakeControllerContext的构造器来仿制查询字符串:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestFakeQueryStringParams()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake controller context
  
  var queryStringParams = new NameValueCollection { { “key1″, “a” }, { “key2″, “b” } };
  
  controller.ControllerContext = new FakeControllerContext(controller, null, queryStringParams);
  
  // Act
  
  var result = controller.Details() as ViewResult;
  
  Assert.AreEqual(“a”, result.ViewData["key1"]);
  
  Assert.AreEqual(“b”, result.ViewData["key2"]);
  
  Assert.AreEqual(queryStringParams.Count, result.ViewData["count"]);
  
  }
  
  [TestMethod]
  
  public void TestFakeQueryStringParams()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake controller context
  
  var queryStringParams = new NameValueCollection { { “key1″, “a” }, { “key2″, “b” } };
  
  controller.ControllerContext = new FakeControllerContext(controller, null, queryStringParams);
  
  // Act
  
  var result = controller.Details() as ViewResult;
  
  Assert.AreEqual(“a”, result.ViewData["key1"]);
  
  Assert.AreEqual(“b”, result.ViewData["key2"]);
  
  Assert.AreEqual(queryStringParams.Count, result.ViewData["count"]);
  
  }
  
  注意查询字符串要作为FakeControllerContext构造器的第二个参数传入。
  
  测试用户
  
  你可能需要测试控制器操作的安全性。例如,你可能希望仅为一个特定的已验证用户显示某个视图。下面的控制器操作为已验证用户显示一个Secret视图,而将所有其他用户重定向到Index视图:
  
  view plaincopy to clipboardprint?
  
  public ActionResult Secret()
  
  {
  
  if (User.Identity.IsAuthenticated)
  
  {
  
  ViewData["userName"] = User.Identity.Name;
  
  return View(“Secret”);
  
  }
  
  else
  
  {
  
  return RedirectToAction(“Index”);
  
  }
  
  }
  
  public ActionResult Secret()
  
  {
  
  if (User.Identity.IsAuthenticated)
  
  {
  
  ViewData["userName"] = User.Identity.Name;
  
  return View(“Secret”);
  
  }
  
  else
  
  {
  
  return RedirectToAction(“Index”);
  
  }
  
  }
  
  你可以使用FakeController来测试该操作的行为是否为你预期的:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestFakeUser()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Check what happens for authenticated user
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”);
  
  var result = controller.Secret() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(ViewResult));
  
  ViewDataDictionary viewData = ((ViewResult) result).ViewData;
  
  Assert.AreEqual(“Stephen”, viewData["userName"]);
  
  // Check what happens for anonymous user
  
  controller.ControllerContext = new FakeControllerContext(controller);
  
  result = controller.Secret() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
  
  }
  
  [TestMethod]
  
  public void TestFakeUser()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Check what happens for authenticated user
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”);
  
  var result = controller.Secret() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(ViewResult));
  
  ViewDataDictionary viewData = ((ViewResult) result).ViewData;
  
  Assert.AreEqual(“Stephen”, viewData["userName"]);
  
  // Check what happens for anonymous user
  
  controller.ControllerContext = new FakeControllerContext(controller);
  
  result = controller.Secret() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
  
  }
  
  该测试实际上测试了三样东西(也许应该分成多个测试)。首先,它测试了一个以验证用户是否从控制器操作取得了所需的视图。其次,它测试了已验证用户的用户名是否被成功地添加到了试图数据中。最后,它检查了匿名用户在控制其操作执行时是否被重定向了。
  
  测试用户角色
  
  你可能希望根据角色的不同,为不同的用户显示不同的内容。例如,某些内容可能只能由管理员(Admins)浏览。假设你有一个类似下面这样的控制器操作:
  
  view plaincopy to clipboardprint?
  
  public ActionResult Admin()
  
  {
  
  if (User.IsInRole(“Admin”))
  
  {
  
  return View(“Secret”);
  
  }
  
  else
  
  {
  
  return RedirectToAction(“Index”);
  
  }
  
  }
  
  public ActionResult Admin()
  
  {
  
  if (User.IsInRole(“Admin”))
  
  {
  
  return View(“Secret”);
  
  }
  
  else
  
  {
  
  return RedirectToAction(“Index”);
  
  }
  
  }
  
  如果你是一个具备Admin角色的成员,该控制器操作将返回Secret视图。
  
  你可以通过下面的测试方法测试该控制器操作:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestFakeUserRoles()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Check what happens for Admin user
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”, new string[] {“Admin”});
  
  var result = controller.Admin() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(ViewResult));
  
  // Check what happens for anonymous user
  
  controller.ControllerContext = new FakeControllerContext(controller);
  
  result = controller.Admin() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
  
  }
  
  [TestMethod]
  
  public void TestFakeUserRoles()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Check what happens for Admin user
  
  controller.ControllerContext = new FakeControllerContext(controller, “Stephen”, new string[] {“Admin”});
  
  var result = controller.Admin() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(ViewResult));
  
  // Check what happens for anonymous user
  
  controller.ControllerContext = new FakeControllerContext(controller);
  
  result = controller.Admin() as ActionResult;
  
  Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
  
  }
  
  该操作能够验证是否只有Admin角色的成员能够看到Secret视图。该测试还检查了匿名用户是否被重定向到了其他页面。
  
  测试浏览器Cookies
  
  假设你需要测试访问了浏览器Cookies的操作。例如,你可能通过浏览器端Cookies传递了一个客户ID。如何测试这类操作呢?
  
  下面的控制器方法简单地讲一个浏览器Cookie添加到了视图数据中:
  
  view plaincopy to clipboardprint?
  
  public ViewResult TestCookie()
  
  {
  
  ViewData["key"] = Request.Cookies["key"].Value;
  
  return View();
  
  }
  
  public ViewResult TestCookie()
  
  {
  
  ViewData["key"] = Request.Cookies["key"].Value;
  
  return View();
  
  }
  
  你可以通过创建一个SessionItemCollection并将其传递到FakeControllerContext中来测试该控制器操作:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestCookies()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake Controller Context
  
  var cookies = new HttpCookieCollection();
  
  cookies.Add( new HttpCookie(“key”, “a”));
  
  controller.ControllerContext = new FakeControllerContext(controller, cookies);
  
  var result = controller.TestCookie() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“a”, result.ViewData["key"]);
  
  }
  
  [TestMethod]
  
  public void TestCookies()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake Controller Context
  
  var cookies = new HttpCookieCollection();
  
  cookies.Add( new HttpCookie(“key”, “a”));
  
  controller.ControllerContext = new FakeControllerContext(controller, cookies);
  
  var result = controller.TestCookie() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“a”, result.ViewData["key"]);
  
  }
  
  该测试验证了从知其操作是否将名为key的Cookie添加到了视图数据中。
  
  测试会话状态
  
  最后的一个例子了。我们来看一下测试访问了会话状态的控制器操作:
  
  view plaincopy to clipboardprint?
  
  public ViewResult TestSession()
  
  {
  
  ViewData["item1"] = Session["item1"];
  
  Session["item2"] = “cool!”;
  
  return View();
  
  }
  
  public ViewResult TestSession()
  
  {
  
  ViewData["item1"] = Session["item1"];
  
  Session["item2"] = “cool!”;
  
  return View();
  
  }
  
  该控制器操作同时读和写了会话状态。它从会话状态中取出了一个名为item1的条目,并将其添加到试图数据中。该控制器操作还创建了一个名为item2的会话状态条目。
  
  使用下面的单元测试可以测试该控制器操作:
  
  view plaincopy to clipboardprint?
  
  [TestMethod]
  
  public void TestSessionState()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake Controller Context
  
  var sessionItems = new SessionStateItemCollection();
  
  sessionItems["item1"] = “wow!”;
  
  controller.ControllerContext = new FakeControllerContext(controller, sessionItems);
  
  var result = controller.TestSession() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“wow!”, result.ViewData["item1"]);
  
  // Assert
  
  Assert.AreEqual(“cool!”, controller.HttpContext.Session["item2"]);
  
  }
  
  [TestMethod]
  
  public void TestSessionState()
  
  {
  
  // Create controller
  
  var controller = new HomeController();
  
  // Create fake Controller Context
  
  var sessionItems = new SessionStateItemCollection();
  
  sessionItems["item1"] = “wow!”;
  
  controller.ControllerContext = new FakeControllerContext(controller, sessionItems);
  
  var result = controller.TestSession() as ViewResult;
  
  // Assert
  
  Assert.AreEqual(“wow!”, result.ViewData["item1"]);
  
  // Assert
  
  Assert.AreEqual(“cool!”, controller.HttpContext.Session["item2"]);
  
  }
  
  注意这里创建了一个SessionStateItemCollection并将其传给了FakeControllerContext的构造器。SessionStateItemCollection表示会话状态中的所有条目。
  
  小结
  
  在这个Tip中,我介绍了如何测试ASP.NET内部对象–如表单参数、查询字符串、用户实体、用户角色、Cookies和会话状态–使用的是一组标准的仿制类。通过下面的链接可以下载到这些仿制类的完整代码(同时有C#和VB.NET代码)。
  
来源:http://www.cnblogs.com/AndersLiu/

\n