Problems in creating unit test for ASP .NET MVC
Asked Answered
B

1

2

I am creating some unit tests for my ASP .NET MVC Controller class and I ran into some very strange errors:

My controller code is below:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(JournalViewModel journal)
{
    var selectedJournal = Mapper.Map<JournalViewModel, Journal>(journal);

    var opStatus = _journalRepository.DeleteJournal(selectedJournal);
    if (!opStatus.Status)
        throw new System.Web.Http.HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));

    return RedirectToAction("Index");
}

My test code is below:

[TestMethod]
public void Delete_Journal()
{
    // Arrange

    // Simulate PDF file
    HttpPostedFileBase mockFile = Mock.Create<HttpPostedFileBase>();
    Mock.Arrange(() => mockFile.FileName).Returns("Test.pdf");
    Mock.Arrange(() => mockFile.ContentLength).Returns(255);

    // Create view model to send.
    JournalViewModel journalViewModel = new JournalViewModel();
    journalViewModel.Id = 1;
    journalViewModel.Title = "Test";
    journalViewModel.Description = "TestDesc";
    journalViewModel.FileName = "TestFilename.pdf";
    journalViewModel.UserId = 1;
    journalViewModel.File = mockFile; // Add simulated file

    Mock.Arrange(() => journalRepository.DeleteJournal(null)).Returns(new OperationStatus
    {
        Status = true
    });

    // Act
    PublisherController controller = new PublisherController(journalRepository, membershipRepository);
    RedirectToRouteResult result = controller.Delete(journalViewModel) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual(result.RouteValues["Action"], "Index");
}

Problem 1 - Mapping Exception:

Every time I run my test I receive the following exception:

Test Name: Delete_Journal Test
FullName: Journals.Web.Tests.Controllers.PublisherControllerTest.Delete_Journal
Test Source: \Source\Journals.Web.Tests\Controllers\PublisherControllerTest.cs : line 132
Test Outcome: Failed Test Duration: 0:00:00,3822468

Result StackTrace: at Journals.Web.Controllers.PublisherController.Delete(JournalViewModel journal) in \Source\Journals.Web\Controllers\PublisherController.cs:line 81 at Journals.Web.Tests.Controllers.PublisherControllerTest.Delete_Journal() in \Source\Journals.Web.Tests\Controllers\PublisherControllerTest.cs:line 156 Result Message: Test method Journals.Web.Tests.Controllers.PublisherControllerTest.Delete_Journal threw exception: AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.

Mapping types: JournalViewModel -> Journal Journals.Model.JournalViewModel -> Journals.Model.Journal

Destination path: Journal

Source value: Journals.Model.JournalViewModel

It seems that there is a mapping problem between the classes JournalViewModel and Journal, however I don't know where that is. I added this code to the Application_Start in Global.asax.cs:

Mapper.CreateMap<Journal, JournalViewModel>();
Mapper.CreateMap<JournalViewModel, Journal>();

And mapping from Journal to JournalViewModel is working.

In the end I tried adding Mapper.CreateMap<JournalViewModel, Journal>(); as the first line of the Delete method and then everything works, however I am not sure why.

Problem 2 - HTML Exception

Once the mapping is running with the workaround above, I have a problem in which the property Status from var opStatus = _journalRepository.DeleteJournal(selectedJournal); is always false, even though I used Mock to override it and make it always true. This causes the throwing of an HTML Exception that shouldn't happen.

EDIT

I changed in my Application_Start to:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<Journal, JournalViewModel>();
    cfg.CreateMap<JournalViewModel, Journal>();
});

But I still have the same error.

EDIT - Problem 2 Solved

It turns out that I forgot to add the mapping to my unit test class, so I did the following:

[TestInitialize]
public void TestSetup()
{
    // Create necessary mappings
    Mapper.CreateMap<Journal, JournalViewModel>();
    Mapper.CreateMap<JournalViewModel, Journal>();

    //...other code omitted for brevity
}

And it turns out that this was the source of the problem. I think that since the Global.asax.cs Application_Start() is never called in the unit tests, the Mapping is never created, so I had to do this myself in the unit tests initialization.

Bucentaur answered 5/10, 2016 at 1:52 Comment(4)
You probably need some sort of TestBase class where you call your mapping configuration code. The static Mapper in your test doesn't know anything about the mapping configuration from your Global.asax.cs.Tillford
I would suggest use a static method to create all maps and then call this static method as part of your test setup. Regarding the second issue, could you please let me know what is JournalRepository. Are you passing it as a dependency?Leastwise
What version of Automapper are you using? I would suggest using abstractions (IMapper) instead of static API as you have already discovered the difficulties in unit testing static APIsExcogitate
on the second problem. what mocking framework are you using? you are arranging for a null parameter but exercising an actual instance so there is no way for the mock to act on what was arranged.Excogitate
E
5

Problem 1

Automapper has both a Static and Instance API. You should consider using the instance API with IMapper and inject that into your controller.

public class PublisherController : Controller {
    private readonly IMapper mapper;

    public PublisherController(IJournalRepository journalRepository, IMembershipRepositry membershipRepository, IMapper mapper) {
        //...other code omitted for brevity
        this.mapper = mapper;
    }

    //...other code omitted for brevity

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(JournalViewModel journal) {
        var selectedJournal = mapper.Map<JournalViewModel, Journal>(journal);

        var opStatus = _journalRepository.DeleteJournal(selectedJournal);
        if (!opStatus.Status)
            throw new System.Web.Http.HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));

        return RedirectToAction("Index");
    }
}

That would allow for better mocking/faking/configuration of the mapping as needed. You should make sure to configure IMapper for dependency injection into your controllers.

if you are unable to change to the instance api then you need to make sure that the mapper is Initialize before running the tests

Mapper.Initialize(cfg => {
    cgf.CreateMap<JournalViewModel, Journal>();
});

Problem 2

your arrangement in the test is

Mock.Arrange(() => journalRepository.DeleteJournal(null)).Returns(new OperationStatus
{
    Status = true
});

This as you realized wont work for cases where you call journalRepository.DeleteJournal with an actual instance. Assuming that you are using JustMock from Telerik you should arrange for a more flexible argument.

Mock.Arrange(() => journalRepository.DeleteJournal(Arg.IsAny<Journal>())).Returns(new OperationStatus
{
    Status = true
});

Source : Handling Arguments in JustMock Arrangements

Complete Test: Instance API

[TestMethod]
public void Delete_Journal() {
    // Arrange

    //Configure mapping just for this test but something like this
    //should be in accessible from your composition root and called here.
    var config = new MapperConfiguration(cfg => {
        cfg.CreateMap<Journal, JournalViewModel>();
        cfg.CreateMap<JournalViewModel, Journal>();
    });

    var mapper = config.CreateMapper(); // IMapper

    // Simulate PDF file
    var mockFile = Mock.Create<HttpPostedFileBase>();
    Mock.Arrange(() => mockFile.FileName).Returns("Test.pdf");
    Mock.Arrange(() => mockFile.ContentLength).Returns(255);

    // Create view model to send.
    var journalViewModel = new JournalViewModel();
    journalViewModel.Id = 1;
    journalViewModel.Title = "Test";
    journalViewModel.Description = "TestDesc";
    journalViewModel.FileName = "TestFilename.pdf";
    journalViewModel.UserId = 1;
    journalViewModel.File = mockFile; // Add simulated file

    var status = new OperationStatus {
        Status = true
    };

    Mock.Arrange(() => journalRepository.DeleteJournal(Arg.IsAny<Journal>())).Returns(status);

    var controller = new PublisherController(journalRepository, membershipRepository, mapper);

    // Act        
    var result = controller.Delete(journalViewModel) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual(result.RouteValues["Action"], "Index");
}

Complete Test: Static API

[TestMethod]
public void Delete_Journal() {
    // Arrange

    //Configure mapping just for this test but something like this
    //should be in accessible from your composition root and called here.
    Mapper.Initialize(cfg => {
        cfg.CreateMap<Journal, JournalViewModel>();
        cfg.CreateMap<JournalViewModel, Journal>();
    });

    // Simulate PDF file
    var mockFile = Mock.Create<HttpPostedFileBase>();
    Mock.Arrange(() => mockFile.FileName).Returns("Test.pdf");
    Mock.Arrange(() => mockFile.ContentLength).Returns(255);

    // Create view model to send.
    var journalViewModel = new JournalViewModel();
    journalViewModel.Id = 1;
    journalViewModel.Title = "Test";
    journalViewModel.Description = "TestDesc";
    journalViewModel.FileName = "TestFilename.pdf";
    journalViewModel.UserId = 1;
    journalViewModel.File = mockFile; // Add simulated file

    var status = new OperationStatus {
        Status = true
    };

    Mock.Arrange(() => journalRepository.DeleteJournal(Arg.IsAny<Journal>())).Returns(status);

    var controller = new PublisherController(journalRepository, membershipRepository);

    // Act        
    var result = controller.Delete(journalViewModel) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual(result.RouteValues["Action"], "Index");
}
Excogitate answered 5/10, 2016 at 7:48 Comment(7)
Hi, I changed my test code in the problem 2 and everything worked! :)Bucentaur
As for the problem 1, is there a way to make the Mapper work with the static methods? Otherwise I would have to change a lot of my current code.Bucentaur
Check updated answer. initialize the mapper before running test. If answer useful vote up. Is answer resolves your problem then mark as answerExcogitate
Hi, I've changed my code based on what you said (see the edited part), but I still have the same error.Bucentaur
You say you made the change in Application_Start. for the test did you do the same?Excogitate
Actually no, I hadn't noticed that I should also do the mapping on my unit test class. I have now added the mapping code to it and everything works! By the way, what is the difference between using Mapper.CreateMap<>() and the Mapper.Initialize() method that you suggested?Bucentaur
I guess there isn't any difference. Many ways to do the same thing. I was just following what was shown in documentation.Excogitate

© 2022 - 2024 — McMap. All rights reserved.