Adding automatic tests to an ASP.NET MVC application (part 1)
Soon after software development has emerged, it became obvious that any application beyond a homework needs testing. Testing evolved naturally along with frameworks, both on client side and server side and now we are talking about Test-driven development and Behavior-driven development (e.g. Cucumber). Of course, most efficient tests are automatic ones.
A couple of years ago, we had the opportunity to start a project from scratch (MDW Automatic Testing along with Claudiu) and I said to my self: why not develop a small framework to easily add automatic tests?
Since back then ASP.NET Core was not mature enough to go into production, we sticked to the more solid ASP.NET MVC 5, along with its natural companion, Entity Framework 6. On the client side, we used AngularJS (Angular was not mature enough back then).
So, this article will focus on automatic testing using this tech stack, but most of the concepts also apply to other tech stacks.
II. Unit testing
Simply put, unit testing refers to testing the functions in your code.
Unit tests represent the base of the automated test pyramid and from my personal experience they are often skipped, because they require lot of coding and thus more time (also check this nice article about levels of testing). We made a compromise and have chosen to cover critical functionality with unit tests and use integrative tests for the rest.
In order for the code to be unit-testable some tools and design-patterns were used to cover the following concepts:
- Dependency Injection — we chose Ninject. Of course, there are alternatives, but this seemed to be most versatile. This allows to easily replace a service dependency when needed (e.g. with a mock object)
- A mocking framework — we chose NSubstitute . This allows to easily to replace classes or functions for unit testing.
- A unit testing framework — we use NUnit. It integrates nicely in Visual Studio and we use it to run all our tests (including Selenium Web driver ones).
- Generic repositories — a design pattern that involves creating a generic class that handles basic data operations (add, insert, insert bulk, update, update bulk etc.). Unfortunately, Entity Framework 6 is not very friendly when it comes to dependency injection and mocking, so generic repositories are of good use since that can simply be replaced with some generic in memory repositories.
Enough talk, let’s dive into some code.
- The generic repositories
All generic repositories implement a single interface (see below). Besides the regular (non-cached) repository and the in memory one, there is also a “cached” one that is plugged for some entities to avoid database fetches (typically mapped to rarely changed and relatively small tables).
The Repository<T> class will mostly delegate functionality to Entity Framework or EntityFramework.Extended (for bulk operations). The CachedRepository<T> will cache all data upon first use in a dictionary object to allow fast seeks by primary key. InMemoryRepository<T> will use an internal list to hold the items.
2. Test setup
Each test must run independently of each other and even in parallel (currently we do not do this, as all unit tests are run in less than 1 minute). That means that each test must have a deterministic context when it runs (i.e. same data in the repositories).
Theoretically, unit tests should provide all the inputs (the first A from Arrange-Act-Assert) within their function body. However, we had a slightly different approach: reinitialize the repositories and arrange other input data only:
- easier to set up test data (an in-memory database) and ensuring the consistency
- smaller “arrange” part
- slightly slower tests since repositories must be refilled before running each test
Some relevant code parts are shown below:
Some special services must be mocked because unit tests should not touch external resources such as logging table, configuration files or external APIs.
Normally, the application integrates with Active Directory (SSO). During unit test phase, users must be “mocked” and security must act like a certain user was logged in before some functionality is called. Thus, security related tests can be performed (e.g. some users should not see some data).
3. Test example
I will chose a simple function to illustrate some unit tests for:
Environments are sets of databases within the data warehouse. Most users are allowed to check information only for some environments (e.g. development and pre-production).
The function relies on a proxy service that calls an external service (API) to get environment information. So, in order to unit test it, we must mock the service:
This mock class is plugged in by a simple rebind, so that all services relying on EnvironmentProxyService will receive a mocked instance:
The following methods contain two unit tests:
Theoretically, a unit test should be mapped to a single function. However, there is no major benefit in doing so: if it fails, the message clearly states what went wrong. The log will also include the test function name. By grouping them, it is easier to follow the business cases (avoid repetition, spot a missing scenario).
4. Test results
Below you can see how the results are reported by Visual Studio. Some tests are repeated using multiple input sets.
Resharper offers a better view, especially when running hundreds of tests or more:
With the proper setup (framework and test data), unit tests can be very useful. However, there are serious costs associated with them since data model and business logic changes involve unit tests changes.
Key points about unit tests:
- pragmatism over dogmatism — in real life there is almost always not enough time to write all the required unit tests. Try to convince the project stakeholders that it is important to allocate resources to cover the most important parts (e.g. for us data sync between data warehouse and our internal database is critical, so we had it covered with unit tests)
- easy to write, even for less experienced developers. However, this is highly dependent on how tested functions look like. Aim for short (some consider empty lines in functions as code smell) and pure functions (no side effects) and simple functions (low cyclomatic complexity).
- very fast execution (usually less than 100ms). This allows to have cool features such as live unit testing
- can be run before committing the code (fail-fast)
- easy to integrate in a pipeline (e.g. nunit3-console.exe MdwAutomaticTesting.Test.dll /result:nunit.xml to generate test results file)
- easy to generate code coverage — check this SO question for full details
III. Next steps
When I began writing this article, I thought of also talking about integrative testing. Clearly, unit testing is long enough, so I decided to have the content split in multiple parts.
Next parts will deal with in memory API testing and UI testing.
I would love to hear from you about how you deal with unit testing in your applications. Also, any suggestions related to the above solution are greatly welcomed.