locked
unit testing an MVC controller RRS feed

  • Question

  • User1571524970 posted

    Hi guys,

    I am trying to get some unit testing up and running for my controllers. I am basically trying to test that the model which is returned contains the correct information for a specific View model. In order to mirror the model data  from a database I created a Booking object which would hold the same data that would be returned from the db

    // GET: Booking/Details/5
            public ActionResult Details(int? id)
            {
                if (id == null)
                {
                    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
                }
                Booking booking = db.Bookings.Find(id);
                if (booking == null)
                {
                    return HttpNotFound();
                }
                return View(booking);
            }

    My Unit Test as it stands:

    namespace BookingSystem.Tests.Controllers
    {
        [TestClass]
        public class BookingControllerTest
        {
            [TestMethod]
            public void TestDetailsView()
            {
                // Arrange
                // Make Booking object to represent data from database
                Booking booking = new Booking()
                {
                    Id = 2,
                    AcademicYear = "2009",
                    Address1 = "Chapelizord",
                    Address2 = "Dublin",
                    Address3 = "Co. Dublin",
                    Eircode = "D20WP68",
                    ClassGroups = "4th Years",
                    County = "Dublin",
                    Date = DateTime.Today,
                    DeisSchool = "No",
                    Email = "office@prtb.ie",
                    EndTime = "15:00",
                    FeePayingSchool = "Yes",
                    GaeltachtArea = "No",
                    LecturerName = "John Doe",
                    OfficialSchoolName = "Loreto",
                    PhoneNumber = "01-4595324",
                    PrincipalName = "John Doe",
                    Religion = "Catholic",
                    RollNumber = "60010P",
                    StartTime = "10:00",
                    SchoolGender = "mixed",
                    Surveys = true,
                    TeacherName = "Ms Smith",
                    Topics = "Intro",
                    TotalBoys = 105,
                    TotalGirls = 100,
                    TotalPupils = 205,
                    X = 320163.012m,
                    Y = 730088.854m,
                    Latitude = 53.61m,
                    Longitude = -6.18m,
                    ITMEast = 72008.39m,
                    ITMNorth = 764023.21m
                };
            
                var controller = new BookingController();
    
                //Act
                var result = controller.Details(2) as ViewResult;
                var model = result.Model as Booking;
    
                //Assert
                //Assert.IsNotNull(result);
                Assert.AreEqual(booking.OfficialSchoolName, model.OfficialSchoolName);
            }
        }
    }

    The test fails with the following message:

    Message:
    Test method BookingSystem.Tests.Controllers.BookingControllerTest.TestDetailsView threw exception:
    System.InvalidOperationException: No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SqlClient'. Make sure the provider is registered in the 'entityFramework' section of the application config file. See http://go.microsoft.com/fwlink/?LinkId=260882 for more information.

    Any help is greatly appreciated

    Sunday, February 23, 2020 2:20 PM

Answers

  • User475983607 posted

    I tried to a really simple test to test a ViewBags contents

    Again, your testing approach does not make logical sense.  Why are you testing the ViewBag?  Microsoft already tested the ViewBag.   It's certainly possible to test the ViewBag but you need to create an HttpContext.  There are samples on GitHub if you wish to take a look but again this testing has already be done.

    https://github.com/aspnet/Mvc

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Saturday, April 11, 2020 7:53 PM

All replies

  • User1120430333 posted

    Yeah, the UT is going to blow up,  because  the Act on the controller method is executing the code to use EF and access the database. If you do get the test to work as it stands in using EF and accessing the database, then it's not a UT anyway.

    https://www.artima.com/weblogs/viewpost.jsp?thread=126923

    If you were following the SoC principles in MVC, then the controller wouldn't have direct database access in the controller method using EF, and EF and the database access would be in a class in the Models folder with  the class implementing an Interface. That way when running the UT and doing the ACT on the controller, the class/object in the Models folder can be mocked out and made to return the mocked object you created in the Assert.

    https://en.wikipedia.org/wiki/Separation_of_concerns

    https://www.c-sharpcorner.com/UploadFile/56fb14/understanding-separation-of-concern-and-Asp-Net-mvc/

    https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/overview/understanding-models-views-and-controllers-cs

    <copied>

    An MVC model contains all of your application logic that is not contained in a view or a controller. The model should contain all of your application business logic, validation logic, and database access logic. For example, if you are using the Microsoft Entity Framework to access your database, then you would create your Entity Framework classes (your .edmx file) in the Models folder.

    A view should contain only logic related to generating the user interface. A controller should only contain the bare minimum of logic required to return the right view or redirect the user to another action (flow control). Everything else should be contained in the model.

    In general, you should strive for fat models and skinny controllers. Your controller methods should contain only a few lines of code. If a controller action gets too fat, then you should consider moving the logic out to a new class in the Models folder.

    <end>

    https://stackoverflow.com/questions/6684480/moq-how-to-return-a-mocked-object-from-a-method

    https://justintimecoder.com/useful-skills-with-moq/

    I can easily run a UT on the Action Method by mocking out GetAll() that is in the AuthorDM class that's sitting in the Models folder and Arranging the test data that would be  returned out of the mock. 

            public IActionResult Index() 
            {
                return View(_authorDM.GetAll());
            }
            
      

    This is what you have to do with most UT(s) is that you have to mock objects.

    Your other option is to try and mock-out EF. I am not going to get into that. There are articles on that using Bing or Google. But to keep it simple create a class in the Models folder, put the database code in the class and have the controller call the method on the class to return the data to the controller with the controller passing it to the view so that you can mock the object out during a UT.

    Sunday, February 23, 2020 4:45 PM
  • User-17257777 posted

    Hi darego,

    System.InvalidOperationException: No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SqlClient'.

    From the error message, it seems that you didn't install EF in your test project. You can try to include it.

    Best Regards,

    Jiadong Meng

    Monday, February 24, 2020 9:28 AM
  • User1571524970 posted

    I can easily run a UT on the Action Method by mocking out GetAll() that is in the AuthorDM class that's sitting in the Models folder and Arranging the test data that would be  returned out of the mock. 

            public IActionResult Index() 
            {
                return View(_authorDM.GetAll());
            }
            
      

    This is what you have to do with most UT(s) is that you have to mock objects.

    I am revisiting my unit testing on this project. Could somebody please give me a simple code example on how I achieve the above? i.e. "run a UT on the Action Method by mocking out GetAll() that is in the AuthorDM class that's sitting in the Models folder and Arranging the test data that would be  returned out of the mock. "

    Saturday, April 11, 2020 7:08 PM
  • User475983607 posted

    Your unit test approach is flawed.  It seem like you are trying to test fetching data from Entity Framework.  A unit test generally tests logic that operates on a data not getting the data.  The getting the data is generally what's mocked so you can control the data. 

    Are you trying to mock Entity Framework?  The following is an Web API example but it will work with MVC as well.

    https://docs.microsoft.com/en-us/aspnet/web-api/overview/testing-and-debugging/mocking-entity-framework-when-unit-testing-aspnet-web-api-2

    Perhaps you need a mocking framework?

    Saturday, April 11, 2020 7:27 PM
  • User1571524970 posted

    I need any simple unit test for a controller to get started but cannot seem to get even the simple unit tests to work.

    I tried to a really simple test to test a ViewBags contents

      [TestMethod]
            public void BookingIndexTest()
            {
                   // Arrange
                   BookingController controller = new BookingController();
    
                   // Act
                   ViewResult result = controller.Index("", "", "school", 1) as ViewResult;
    
                   // Assert
                   Assert.AreEqual("school", result.ViewBag.CurrentFilter);
            }

    But got the error: Message:
    Test method BookingSystem.Tests.Controllers.BookingControllerTest.BookingIndexTest threw exception:
    System.InvalidOperationException: The model backing the 'RDSContext' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).

    Do I need to enable migrations for the Test project or what is this error asking me to do?

    Saturday, April 11, 2020 7:37 PM
  • User475983607 posted

    I tried to a really simple test to test a ViewBags contents

    Again, your testing approach does not make logical sense.  Why are you testing the ViewBag?  Microsoft already tested the ViewBag.   It's certainly possible to test the ViewBag but you need to create an HttpContext.  There are samples on GitHub if you wish to take a look but again this testing has already be done.

    https://github.com/aspnet/Mvc

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Saturday, April 11, 2020 7:53 PM
  • User1571524970 posted

    Great, thanks a lot! :)

    Saturday, April 11, 2020 8:07 PM
  • User1571524970 posted

    I have been trying to use Moq to mock up my db context/models. I have a Booking Controller which contains various actions such as Index, Details, Delete etc. I am trying to create a unit test to test the Details action method. Do I need to mock up a Booking model? Or should I mock the db context (the Bookings context)?

    DbContext:

    namespace BookingSystem.Models
    {
        public class RDSContext : DbContext
        {
            public DbSet<CampDate> CampDates { get; set; } 
            public DbSet<School> Schools { get; set; } 
            public DbSet<Booking> Bookings { get; set; }
            public DbSet<Organisation> Organisations { get; set; }
            public DbSet<School2> School2 { get; set; }
            public DbSet<CompletedCamp> CompletedCamps { get; set; }
            public DbSet<SecondarySchoolSurvey> SecondarySchoolSurveys { get; set; }
    
        }
    }

    Details action method:

    // GET: Booking/Details/5
        public ActionResult Details(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            Booking booking = db.Bookings.Find(id);
            if (booking == null)
            {
                return HttpNotFound();
            }
            return View(booking);
        }

    Current TestMethod:

    [TestMethod]
            public void BookingIndexTest()
            {
    
                var bookings = new List<Booking>
                {
                    new Booking
                    {
                        Id = 2,
                        AcademicYear = "2009",
                        Address1 = "Chapelizord",
                        Address2 = "Dublin",
                        Address3 = "Co. Dublin",
                        Eircode = "D20WP68",
                        ClassGroups = "4th Years",
                        County = "Dublin",
                        Date = DateTime.Today,
                        DeisSchool = "No",
                        Email = "office@prtb.ie",
                        EndTime = "15:00",
                        FeePayingSchool = "Yes",
                        GaeltachtArea = "No",
                        LecturerName = "John Doe",
                        OfficialSchoolName = "Loreto",
                        PhoneNumber = "01-4595324",
                        PrincipalName = "John Doe",
                        Religion = "Catholic",
                        RollNumber = "60010P",
                        StartTime = "10:00",
                        SchoolGender = "mixed",
                        Surveys = true,
                        TeacherName = "Ms Smith",
                        Topics = "Intro",
                        TotalBoys = 105,
                        TotalGirls = 100,
                        TotalPupils = 205,
                        X = 320163.012m,
                        Y = 730088.854m,
                        Latitude = 53.61m,
                        Longitude = -6.18m,
                        ITMEast = 72008.39m,
                        ITMNorth = 764023.21m
                   }
                };
            var context = new Mock<RDSContext>();
            context.Setup(e => e.Bookings.AsQueryable());
    
    
    
            var controller = new BookingController();
            var model = Model as Booking;
    
            //Assert.AreEqual (model.id, 2)
    
        }

    Saturday, April 11, 2020 9:31 PM
  • User1120430333 posted

    Your problem is that you are doing direct database access from the controller. It's not an optimal choice, and you are trying to UT EF, which again IMO is not a optimal choice. You should UT a class that will call EF and mock the class out returning a Arranged result from the mocked-out class and leave EF out of it. You are also trying to use the EF model the persistence model trying to use it as a strong typed model for a view. The EF model is not a viewmodel that is strong typed to a view. There should be a mapping between the EF model and the viewmodel. 

    Now, I could have set up a UT and just mocked-out Edit() that is sitting in the ProjectModel class sitting in the Models folder and setup a viewmodel on an Arrange  for the mocked-out Edit(). I can also mock-out the _webApi.GetProjByIdApi(id) that will eventually use EF and Arrange for its returned data.

       [Authorize]
        public ActionResult Details(int id = 0)
        {
           return id == 0 ? null : View(_projectModel.Edit(id));
        }
            public ProjectViewModels.Project Edit(int id)
            {
                var responseDto = _webApi.GetProjByIdApi(id);
    
                var project = new ProjectViewModels.Project
                {
                    ProjectId = responseDto.ProjectId,
                    ClientName = responseDto.ClientName,
                    ProjectName = responseDto.ProjectName,
                    Technology = responseDto.Technology,
                    ProjectType = responseDto.ProjectType,
                    StartDate = responseDto.StartDate,
                    EndDate = responseDto.EndDate,
                    Cost = responseDto.Cost
                };
    
                project = PopulateSelectedList(project);
    
                return project;
            }
    
         
    

    It comes back to SoC and does one know how to use an Interface for loose coupling and clean coding. 

    https://www.c-sharpcorner.com/blogs/understanding-interfaces-via-loose-coupling-and-tight-coupling

    It doesn't matter if you're using Core or non Core, becuase it appliers to both in writing clean code that is loosely coupled.

    https://docs.microsoft.com/en-us/archive/msdn-magazine/2016/may/asp-net-writing-clean-code-in-asp-net-core-with-dependency-injection

    https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/overview/understanding-models-views-and-controllers-cs

    <copied>

    An MVC model contains all of your application logic that is not contained in a view or a controller. The model should contain all of your application business logic, validation logic, and database access logic.

    A view should contain only logic related to generating the user interface. A controller should only contain the bare minimum of logic required to return the right view or redirect the user to another action (flow control). Everything else should be contained in the model.

    In general, you should strive for fat models and skinny controllers. Your controller methods should contain only a few lines of code. If a controller action gets too fat, then you should consider moving the logic out to a new class in the Models folder.

    <end>

    https://www.c-sharpcorner.com/UploadFile/56fb14/understanding-separation-of-concern-and-Asp-Net-mvc/

    Saturday, April 11, 2020 11:04 PM