Test Driving an ASP.NET MVC Core Web Application From Scratch


Creating the Project

  1. Open Visual Studio. Select Create a new project.
  2. Select C# for the language filter. Search for asp.net core templates.
  3. Select the ASP.NET Core Web Application template. Click Next.
  4. Name your Project and select an appropriate Location for saving your project.
  5. Uncheck the Place solution and project in the same directory box.
  6. Click Create.
  7. Select Empty project template. Other defaults should already be set to .NET Core, ASP.NET Core 3.1, and No Authentication.
  8. Click Create.

Adding the Unit Testing Project

We will use xUnit to test our project.

  1. Right click your Solution and select Add -> New Project.
  2. Select C# for the Language filter. Search for xUnit templates.
  3. Select the xUnit Test Project (.NET Core). Click Next.
  4. Name the project exactly the same as your first project, but add .Tests to the end of the name. (Why? See below.)
  5. Click Create.
  6. Right click on the Test Projects Dependencies and Add Reference from the .Tests project to your MVC project (So the test code can use the app code).

Why does the name need to match? When you're creating classes in C#, each class is placed inside a namespace. Classes can automatically "see" other classes in the same namespaces. They can also see class defined in parent namespaces. When we need to use code from other namespaces we use the using statement at the top of file. This means that if our test code classes are in the Foo.Tests namespace they can automatically see classes defined in the Foo namespace. If you put your tests in the Bar.Tests namespace, however, you would have to add using Foo; at the top of every test class you create.


Running the App

Hit F5 to run the application. You should see "Hello World" printed out on a web page.

Our web server is running at the http://localhost:xxxx/ url. Right now, it's just hard coded to return that Hello World string.

Let's make it use MVC and set up the basics.


Configuring our MVC Project

ASP.NET Core apps use a Startup class which may include ConfigureServices() and Configure() methods.

Let's open the Startup.cs file and take a look...

First, add a Startup constructor that initializes a Configuration interface object using Constructor Dependency Injection. Don't worry if you don't understand this now...there will be more on Dependency Injection later.

Edit your Startup class to include the following Constructor:

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

    public IConfiguration Configuration {get; }

    // The ConfigureServices() and Configure() methods continue here...
}

Does IConfiguration have an error? Fix it by adding in the using Microsoft.Extensions.Configuration; using statement.

ConfigureServices() Method

The ConfigureServices() method is an optional method used to set configuration options for the application's services. A service is a reusable component that provides functionality. Services are registered in this method, which makes them available within the application and in the Configure() method.

So let's add the MVC Service to our app.

Why? The MVC Service is what our application will use to do things like routing, dispatching HTTP requests to our controllers / actions, and all the Razor templating functionality.

Edit your ConfigureServices() method to include the following:

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

Configure() Method

The Configure() method is used to specify how the application responds to requests from the browser, otherwise known as HTTP Requests.

Next, let's reconfigure our application so that it actually uses the MVC service. Update your Configure method so that it looks 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();
    });
}

We've made two important changes here.

First, we enabled "Static Files" support. That will allow us to host things like .css and .js files on our server.

Finally, we've enabled MVC and defined some route information. We'll get into routes more later, but for now just know that routes are what associate a url to a method on a controller.

Examples:

http://localhost:xxxx/Home/Index => HomeController.Index()

http://localhost:xxxx/Foo/Create => FooController.Create()

Hit F5 to run the app again, and bask in the beautiful 404 error message. This is progress, ugly as it may be.


Adding a Controller

We're getting a 404 error message because we don't have a Controller and a View defined.

Take another peek at the route we defined:

    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    //              ^default            ^default     ^? means it isn't required

By default we're looking for a HomeController with an Index action.

In your Tests project, create a new test: HomeControllerTests.cs

public class HomeControllerTests
{
    [Fact]
    public void Index_Returns_Hello_World()
    {
        var underTest = new HomeController();

        var result = underTest.Index();

        Assert.Equal("Hello World!", result);
    }
}

Lets take care of the error that was created.

HomeController is not defined

The type or namespace name 'HomeController' could not be found (are you missing a using directive or an assembly reference?)

Sure enough, we're missing a HomeController class.

First we'll create a Controllers namespace inside our MVC project by right clicking on the project and selecting Add -> New Folder. Name the folder "Controllers".

Next let's add the controller class inside our new namespace. Right click the Controllers folder and select Add -> Class.... Name the class HomeController.

NOTE: All Controllers must be suffixed with the word Controller, and they must be in the Controllers namespace!

Update your class so that it has the Index method and it derives from MVC's base Controller class. You will also need to add the using Microsoft.AspNetCore.Mvc; using statement.

using System;
using Microsoft.AspNetCore.Mvc;

namespace TestCoreApp.Controllers
{            // ^ Use your project name, not mine!
    public class HomeController : Controller
    {
        public string Index()
        {
            throw new NotImplementedException();
        }
    }
}

Back in your Test Project there is still an error on the HomeController class. But this time the suggested fixes are different. Add a using statement to your HomeControllerTests class that makes your Controllers namespace available. For example, using TestCoreApp.Controllers;, but make sure the project name matches yours. The HomeController class should now be recognized.

Test fails, Make it pass

Run your test (CTRL R, A) and watch it fail, and then we'll make it pass by replacing the body of your method.

(NOTE: If your test project won't run, try Rebuilding your Solution first.)

We call this Index method an action. Actions are just methods on Controllers. Modify your project's Index method to look like this:

public class HomeController : Controller
{
    public string Index()
    {
        return "Hello World!";
    }
}

Run the app again, and check out the result.

At this point, we have a functional MVC app, but notice that we are only returning a string.

Wouldn't it be awesome if we could return some HTML instead of our boring string?


Returning a View

Similar to how our Controllers all live inside a folder named Controllers, all of our views must live inside a Views folder.

Within the Views folder, we will create a folder for each of our Controllers to house the views used by that Controller.

Right click on your web project and add a folder called Views. Make sure this folder lives in your project, not in the Controllers folder.

Within the Views folder, create a folder called Home that will hold the views for our HomeController.

Let's add our Home View. Right click on the Views\Home folder and select Add View.

  • Name the View Index.
  • Deselect the "Use a layout page" checkbox.

After a moment, you should have an Index.cshtml file that looks like this:

@{ Layout = null; }

<!DOCTYPE html>

<html>
  <head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
  </head>
  <body></body>
</html>

Let's add a bit of custom HTML to the body:

<body>
  <h1>This is html!</h1>
  <p>Here's a paragraph.</p>
</body>

Run the app again and view the home page.

Well, that was a let down. We created this HTML, but our controller is still just returning a string. We need to write the code that tells it to use our fancy view.


Returning a View from a Controller

Let's modify our initial test so that it expects our Controller to return a ViewResult.

[Fact]
public void Index_Returns_A_View()
{
    var underTest = new HomeController();

    var result = underTest.Index();

    Assert.IsType<ViewResult>(result);
}

If you have an error on ViewResult its because your test project doesn't have any knowledge of the ViewResult class, which is part of the AspNetCore.Mvc namespace. Add the using Microsoft.AspNetCore.Mvc; statement to fix the error.

Run the test and watch it fail. The test expected the result to be a ViewResult type, but instead it returned a string.

Now, let's make the test pass. Open up the HomeController and modify the Index Action (method) so that it looks like so:

public ViewResult Index()
{
    return View();
}

We modified the return type to be a ViewResult, and we're no longer returning a string literal...we're running the View() method and returning the result.

The View method looks for a .cshtml file in the following file folder location by default: Views\{Controller}\{Action Name}.cshtml

Since we are in the HomeController, and more specifically the Index Action, it will look for the following file: Views\Home\Index.cshtml. That's right where we created the view.

Run the unit test again and it should pass.

Run the app and you should see your HTML page being rendered.


Success!

YOU DID IT!!! You have successfully created an ASP.NET MVC Core Web Application with a Home Controller and View.