Chapter Summary
Problem | Solution | Listing |
---|---|---|
Creating a unit test project | Use the dotnet new command with the project template for your preferred test framework | 7 |
Creating an XUnit test | Create a class with methods decorated with the Fact attribute and use the Assert class to inspect the test results | 9 |
Running unit tests | Use the Visual Studio or Visual Studio Code test runners or use the dotnet test command | 11 |
Isolating a component for testing | Create mock implementations of the objects that the component under test requires | 12–19 |
Being able to easily perform unit testing is one of the benefits of using ASP.NET Core, but it isn’t for everyone, and I have no intention of pretending otherwise.
I like unit testing, and I use it in my own projects, but not all of them and not as consistently as you might expect. I tend to focus on writing unit tests for features and functions that I know will be hard to write and likely to be the source of bugs in deployment. In these situations, unit testing helps structure my thoughts about how to best implement what I need. I find that just thinking about what I need to test helps produce ideas about potential problems, and that’s before I start dealing with actual bugs and defects.
That said, unit testing is a tool and not a religion, and only you know how much testing you require. If you don’t find unit testing useful or if you have a different methodology that suits you better, then don’t feel you need to unit test just because it is fashionable. (However, if you don’t have a better methodology and you are not testing at all, then you are probably letting users find your bugs, which is rarely ideal. You don’t have to unit test, but you really should consider doing some testing of some kind.)
If you have not encountered unit testing before, then I encourage you to give it a try to see how it works. If you are not a fan of unit testing, then you can skip this chapter and move on to Chapter 7, where I start to build a more realistic ASP.NET Core application.
Preparing for This Chapter
To prepare for this chapter, I need to create a simple ASP.NET Core project. Open a new PowerShell command prompt using the Windows Start menu, navigate to a convenient location, and run the commands shown in Listing 6-1.
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/apress/pro-asp.net-core-3. See Chapter 1 for how to get help if you have problems running the examples.
Creating the Example Project
These commands create a new project named SimpleApp using the web template, which contains the minimal configuration for ASP.NET Core applications. The project folder is contained within a solution folder also called Testing.
Opening the Project
If you are using Visual Studio, select File ➤ Open ➤ Project/Solution, select the Testing.sln file in the Testing folder, and click the Open button to open the solution file and the project it references. If you are using Visual Studio Code, select File ➤ Open Folder, navigate to the Testing folder, and click the Select Folder button.
Selecting the HTTP Port
Enabling the MVC Framework
Enabling the MVC Framework in the Startup.cs File in the SimpleApp Folder
Creating the Application Components
Now that the MVC Framework is set up, I can add the application components that I will use to demonstrate important C# language features.
Creating the Data Model
The Contents of the Product.cs File in the SimpleApp/Models Folder
The Product class defines Name and Price properties, and there is a static method called GetProducts that returns a Products array.
Creating the Controller and View
The Contents of the HomeController.cs File in the SimpleApp/Controllers Folder
The Contents of the Index.cshtml File in the SimpleApp/Views/Home Folder
Running the Example Application
Running the Example Application
Creating a Unit Test Project
The Unit Test Project Tools
Name | Description |
---|---|
mstest | This template creates a project configured for the MS Test framework, which is produced by Microsoft. |
nunit | This template creates a project configured for the NUnit framework. |
xunit | This template creates a project configured for the XUnit framework. |
These testing frameworks have largely the same feature set and differ only in how they are implemented and how they integrate into third-party testing environments. I recommend starting with XUnit If you do not have an established preference, largely because it is the test framework that I find easiest to work with.
Creating the Unit Test Project
If you are using Visual Studio, you will be prompted to reload the solution, which will cause the new unit test project to be displayed in the Solution Explorer, alongside the existing project. You may find that Visual Studio Code doesn’t build the new project. If that happens, select Terminal ➤ Configure Default Build Task, select “build” from the list, and, if prompted, select .NET Core from the list of environments.
Removing the Default Test Class
Removing the Default Test Class File
Writing and Running Unit Tests
Now that all the preparation is complete, I can write some tests. To get started, I added a class file called ProductTests.cs to the SimpleApp.Tests project and used it to define the class shown in Listing 6-9. This is a simple class, but it contains everything required to get started with unit testing.
The CanChangeProductPrice method contains a deliberate error that I resolve later in this section.
The Contents of the ProductTests.cs File in the SimpleApp.Tests Folder
There are two unit tests in the ProductTests class, each of which tests a behavior of the Product model class from the SimpleApp project. A test project can contain many classes, each of which can contain many unit tests.
Conventionally, the name of the test methods describes what the test does, and the name of the class describes what is being tested. This makes it easier to structure the tests in a project and to understand what the results of all the tests are when they are run by Visual Studio. The name ProductTests indicates that the class contains tests for the Product class, and the method names indicate that they test the ability to change the name and price of a Product object.
The Fact attribute is applied to each method to indicate that it is a test. Within the method body, a unit test follows a pattern called arrange, act, assert (A/A/A). Arrange refers to setting up the conditions for the test, act refers to performing the test, and assert refers to verifying that the result was the one that was expected.
The arrange and act sections of these tests are regular C# code, but the assert section is handled by xUnit.net, which provides a class called Assert, whose methods are used to check that the outcome of an action is the one that is expected.
The Fact attribute and the Asset class are defined in the Xunit namespace, for which there must be a using statement in every test class.
Commonly Used xUnit.net Assert Methods
Name | Description |
---|---|
Equal(expected, result) | This method asserts that the result is equal to the expected outcome. There are overloaded versions of this method for comparing different types and for comparing collections. There is also a version of this method that accepts an additional argument of an object that implements the IEqualityComparer<T> interface for comparing objects. |
NotEqual(expected, result) | This method asserts that the result is not equal to the expected outcome. |
True(result) | This method asserts that the result is true. |
False(result) | This method asserts that the result is false. |
IsType(expected, result) | This method asserts that the result is of a specific type. |
IsNotType(expected, result) | This method asserts that the result is not a specific type. |
IsNull(result) | This method asserts that the result is null. |
IsNotNull(result) | This method asserts that the result is not null. |
InRange(result, low, high) | This method asserts that the result falls between low and high. |
NotInRange(result, low, high) | This method asserts that the result falls outside low and high. |
Throws(exception, expression) | This method asserts that the specified expression throws a specific exception type. |
Running Tests with the Visual Studio Test Explorer
Visual Studio includes support for finding and running unit tests through the Test Explorer window, which is available through the Test ➤ Test Explorer menu and is shown in Figure 6-3.
Build the solution if you don’t see the unit tests in the Test Explorer window. Compilation triggers the process by which unit tests are discovered.
Run the tests by clicking the Run All Tests button in the Test Explorer window (it is the button that shows two arrows and is the first button in the row at the top of the window). As noted, the CanChangeProductPrice test contains an error that causes the test to fail, which is clearly indicated in the test results shown in the figure.
Running Tests with Visual Studio Code
Close and reopen the Testing folder in Visual Studio Code if you don’t see the code lens test features.
Visual Studio Code runs the tests using the command-line tools that I describe in the following section, and the results are displayed as text in a terminal window.
Running Tests from the Command Line
Running Unit Tests
Correcting the Unit Test
The problem with the unit test is with the arguments to the Assert.Equal method, which compares the test result to the original Price property value rather than the value it has been changed to. Listing 6-11 corrects the problem.
When a test fails, it is always a good idea to check the accuracy of the test before looking at the component it targets, especially if the test is new or has been recently modified.
Correcting a Test in the ProductTests.cs File in the SimpleApp.Tests Folder
Isolating Components for Unit Testing
Writing unit tests for model classes like Product is easy. Not only is the Product class simple, but it is self-contained, which means that when I perform an action on a Product object, I can be confident that I am testing the functionality provided by the Product class.
The situation is more complicated with other components in an ASP.NET Core application because there are dependencies between them. The next set of tests that I define will operate on the controller, examining the sequence of Product objects that are passed between the controller and the view.
The Contents of the Comparer.cs File in the SimpleApp.Tests Folder
These classes will allow me to create IEqualityComparer<T> objects using lambda expressions rather than having to define a new class for each type of comparison that I want to make. This isn’t essential, but it will simplify the code in my unit test classes and make them easier to read and maintain.
The HomeControllerTests.cs File in the SimpleApp.Tests Folder
The unit test creates an array of Product objects and checks that they correspond to the ones the Index action method provides as the view model. (Ignore the act section of the test for the moment; I explain the ViewResult class in Chapters 21 and 22. For the moment, it is enough to know that I am getting the model data returned by the Index action method.)
The test passes, but it isn’t a useful result because the Product data that I am testing is coming from the hardwired objects’ Product class. I can’t write a test to make sure that the controller behaves correctly when there are more than two Product objects, for example, or if the Price property of the first object has a decimal fraction. The overall effect is that I am testing the combined behavior of the HomeController and Product classes and only for the specific hardwired objects.
Unit tests are effective when they target small parts of an application, such as an individual method or class. What I need is the ability to isolate the Home controller from the rest of the application so that I can limit the scope of the test and rule out any impact caused by the repository.
Isolating a Component
The Contents of the IDataSource.cs File in the SimpleApp/Models Folder
Creating a Data Source in the Product.cs File in the SimpleApp/Models Folder
The next step is to modify the controller so that it uses the ProductDataSource class as the source for its data, as shown in Listing 6-16.
ASP.NET Core supports a more elegant approach for solving this problem, known as dependency injection, which I describe in Chapter 14. Dependency injection often causes confusion, so I isolate components in a simpler and more manual way in this chapter.
Adding a Property in the HomeController.cs File in the SimpleApp/Controllers Folder
Isolating the Controller in the HomeControllerTests.cs File in the SimpleApp.Tests Folder
I have defined a fake implementation of the IDataSource interface that lets me use any test data with the controller.
I have followed the most commonly used unit testing style in this chapter, in which an application feature is written and then tested to make sure it works as required. This is popular because most developers think about application code first and testing comes second (this is certainly the category that I fall into).
This approach is that it tends to produce unit tests that focus only on the parts of the application code that were difficult to write or that needed some serious debugging, leaving some aspects of a feature only partially tested or untested altogether.
An alternative approach is Test-Driven Development (TDD). There are lots of variations on TDD, but the core idea is that you write the tests for a feature before implementing the feature itself. Writing the tests first makes you think more carefully about the specification you are implementing and how you will know that a feature has been implemented correctly. Rather than diving into the implementation detail, TDD makes you consider what the measures of success or failure will be in advance.
The tests that you write will all fail initially because your new feature will not be implemented. But as you add code to the application, your tests will gradually move from red to green, and all your tests will pass by the time that the feature is complete. TDD requires discipline, but it does produce a more comprehensive set of tests and can lead to more robust and reliable code.
Using a Mocking Package
It was easy to create a fake implementation for the IDataSource interface, but most classes for which fake implementations are required are more complex and cannot be handled as easily.
A better approach is to use a mocking package, which makes it easy to create fake—or mock—objects for tests. There are many mocking packages available, but the one I use (and have for years) is called Moq. To add Moq to the unit test project, run the command shown in Listing 6-18 in the Testing folder.
The Moq package is added to the unit testing project and not the project that contains the application to be tested.
Installing the Mocking Package
Creating a Mock Object
Creating a Mock Object in the HomeControllerTests.cs File in the SimpleApp.Tests Folder
The use of Moq has allowed me to remove the fake implementation of the IDataSource interface and replace it with a few lines of code. I am not going to go into detail about the different features that Moq supports, but I will explain the way that I used Moq in the examples. (See https://github.com/Moq/moq4 for examples and documentation for Moq. There are also examples in later chapters as I explain how to unit test different types of components.)
The SetupGet method is used to implement the getter for a property. The argument to this method is a lambda expression that specifies the property to be implemented, which is Products in this example. The Returns method is called on the result of the SetupGet method to specify the result that will be returned when the property value is read.
The VerifyGet method is one of the methods defined by the Mock class to inspect the state of the mock object when the test has completed. In this case, the VerifyGet method allows me to check the number of times that the Products property method has been read. The Times.Once value specifies that the VerifyGet method should throw an exception if the property has not been read exactly once, which will cause the test to fail. (The Assert methods usually used in tests work by throwing an exception when a test fails, which is why the VerifyGet method can be used to replace an Assert method when working with mock objects.)
The overall effect is the same as my fake interface implementation, but mocking is more flexible and more concise and can provide more insight into the behavior of the components under test.
Summary
This chapter focused on unit testing, which can be a powerful tool for improving the quality of code. Unit testing doesn’t suit every developer, but it is worth experimenting with and can be useful even if used only for complex features or problem diagnosis. I described the use of the xUnit.net test framework, explained the importance of isolating components for testing, and demonstrated some tools and techniques for simplifying unit test code. In the next chapter, I start the development of a more realistic project, named SportsStore.