Click here to Skip to main content
15,867,453 members
Articles / DevOps / Testing

Test The Multi-Layers Of A MVC3 Application

Rate me:
Please Sign up or sign in to vote.
4.89/5 (7 votes)
3 Apr 2013CPOL8 min read 33.5K   517   32   3
Create test project for client, server and GUI layers of a MVC application - so that they can then be incorporated into a continuous integration server build

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. 

Image 1

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.

Image 2 

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. 

Image 3

Fig 3

Image 4 

Fig 4

Image 5 

Fig 5

Image 6 

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. 

Image 7

Fig 7

Project References Used

Within the "TestClientCode" project you will see I have added the following references (fig 8).

Image 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. 

JavaScript
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();
});
 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: 

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>

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. 

Image 9 

Fig 10

Image 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.

Image 11 

Fig 12

Image 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 

Image 13

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.

C#
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:

C#
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
    }

 Below is the NUnit test class for the "Info" method:

C#
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
    } 

 Below is the NUnit test class for the "InfoPost" method:

C#
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
   }

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.  

Image 14

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. 

Image 15 

Fig 16

References Used 

Within the "SeleniumUnitTests" project you will see I have added the following references (fig 17)

Image 16 

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.

 Image 17

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: 

C#
[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)
            {
                // 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
    } 

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:

C#
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:  

C#
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.

Image 18 

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Ireland Ireland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionwhat is Selenium IDE? Pin
Tridip Bhattacharjee3-Apr-13 8:54
professionalTridip Bhattacharjee3-Apr-13 8:54 
GeneralMy vote of 5 Pin
Kanasz Robert28-Sep-12 5:53
professionalKanasz Robert28-Sep-12 5:53 
Excellent article
SuggestionClickable Links Pin
thatraja31-Jul-12 20:36
professionalthatraja31-Jul-12 20:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.