Macs Dickinson
Macs Dickinson

Reputation: 987

Nancy testing GetModel<T> throws KeyNotFoundException

I'm trying to test that the model returned from my Nancy application is as expected. I have followed the docs here but whenever I call the GetModel<T> extension method it throws a KeyNotFoundException.

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.

I know what the error means but I'm failing to see why it's being thrown.

Here's my module

public class SanityModule : NancyModule
{
    public SanityModule()
    {
        Get["sanity-check"] = _ => Negotiate.WithModel(new SanityViewModel { Id = 1 })
                                            .WithStatusCode(HttpStatusCode.OK);
    }
}

my view model

public class SanityViewModel
{
    public int Id { get; set; }
}

and here's my test

[TestFixture]
public class SanityModuleTests
{
    [Test]
    public void Sanity_Check()
    {
        // Arrange
        var browser = new Browser(with =>
        {
            with.Module<SanityModule>();
            with.ViewFactory<TestingViewFactory>();
        });

        // Act
        var result = browser.Get("/sanity-check", with =>
        {
            with.HttpRequest();
            with.Header("accept", "application/json");
        });
        var model = result.GetModel<SanityViewModel>();

        // Asset
        model.Id.ShouldBeEquivalentTo(1);
    }
}

Debugging this test shows that the module is hit and completes just fine. Running the application shows that the response is as expected.

Can anyone shed some light on this?

Upvotes: 4

Views: 505

Answers (1)

Macs Dickinson
Macs Dickinson

Reputation: 987

Thanks to the lovely guys, albertjan and the.fringe.ninja, in the Nancy Jabbr room we've got an explanation as to what's going on here.

TL;DR It makes sense for this to not work but the error message should be more descriptive. There is a workaround below.

The issue here is that I am requesting the response as application/json whilst using TestingViewFactory.

Let's take a look at the implementation of GetModel<T>();

public static TType GetModel<TType>(this BrowserResponse response)
{
    return (TType)response.Context.Items[TestingViewContextKeys.VIEWMODEL];
}

This is simply grabbing the view model from the NancyContext and casting it to your type. This is where the error is thrown, as there is no view model in NancyContext. This is because the view model is added to NancyContext in the RenderView method of TestingViewFactory.

public Response RenderView(string viewName, dynamic model, ViewLocationContext viewLocationContext)
{
    // Intercept and store interesting stuff
    viewLocationContext.Context.Items[TestingViewContextKeys.VIEWMODEL] = model;
    viewLocationContext.Context.Items[TestingViewContextKeys.VIEWNAME] = viewName;
    viewLocationContext.Context.Items[TestingViewContextKeys.MODULENAME] = viewLocationContext.ModuleName;
    viewLocationContext.Context.Items[TestingViewContextKeys.MODULEPATH] = viewLocationContext.ModulePath;

    return this.decoratedViewFactory.RenderView(viewName, model, viewLocationContext);
}

My test is requesting json so RenderView will not be called. This means you can only use GetModel<T> if you use a html request.

Workaround

My application is an api so I do not have any views so changing the line

with.Header("accept", "application/json");

to

with.Header("accept", "text/html");

will throw a ViewNotFoundException. To avoid this I need to implement my own IViewFactory. (this comes from the.fringe.ninja)

public class TestViewFactory : IViewFactory
{
    #region IViewFactory Members

    public Nancy.Response RenderView(string viewName, dynamic model, ViewLocationContext viewLocationContext)
    {
        viewLocationContext.Context.Items[Fixtures.SystemUnderTest.ViewModelKey] = model;
        return new HtmlResponse();
    }

    #endregion
}

Then it is simply a case of updating

with.ViewFactory<TestingViewFactory>();

to

with.ViewFactory<TestViewFactory>();

Now GetModel<T> should work without needing a view.

Upvotes: 3

Related Questions