Introduction
The purpose of this post, is to demonstrate how to create a
multilayered approach to testing the different frameworks within a MVC
application (client, server, GUI), so that each test project can then be easily
integrated into a build script for your continuous integration server. I’ll describe how you can automate each of these processes
into a NUnit framework.
Background
As developers we can easily create NUnit tests for our server business logic code, that has been encapsulated out from the GUI pages of our application with the use of dependency injection, but that is only part of the testing circle. We need to test our client code and create end to end tests to ensure that our application performs as expected all the way through.
Technologies Used
Client
- MockJax (mock the Ajax JavaScript calls)
- QUnit (framework for testing JavaScript code, results displayed in a browser template)
- NQUnit (parsing QUnit browser results into a NUnit format)
Server
GUI
Project Structure
Fig. 1, lists the main MVC project, and the projects that
will be used to test the different layers. I have made use of NuGet, to easily
add references and boiler template structure to the various test projects, to
get up and running quickly.
Fig 1
The MVC Project
To make the MVC project more realistic, I will be using
Microsoft's Unity dependency injection for some of the interfaces, which the
server test project (TestServerCode
) will have to mock.
Inside the "Scripts
" folder, there are a couple of
JavaScript's files that will perform Ajax calls (again making the application
more realistic) and these calls will need to be mocked in the client test
project (TestClientCode
), fig. 2 displays the structure of the main MVC
project.
Fig 2
Running The Application
If you download the application (making sure to install MVC3 first) and run the "TestMvcJQuery
" project, you will see the screen-shots below being
displayed. JQuery controls are used for the date selection, along with Ajax and
standard post-back events to make the application cover general programming
scenarios, and these are scenarios that we will want to test. Only thing to
note is in fig. 6, where an Ajax call is made by typing text into the textarea
control and a caption is displayed below it.
Fig 3
Fig 4
Fig 5
Fig 6
Testing The Client JavaScript Code
The main MVC application contains a JavaScript file that you
want to test (and eventually automate into your CI server build). This file,
called "MvcClient.js
" (fig 7), will contain our JavaScript
functionality that the MVC views will interact with - so this is the JavaScript file
we will be testing. You most certainly will have a number of JavaScript files
to test, but the test scenario is just the same for one file as it is for
multiple files.
Fig 7
Project References Used
Within the "TestClientCode
" project you will see I
have added the following references (fig 8).
Fig 8
Use the Package Manager Console to install the NuGet NQunit below:
- NQUnit & NUnit -> Install-Package NQUnit.NUnit
NB. NQunit will create a folder structure in your class library project, you can alter this (remove the test html files that come with it for example) .
NB. The libraries highlighted in red (fig 8) will need there "Embedded Object Types
" set to "False" and "Copy Local
" set to "True", you must do this for your JavaScript files also.
Test Fixture Code
Our test methods below, will call the JavaScript methods in our MVC Application (in isolation), mocking any Ajax calls as necessary. The file that is under test is the "MvcClient.js
" file, and this is in a folder called "ProductionScripts
". This file is kept up to date (within the test project) by having a post build event within the main MVC project as in fig 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();
var url = base_url + "Home/Info";
$.mockjax({
url: url,
name: "Bert",
responseText: "Well howdy Bert !",
responseTime: 750
});
$.ajaxSetup({
complete: function () {
ok(true, "triggered ajax request");
start();
equal($("#results").html(), "Well howdy Bert !", "mocked ajax response worked");
}
});
AjaxCallGetServerDateForControl("Bert");
$.mockjaxClear();
});
xcopy "$(ProjectDir)Scripts\MvcClient.js"
"$(SolutionDir)TestClientCode\JavaScriptTests\ProductionScripts\MvcClient.js"
/Y
Fig 9
The Tests.html
file will combine the tests and the MVC JavaScript to be tested into a browser result (with the aid of QUnit.js
). The
layout of the Test.html file is below:
<!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>
<!--
<script language="javascript" type="text/javascript">
var base_url = "localhost"; <!-- Hardcode for tests -->
</script>
<script type="text/javascript" src="ProductionScripts/MvcClient.js"></script>
<!--
<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">
<!--
<label id="daysDifferent" />
<div id="results">
</div>
<!--
</div>
</body>
</html>
</p>
One part of interest is
the "<div id="qunit-fixture">" element,
within this div, you will place any GUI controls that your MVC script would of
interacted with, in the MVC view. You can query these controls after a test to
verify a value that a method would otherwise set in a live environment.
View Test Results
To view the results of your test, view the Test.html
file in
the browser (fig 10). This will display the results in the browser (fig 11). A
thing to note here is that it is best to use the .html extension not the .htm
as .html is what is used by the QUnit template code.
Fig 10
Fig 11
Make Test Scripts Usable By NUnit
Basically, with the NQUnit package already added, you shouldn't
have to change anything, just right click on the project and run NUnit (fig 12).
This will produce the NUnit Gui as in
fig 13, note that you should be able to see the test methods in your test
script file on the left hand pane. So now, the client JavaScript tests are
available to your CI server through a (NAnt) build script just like any class
library tests.
Fig 12
Fig 13
Testing The Server Side Code
The MVC application uses dependency injection which we will mock within our own tests.
Project References Used
Within the "
TestServerCode
" project you will see I have added the following references (fig 14)
Points of Interest
Fig 14
Use the "Package
Manager Console" to install the NuGet packages below:
- Moq -> Install-Package Moq
- NUNit -> Install-Package NUnit
You will also need to add a reference to the project that
you are testing - in this case "TestMvcJQuery
".
Test Fixture Code
Below is the code from the "Index" controller class used within the MVC application, that we will want to test. I have created three test classes to test each method in the "Index
" controller.
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);
}
}
Below is the NUnit test class for the "Index
"
method:
public class HomeControllerIndex
{
public HomeControllerIndex() { }
#region Index View Tests
[Test]
public void Index_Should_Return_Type_Of_IndexView()
{
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!");
var result = new HomeController(dateData.Object).Index() as ViewResult;
IDateData viewData = result.Model as IDateData;
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()
{
DateData dataDate = new DateData();
var result = new HomeController(dataDate).Index() as ViewResult;
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()
{
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object);
result.Index();
Assert.AreEqual(result.ViewBag.Message.ToString(), "Date Calculations");
}
#endregion
}
Below is the NUnit test class for the "Info
"
method:
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()
{
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object).Info(this.ActionParam) as ActionResult;
Assert.AreEqual(result.GetType(), typeof(System.Web.Mvc.JsonResult));
}
[Test]
public void Info_Should_Return_ViewBag_Message()
{
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object);
result.Info(this.ActionParam);
Assert.AreEqual(result.ViewBag.Message.ToString(), "Post to info action");
}
#endregion
}
Below is the NUnit test class for the "InfoPost
"
method:
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()
{
const string indexViewName = "../Home/Index";
var dateData = new Mock<IDateData>();
frmCol.Clear();
frmCol.Set("txtField", "Bert");
var result = new HomeController(dateData.Object).InfoPost(frmCol) as ViewResult;
Assert.AreEqual(indexViewName, result.ViewName, "View name should have been {0}", indexViewName);
}
[Test]
public void Info_Should_Return_IDatData_Model()
{
DateData dataDate = new DateData();
frmCol.Clear();
frmCol.Set("txtField", "Bert");
var result = new HomeController(dataDate).InfoPost(frmCol) as ViewResult;
Assert.AreEqual(typeof(DateData), (result.Model.GetType()));
}
[Test]
public void Info_Should_Return_ViewData_Response()
{
string txtFieldData = "Bert";
var dateData = new Mock<IDateData>();
var result = new HomeController(dateData.Object).InfoPost(frmCol) as ViewResult;
Assert.AreEqual(result.ViewData["response"].ToString(), "Well howdy " + txtFieldData + " poster");
}
#endregion
}
View Test Results
If you run the test project through NUnit (fig 15), you should see the following results, now we have the server side tests ready for a build script.
Fig 15
Integration Tests
Installing Firefox IDE
Download and install the Firefox plug-in, and then you should have the IDE within your Firefox browser (fig 16).
NB. It's not available to add to any other browser at present.
Fig 16
References Used
Within the "SeleniumUnitTests
" project you will see I have added the following references (fig 17)
Fig 17
Use the "Package Manager Console" to install the NuGet Selenium below:
• Selenium -> Install-Package Selenium.WebDriver
Creating Selenium Tests
First you will need to deploy your MVC application to your test server, as the tests will need to start up a browser and point at a valid\existing URL to carry out the tests. As part of your CI process, the MVC application will need to be deployed before carrying out any tests on the latest code.
But for our purpose, we will create Selenium tests and run them in NUnit, thus knowing that the tests are ready to be picked up by a CU build script and run like any NUnit test project.
I have deployed my application to my local IIS server with the virtual directory "TestApp
". Now, I will start-up Firefox Selenium IDE as in fig 16. This will display the Selenium IDE fig 18. A guide on using FF's Selenium IDE is available here.
Fig 18
Notice the red (toggled) button at the top right hand
corner, this indicates that the plug-in is recording, if you now navigate to
where you deployed the MVC application (http://localhost/AppTest), the plug-in
will record each click and entry. When you are happy with your test, click the
red button, so as to stop the recording. There will now be (C#) code in the
right pane of the plug-in. This is the code that you will copy to your NUnit
test class. You will then add some logic to perform the actual assertions (use Fire-bug to get page controls id's to later query in your tests), for e.g.
see below:
[TestFixture]
public class Gui
{
private IWebDriver driver;
private StringBuilder verificationErrors;
private string baseURL;
[SetUp]
public void SetupTest()
{
driver = new FirefoxDriver();
baseURL = "http://localhost";
verificationErrors = new StringBuilder();
}
[TearDown]
public void TeardownTest()
{
try
{
driver.Quit();
}
catch (Exception)
{
}
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();
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
}
When testing Ajax calls, you don't know when the callback will be returned, so as to test the result. But there are a couple of ways to work around this. One is to use the code below:
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(2));
This will make the test wait a number of seconds (in this case 2) before continuing the test. Another way is to add a boolean to your C# code and add an event to listen to it changing, once it has been changed in your callback method, you know you code is ready to continue testing. But the easiest option to get up and running is to use the selenium wait object.
Testing Against Different Browsers
The reason that Firefox starts up the tests, is because we are using the Firefox driver:
driver = new FirefoxDriver();
To use Internet Explorer or Chrome driver for example, follow the link below and download the respective driver - you can download multiple drivers and test against multiple browsers - the browser version will depend on what version of that browser is the default.
Running the Selenium Tests In NUnit
If you now run the tests in NUnit, you will notice that a Firefox browser is opened and the recording that you created is acted out and your assertions are evaluated fig 19.
Fig 19
Conclusion
So now we have the different layers of an MVC application (client, server & GUI) tests running in NUnit. Thus, we can now have them integrated into a build script which can be used by a CI server. In my next blog I will show a build script using the three test projects, by a CI server.
Useful Link
Qunit
NQUnit
Selenium
Selenium & Ajax