测试 MVC3 应用程序的多层






4.89/5 (7投票s)
为 MVC 应用程序的客户端、服务器和 GUI 层创建测试项目 - 以便它们能够被集成到持续集成服务器构建中
介绍
本文的目的是演示如何创建一种多层方法来测试 MVC 应用程序(客户端、服务器、GUI)中的不同框架,以便每个测试项目都可以轻松地集成到持续集成服务器的构建脚本中。我将描述如何将这些过程自动化到 NUnit 框架中。
背景
作为开发者,我们可以轻松地为我们的服务器业务逻辑代码创建 NUnit 测试,这些代码通过依赖注入从应用程序的 GUI 页面中分离出来,但这只是测试循环的一部分。我们需要测试我们的客户端代码,并创建端到端测试,以确保我们的应用程序按预期运行。
使用的技术
客户端
- MockJax (模拟 Ajax JavaScript 调用)
- QUnit (用于测试 JavaScript 代码的框架,结果显示在浏览器模板中)
- NQUnit (将 QUnit 浏览器结果解析为 NUnit 格式)
服务器
GUI
项目结构
图 1 列出了主要的 MVC 项目以及将用于测试不同层的项目。我使用了 NuGet 来轻松地为各种测试项目添加引用和模板结构,以便快速上手。
图 1
MVC 项目
为了使 MVC 项目更具现实性,我将使用 Microsoft 的 Unity 依赖注入来处理一些接口,服务器测试项目(TestServerCode
)将需要模拟这些接口。
在 "Scripts
" 文件夹中,有几个 JavaScript 文件将执行 Ajax 调用(再次使应用程序更具现实性),这些调用需要在客户端测试项目(TestClientCode
)中进行模拟,图 2 显示了主 MVC 项目的结构。
图 2
运行应用程序
如果您下载了该应用程序(确保首先安装 MVC3),然后运行 "TestMvcJQuery
" 项目,您将看到下面显示的屏幕截图。JQuery 控件用于日期选择,以及 Ajax 和标准回发事件,以使应用程序涵盖一般编程场景,而这些场景正是我们想要测试的。唯一需要注意的是图 6,其中通过在 textarea 控件中输入文本来发出 Ajax 调用,并在其下方显示一个标题。
图 3
图 4
图 5
图 6
测试客户端 JavaScript 代码
主要的 MVC 应用程序包含一个您想要测试(并最终将其自动化到您的 CI 服务器构建中)的 JavaScript 文件。这个文件名为 "MvcClient.js
"(图 7),它将包含 MVC 视图将与之交互的 JavaScript 功能 - 因此,这就是我们要测试的 JavaScript 文件。您很可能有很多 JavaScript 文件需要测试,但对于一个文件和多个文件的测试场景是相同的。
图 7
使用的项目引用
在 "TestClientCode
" 项目中,您会看到我添加了以下引用(图 8)。
图 8
使用 包管理器控制台 安装下面的 NuGet NQunit
- NQUnit & NUnit -> Install-Package NQUnit.NUnit
注意。NQunit 会在您的类库项目中创建一个文件夹结构,您可以进行修改(例如,删除它自带的测试 HTML 文件)。
注意。图中红色高亮的库(图 8)需要将其 "Embedded Object Types
" 设置为 "False",并将 "Copy Local
" 设置为 "True",对于您的 JavaScript 文件也必须这样做。
测试夹具代码
我们的测试方法下面将(独立地)调用 MVC 应用程序中的 JavaScript 方法,并在必要时模拟任何 Ajax 调用。正在测试的文件是 "MvcClient.js
" 文件,它位于 "ProductionScripts
" 文件夹中。此文件(在测试项目中)通过主 MVC 项目中的一个生成后事件来保持最新,如图 9 所示。
var timeRun = null;
var fromDate = null;
var toDate = null;
module("Test CalculateDaysBetweenDates Method", {
setup: function () {
timeRun = new Date();
fromDate = new Date("October 06, 1975 11:13:00")
toDate = new Date("October 08, 1975 11:13:00")
},
teardown: function () {
timeRun = null;
}
});
test("Test with date range", function () {
expect(1);
equal(CalculateDaysBetweenDates(fromDate, toDate), 2, "2 days between dates");
});
test("Testing seup - timeRun not null", function () {
expect(1);
notEqual(timeRun, null, "2 days between dates");
});
test("Mock Ajax callback and see 'results' control is updated by success method", function () {
expect(2);
stop(); // stop flow until asyc call is completed
var url = base_url + "Home/Info";
$.mockjax({
url: url, // server path/method
name: "Bert", // data passed to server (will not be used)
responseText: "Well howdy Bert !", // hardcoded response
responseTime: 750
});
// Set default values for future Ajax requests.
$.ajaxSetup({
complete: function () {
ok(true, "triggered ajax request");
start(); // allow tests to continue
equal($("#results").html(), "Well howdy Bert !", "mocked ajax response worked");
}
});
AjaxCallGetServerDateForControl("Bert");
// NB. setTimeout(function () { start(); }, 1000); // -> 1 second to start tests again (as alternative to Start() above)
$.mockjaxClear();
});
图 9
Tests.html
文件将结合测试和要测试的 MVC JavaScript,在浏览器结果中显示(在 QUnit.js
的帮助下)。Test.html 文件的布局如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>QUnit Test Suite</title>
<link rel="stylesheet" href="Scripts/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="Scripts/jquery-1.4.4.js"></script>
<script type="text/javascript" src="Scripts/qunit.js"></script>
<script type="text/javascript" src="Scripts/jquery.mockjax.js"></script>
<!-- Libraries to test: set to copy as post-build event -->
<script language="javascript" type="text/javascript">
var base_url = "localhost"; <!-- Hardcode for tests -->
</script>
<script type="text/javascript" src="ProductionScripts/MvcClient.js"></script>
<!-- Test code -->
<script type="text/javascript" src="TestFixtures/TestFixtures.js"></script>
</head>
<body>
<h1 id="qunit-header">
QUnit Test Suite</h1>
<h2 id="qunit-banner">
</h2>
<div id="qunit-testrunner-toolbar">
</div>
<h2 id="qunit-userAgent">
</h2>
<ol id="qunit-tests">
</ol>
<div id="qunit-fixture">
<!-- Start - Any HTML you may require for your tests to work properly -->
<label id="daysDifferent" />
<div id="results">
</div>
<!-- End - Any HTML you may require for your tests to work properly -->
</div>
</body>
</html>
</p>
一个值得关注的部分是 "<div id="qunit-fixture">
" 元素。在这个 div 中,您将放置 MVC 脚本在 MVC 视图中交互过的任何 GUI 控件。您可以在测试后查询这些控件,以验证一个方法在实际环境中会设置的值。
查看测试结果
要查看测试结果,请在浏览器中查看 Test.html
文件(图 10)。这将在浏览器中显示结果(图 11)。这里需要注意的一点是,最好使用 .html 扩展名而不是 .htm,因为 .html 是 QUnit 模板代码所使用的。
图 10
图 11
使测试脚本可供 NUnit 使用
基本上,由于已经添加了 NQUnit 包,您应该不需要更改任何内容,只需右键单击项目并运行 NUnit(图 12)。这将产生 NUnit GUI,如图 13 所示,请注意,您应该能够在左侧窗格中看到测试脚本文件中的测试方法。现在,客户端 JavaScript 测试就像任何类库测试一样,可以通过(NAnt)构建脚本被 CI 服务器访问。
图 12
图 13
测试服务器端代码
MVC 应用程序使用依赖注入,我们将在自己的测试中模拟它。
使用的项目引用
在 "TestServerCode
" 项目中,您会看到我添加了以下引用(图 14)。 兴趣点
图 14
使用 "包管理器控制台" 安装下面的 NuGet 包
- Moq -> Install-Package Moq
- NUNit -> Install-Package NUnit
您还需要添加对您正在测试的项目(在这种情况下是 "TestMvcJQuery
")的引用。
测试夹具代码
下面是 MVC 应用程序中使用的 "Index" 控制器类的代码,这是我们要测试的。我已经创建了三个测试类来测试 "Index
" 控制器中的每个方法。
public class HomeController : Controller
{
private IDateData dateData{get; set;}
public HomeController(IDateData testService)
{
this.dateData = testService;
}
public ActionResult Index()
{
ViewBag.Message = "Date Calculations";
dateData.DateFrom = "12/01/2012";
dateData.DateTo = this.dateData.GetTime();
dateData.TextArea = "Hello from controller";
return View("Index", dateData);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Info(string name)
{
ViewBag.Message = "Post to info action";
return Json("Well howdy " + name + "!");
}
[HttpPost]
public ActionResult InfoPost(FormCollection formValue)
{
dateData.DateFrom = "12/01/2012";
dateData.DateTo = this.dateData.GetTime();
dateData.TextArea = "Hello from controller";
string txtFieldData = formValue["txtField"].ToString();
ViewData["response"] = "Well howdy " + txtFieldData + " poster";
return View("../Home/Index", dateData);
}
}
下面是 "Index
" 方法的 NUnit 测试类。
public class HomeControllerIndex
{
public HomeControllerIndex() { }
#region Index View Tests
[Test]
public void Index_Should_Return_Type_Of_IndexView()
{
//Arrange
const string indexViewName = "Index";
var dateData = new Mock<IDateData>();
dateData.Setup(req => req.DateFrom).Returns("12/12/2012 14:16:01");
dateData.Setup(req => req.DateTo).Returns("16/12/2012 14:16:01");
dateData.Setup(req => req.TextArea).Returns("Hello there from mock property!");
// Act
var result = new HomeController(dateData.Object).Index() as ViewResult;
IDateData viewData = result.Model as IDateData;
//Assert
Assert.AreEqual(indexViewName, result.ViewName, "View name should have been {0}", indexViewName);
Assert.AreEqual("12/12/2012 14:16:01", viewData.DateFrom);
Assert.AreEqual("16/12/2012 14:16:01", viewData.DateTo);
Assert.AreEqual("Hello there from mock property!", viewData.TextArea);
}
[Test]
public void Index_Should_Return_IDatData_Model()
{
//Arrange
DateData dataDate = new DateData();
//Act
var result = new HomeController(dataDate).Index() as ViewResult;
//Assert
Assert.AreEqual(typeof(DateData), (result.Model.GetType()));
Assert.IsNotNull(result.Model);
Assert.AreEqual("12/01/2012", ((DateData)result.Model).DateFrom);
Assert.AreEqual("23/07/2012 12:59:13", ((DateData)result.Model).DateTo);
Assert.AreEqual("Hello from controller", ((DateData)result.Model).TextArea);
}
[Test]
public void Index_Should_Return_ViewBag_Message()
{
//Arrange
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object);
//Act
result.Index();
//Assert
Assert.AreEqual(result.ViewBag.Message.ToString(), "Date Calculations");
}
#endregion
}
下面是 "Info
" 方法的 NUnit 测试类。
public class HomeControllerInfo
{
private string ActionParam { get; set; }
public HomeControllerInfo() { this.ActionParam = "Bert"; }
#region Info Action (w\String param)- Index View
[Test]
public void Info_Should_Return_Type_Of_Json()
{
//Arrange
var dateData = new Mock<IDateData>();
//Act
var result = new HomeController(dateData.Object).Info(this.ActionParam) as ActionResult;
//Assert
Assert.AreEqual(result.GetType(), typeof(System.Web.Mvc.JsonResult));
}
[Test]
public void Info_Should_Return_ViewBag_Message()
{
//Arrange
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object);
//Act
result.Info(this.ActionParam);
//Assert
Assert.AreEqual(result.ViewBag.Message.ToString(), "Post to info action");
}
#endregion
}
下面是 "InfoPost
" 方法的 NUnit 测试类。
public class HomeControllerInfoParam
{
private FormCollection frmCol;
public HomeControllerInfoParam() { frmCol = new FormCollection(); }
#region Info Action (w/Param) - Index View
[Test]
public void Info_Should_Return_Type_Of_IndexView()
{
//Arrange
const string indexViewName = "../Home/Index";
var dateData = new Mock<IDateData>();
// populate FormCollection
frmCol.Clear();
frmCol.Set("txtField", "Bert");
//Act
var result = new HomeController(dateData.Object).InfoPost(frmCol) as ViewResult;
//Assert
Assert.AreEqual(indexViewName, result.ViewName, "View name should have been {0}", indexViewName);
}
[Test]
public void Info_Should_Return_IDatData_Model()
{
//Arrange
DateData dataDate = new DateData();
// populate FormCollection
frmCol.Clear();
frmCol.Set("txtField", "Bert");
//Act
var result = new HomeController(dataDate).InfoPost(frmCol) as ViewResult;
//Assert
Assert.AreEqual(typeof(DateData), (result.Model.GetType()));
}
[Test]
public void Info_Should_Return_ViewData_Response()
{
//Arrange
string txtFieldData = "Bert";
var dateData = new Mock<IDateData>();
//Act
var result = new HomeController(dateData.Object).InfoPost(frmCol) as ViewResult;
//Assert
Assert.AreEqual(result.ViewData["response"].ToString(), "Well howdy " + txtFieldData + " poster");
}
#endregion
}
查看测试结果
如果您通过 NUnit 运行测试项目(图 15),您应该会看到以下结果,现在我们已经准备好了服务器端测试,可以用于构建脚本。
图 15
集成测试
安装 Firefox IDE
下载并安装 Firefox 插件,然后在您的 Firefox 浏览器中应该会出现 IDE(图 16)。
注意。目前还不能将其添加到任何其他浏览器。
图 16
使用的参考
在 "SeleniumUnitTests
" 项目中,您会看到我添加了以下引用(图 17)。
图 17
使用 "包管理器控制台" 安装下面的 NuGet Selenium
• Selenium -> Install-Package Selenium.WebDriver
创建 Selenium 测试
首先,您需要将您的 MVC 应用程序部署到您的测试服务器,因为测试需要启动浏览器并指向一个有效/存在的 URL 来执行测试。作为 CI 流程的一部分,MVC 应用程序需要在执行任何针对最新代码的测试之前进行部署。
但就我们的目的而言,我们将创建 Selenium 测试并在 NUnit 中运行它们,从而知道这些测试已准备好被 CU 构建脚本拾取并像任何 NUnit 测试项目一样运行。
我已经将我的应用程序部署到了我的本地 IIS 服务器,虚拟目录为 "TestApp
"。现在,我将启动 Firefox Selenium IDE,如图 16 所示。这将显示 Selenium IDE 图 18。有关使用 FF 的 Selenium IDE 的指南可以在这里找到。
图 18
注意右上角的红色(已切换)按钮,这表示插件正在录制。如果您现在导航到部署 MVC 应用程序的位置(https:///AppTest),插件将记录每一次点击和输入。当您对测试满意后,单击红色按钮停止录制。此时,插件的右窗格中将显示(C#)代码。这就是您将复制到 NUnit 测试类中的代码。然后,您将添加一些逻辑来执行实际的断言(使用 Fire-bug 获取页面控件的 ID 以便以后在测试中查询),例如,请参见下方。
[TestFixture]
public class Gui
{
private IWebDriver driver;
private StringBuilder verificationErrors;
private string baseURL;
[SetUp]
public void SetupTest()
{
driver = new FirefoxDriver();
baseURL = "https://";
verificationErrors = new StringBuilder();
}
[TearDown]
public void TeardownTest()
{
try
{
driver.Quit();
}
catch (Exception)
{
// Ignore errors if unable to close the browser
}
Assert.AreEqual("", verificationErrors.ToString());
}
#region Tests
[Test]
public void CallLocalJavascriptMethod_Expect_269DaysDifference()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("datepickerFrom")).Clear();
driver.FindElement(By.Id("datepickerFrom")).SendKeys("03/07/2012");
driver.FindElement(By.Id("validate")).Click();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
Assert.AreEqual(driver.FindElement(By.Id("daysDifferent")).Text, "269");
}
[Test]
public void SubmitButtonPostClickEvent_Expect_WellHodyPoster()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("submit")).Click(); // post
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
Assert.AreEqual(driver.FindElement(By.Id("txtField")).Text, "Well howdy poster");
}
[Test]
public void AjaxCallValidateResponse_Expect_WellHowdyHello()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("txtField")).Clear();
driver.FindElement(By.Id("txtField")).SendKeys("hello");
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(5));
Assert.AreEqual("Well howdy hello!", driver.FindElement(By.Id("results")).Text);
}
[Test]
public void VerifyFromDateControlPopulatedAfterClicking()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("datepickerFrom")).Click();
driver.FindElement(By.LinkText("11")).Click();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
var javascriptExecutor = ((IJavaScriptExecutor)driver);
string dateFrom = javascriptExecutor.ExecuteScript("return datepickerFrom.value").ToString();
Assert.IsTrue(dateFrom.Contains("11"), dateFrom, String.Format("From date is: {0}", dateFrom));
}
[Test]
public void VerifyAbleToTypeIntoFromDateControl()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("datepickerFrom")).Clear();
driver.FindElement(By.Id("datepickerFrom")).SendKeys("08/11/2012");
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
var javascriptExecutor = ((IJavaScriptExecutor)driver);
string dateFrom = javascriptExecutor.ExecuteScript("return datepickerFrom.value").ToString();
Assert.AreEqual("08/11/2012 00:00:00", Convert.ToDateTime(dateFrom).ToString());
}
[Test]
public void VerifyToDateControlPopulatedAfterClicking()
{
driver.Navigate().GoToUrl(baseURL + "/TestApp");
driver.FindElement(By.Id("datepickerTo")).Click();
driver.FindElement(By.LinkText("2")).Click();
var javascriptExecutor = ((IJavaScriptExecutor)driver);
string dateTo = javascriptExecutor.ExecuteScript("return datepickerTo.value").ToString();
Assert.IsTrue(dateTo.Contains("2"), String.Format("To date is: {0}", dateTo));
}
#endregion
}
在测试 Ajax 调用时,您不知道回调何时返回,因此无法测试结果。但是有几种方法可以解决这个问题。一种方法是使用下面的代码。
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
这将使测试等待几秒钟(本例中为 2 秒)后再继续。另一种方法是向您的 C# 代码添加一个布尔值,并添加一个事件来监听它的变化,一旦在您的回调方法中更改了它,您就知道代码已准备好继续测试。但最简单的上手选项是使用 Selenium 等待对象。
针对不同浏览器进行测试
Firefox 启动测试的原因是,我们正在使用 Firefox 驱动程序:
driver = new FirefoxDriver();
例如,要使用 Internet Explorer 或 Chrome 驱动程序,请按照下面的链接下载相应的驱动程序 - 您可以下载多个驱动程序并针对多个浏览器进行测试 - 浏览器版本将取决于该浏览器的默认版本。
在 NUnit 中运行 Selenium 测试
如果您现在在 NUnit 中运行测试,您会注意到会打开一个 Firefox 浏览器,并执行您创建的录制内容,并且您的断言将被评估(图 19)。
图 19
结论
现在,我们已经有了 MVC 应用程序(客户端、服务器和 GUI)的不同层的测试,它们都在 NUnit 中运行。因此,我们现在可以将它们集成到构建脚本中,CI 服务器可以使用该脚本。在我的下一篇博客中,我将展示一个使用三个测试项目并通过 CI 服务器的构建脚本。
有用链接
Qunit
- https://qunit.jqueryjs.cn/
- http://net.tutsplus.com/tutorials/javascript-ajax/how-to-test-your-javascript-code-with-qunit/
- http://www.testdrivenjs.com/getting-started/qunit-setup/
- http://blog.building-blocks.com/javascript-unit-testing-with-qunit-and-sinon-js
- http://docs.jquery.com/QUnit
- http://lostechies.com/chadmyers/2008/08/29/getting-started-with-jquery-qunit-for-client-side-javascript-testing/
NQUnit
- https://github.com/robdmoore/NQUnit
- https://nuget.net.cn/packages/NQUnit
- http://robotfoodsnow.wordpress.com/2012/07/20/unit-testing-with-qunit/
Selenium
- http://seleniumhq.org/projects/
- http://seleniumhq.org/docs/appendix_installing_dotnet_driver_client.html
- http://www.seleniumwiki.com/
- http://iainhunter.wordpress.com/2012/03/29/testing-with-selenium-webdriver-visual-studio-and-nunit/
Selenium & Ajax
- http://draconianoverlord.com/2011/10/14/sane-selenium-testing.html
- http://www.infoq.com/articles/testing-ajax-selenium
- http://sqa.stackexchange.com/questions/160/how-do-you-wait-for-jquery-ajax-calls-to-complete-in-selenium-2
- http://agiletesting.blogspot.co.uk/2006/03/ajax-testing-with-selenium-using_21.html