Test Driven MVC in C#

Getting Started

Add a new ASP.NET Core Web Application

  • Name it "HelloMVC"
  • Choose the "Empty" Template with No Authentication.

Add a xUnit Test Project (.NET Core) to the solution

  • Name it "HelloMVC.Tests"
  • Add a reference from HelloMVC.Tests to HelloMVC.
  • Rename UnitTest1.cs in HelloMVC.Tests to "HomeControllerTests.cs"

Setup a project repository on GitHub

  • Initialize git to track your project
  • Add a .gitignore file
  • Get your first commit locked in
  • Finish connecting your project to the GitHub repository by finishing the setup of your remote origin and pushing your changes

Add the First Test

Add a test to verify that the return value of the HomeController's Index() action is a ViewResult.

namespace HelloMVC.Tests
{
    public class HomeControllerTests
    {
        [Fact]
        public void Index_Returns_ViewResult()
        {
            var controller = new HomeController();

            var result = controller.Index();

            Assert.IsType<ViewResult>(result);
        }
    }
}

Create the Controller

There is an error when trying to instantiate a new HomeController():

The type or namespace 'HomeController' could not be found

Add a "Controllers" folder to your "HelloMVC" project, then a "HomeController" class inside your Controllers folder.

Home Controller


Check your Test

Import the HelloMVC.Controllers namespace to fix the first compile error.

using HelloMVC.Controllers; //<--- add this line to your code

namespace HelloMVC.Tests
{
    public class HomeControllerTests
    {
        ...

Add the Index() Action

The HomeController's Index() action needs to have a ViewResult defined as its return type.

For now, let's return null from the method.

Add a using statement for the Microsoft.AspNetCore.Mvc namespace to eliminate the error on the ViewResult type.

using Microsoft.AspNetCore.Mvc;

namespace HelloMVC.Controllers
{
    public class HomeController
    {
        public ViewResult Index()
        {
            return null;
        }
    }
}

How is the test now?

Hover on the ViewResult error and leave your cursor there for a second.

The type or namespace 'ViewResult' could not be found..

Hit Ctrl + . to pull up the quick fix menu.

Select "using Microsoft.AspNetCore.Mvc;"

It should build now!

using Xunit;
using Microsoft.AspNetCore.Mvc;

High Fives == time to git commit

Getting this test to compile is a pretty big milestone for us.

Let's lock this in a git commit before we start our next task.

If things go sideways this will be a great spot to come back to.


Run the test

Now that we can compile, it's time to actually run our test!

Hit ctrl + r, a to run the test


The Results are in...

Results


What's a ViewResult, anyway?

ViewResult is a class that's built into our MVC framework.

We will use ViewResult to write methods that return HTML.

Blown


We need a little help...

We need some help creating an instance of ViewResult.

Luckily, the Controller class has one built in.

Let's make our HomeController inherit from Controller so we can use it.

public class HomeController : Controller
{
    public ViewResult Index()
    {
        return null;
    }
}

Using View()

In the Index() action, delete "null" and start typing "View()" in its place.

Intellisense should show you the View method we inherited.

View


Run those tests...

Run tests


Time to commit

Commit


Tests are green... but does it work?

Hit F5 and find out!


Not what we expected

Where is 'Hello World' coming from?

Hello World


Change the Startup.cs File

Our Startup.cs file has been configured with some default code, including a WriteAsync() method which is automatically writing "Hello World!" in response to an HTTP request.

Let's write over it.

Add a Startup() constructor and Configuration property to look like this:

public class Startup
{ 
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration {get; }

}

You will need to add a using statement for Microsoft.Extensions.Configuration in order to use the IConfiguration interface class.

Change the ConfigureServices() method of the Startup.cs class to look like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}

And, change the Configure() method of the Startup.cs class to look like this:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });
}

Try again... does it work?

Hit F5 and find out!


That's a no.

No


That's a no.

No Highlight


Where's the View?

MVC will look for our View in a bunch of places by default.

Sometimes you’ll hear this called “Convention over configuration”.

We didn’t need to configure where our View file is, it just uses some intuitive defaults.

By default, our HomeController’s Index view should be in the Views/Home/Index.cshtml file.

All controllers follow this convention by default.


Creating the View

Add a "Views" folder to your project and then the "Home" folder inside "Views".

Right click on your Home folder and select Add > New Item > View.

Deselect the “Use a layout page” checkbox.

Name your view “Index”.

Click Add.

Add


Our View

Our View


Does it work now?

Hit F5 and find out!


That's better, but why is it empty?

Take another look at the view.

There's a lot of code in there...

But it's really just an empty page.

Add some text to the view.

Hit F5 again.


Hello world!

Hello


Time to commit

100


That's the C and the V

Our app receives a HTTP Get request when someone visits our site.

Our app finds the HomeController’s Index Action (aka Method).

Our Index action returns a default View.

Our default view lives in the Views/{ControllerName}/{ActionName}.cshtml file

The view’s HTML is rendered in the web browser.


The Big Picture

Big Picture 1


Big Picture 2


Big Picture 3


Big Picture 4


Big Picture 5


Big Picture 6


Big Picture 7


Big Picture 8


Big Picture 9


Let's Personalize It

Add a "Name:" Textbox and a "Greet" button to our page, to look like this:

Greet

Put them inside a form element that will send a Get request to "/Greet/Index"

Personalize


Try it out

Enter "test" into the text box and click "Greet".

What happened?

Not Found


The Resource Cannot be Found...

We're trying to visit the URL: https:// <yourlocalserver>/Greet/Index?name="test"

What Controller name do we need?

  • GreetController

What action does the Controller need?

  • Index

Does the action take any arguments?

  • Yes, a string called "name"

What's our first step?

Writing a failing unit test, of course!


The Test

Add a new GreetControllerTests.cs class in your HelloMVC.Tests project.

Add a using Xunit statement and make the class public.

Continue by adding the following test method to verify that the return value of the GreetController's Index() action is a ViewResult:

public class GreetControllerTests
{
    [Fact]
    public void Index_Returns_ViewResult()
    {
        var controller = new GreetController();

        var result = controller.Index("ThisIsAString");

        Assert.IsType<ViewResult>(result);
    }
}

Make it compile (first step to getting red)

There is an error on the instantiation of the GreetController() object.

The type or namespace name 'GreetController' cannot be found

We've seen this error message before.

Add a new "GreetController" class inside your Controllers folder.

Make the class public and add the following Index() action:

namespace HelloMVC.Controllers
{
    public class GreetController
    {
        public ViewResult Index(string name)
        {
            return null;
        }
    }
}

Don't forget the using Microsoft.AspNetCore.Mvc; statement.


More errors on GreetControllerTests

The type or namespace name 'GreetController()' cannot be found

Intellisense suggests we add a using HelloMVC.Controllers; statement.

Also, The type or namespace 'ViewResult' cannot be found

We are also missing the using Microsoft.AspNetCore.Mvc; statement.

Now let's run our test...


It fails for the right reason

Fails


Add just enough code to make it pass...

Make sure GreetController is inheriting from the Controller class, and add:

public class GreetController : Controller
{
    public ViewResult Index(string name)
    {
        return View();
    }
}

The Missing Link

Our controller has the name value. But we want to pass the name to the view, so we can say "Hello {username}!" on our web page.

How do we get that piece of data from the Controller, and into the View?

That's what the M in MVC is for...

Models let us move data from a Controller into a View.


Creating a Model

Add a "Models" folder to your "HelloMVC" project, then a "GreetModel" class inside your Models folder.

Add a Name property to it.

namespace HelloMVC.Models
{
    public class GreetModel
    {
        public string Name { get; set; }
    }
}

The Controller passes the Model to the View

Let's write some unit tests that confirm...

  • The GreetController passes a GreetModel to the View.

  • The GreetModel passed to the View has the correct name property.


GreetController passes Model to the View

Add the following test to your GreetControllerTests().

Don't forget to add the using HelloMVC.Models; statement.

[Fact]
public void Index_Passes_GreetModel_To_View()
{
    var controller = new GreetController();

    var result = controller.Index("ThisIsAString");

    Assert.IsType<GreetModel>(result.Model);
}

Run the test and it should fail. The model that we return from our GreetController Index() action does not match the GreetModel type.


GreetController passes Model to the View

Let's add some code to our Index() action to make the test pass.

Don't forget to add a using HelloMVC.Models; using statement.

public class GreetController : Controller
{
    public ViewResult Index(string name)
    {
        var model = new GreetModel();
        return View(model);
    }
}

Now run the test again and it should pass!


GreetController Sets Name on the Model

Add the following test method to your GreetControllerTests().

[Fact]
public void Index_Sets_Name_On_Model()
{
    var expectedName = "ExampleString";
    var controller = new GreetController();

    var result = controller.Index(expectedName);

    var model = (GreetModel)result.Model;
    Assert.Equal(expectedName, model.Name);
}

Run the test and it should fail. The Name property of the model we return from our GreetController Index() action does not equal "ExampleString".


GreetController Sets Name on the Model

Let's add some more code to our Index() action to make the test pass. Let's set the Name property of our Model to the name value that was passed into the Index() action.

public class GreetController : Controller
{
    public ViewResult Index(string name)
    {
        var model = new GreetModel();
        model.Name = name;
        return View(model);
    }
}

Now run the test again and it should pass!


Does it work?

Hit F5 to find out!

Enter a name into the field and click "Greet".

Uh oh...It looks like we don't have a /Greet/Index view. Stop the code from running and let's fix it.


Creating the View

Remember that views are just .cshtml files that live inside the Views folder.

By convention, each Controller has a folder in there.

File names match the method name, with the .cshtml file extension.

In the Index() action of your GreetController, right click on your call to the View method and select Add View.

Create View


Add View Dialog

Dialog


Using our Model in the View

We already know the Controller passes the name to the View.

But, how do we use the Model inside the View?


Tell the View the Type of our Model

Add this line of razor code to the top of your Greet Index view. It is a special command that let's our view know to use the GreetModel found in our Models folder.

View Type


Now let's use the model

Add a <div> element that prints the Name property of our model to the page.

<body>
  <div>Hello, @Model.Name</div>
</body>

What's with the @s?

That's called Razor Syntax, and we'll learn more about it later.

For now, you should know that we use them to write C# code inside of HTML.


Did it Run???

Hit F5 to find out!

Enter a name into the field and click "Greet".

Yes!...Our greeting has appeared on the page!


Commit your Success!

Time to commit.

git add .

git commit -m "Added GreetController and its Index view"


Recap

  • We built an MVC application from scratch.
  • We test drove the creation of the controllers.
  • We used a Form to pass a piece of data from one page to another.
  • We created a controller that passes a model into the View.