Testing ASP.NET Core MVC Apps
“If you don’t like unit testing your product, most likely your customers won’t like to test it, either.”
Anonymous
Software of any complexity can fail in unexpected ways in response to changes. Thus, testing after making changes is required for all but the most trivial (or least critical) applications. Manual testing is the slowest, least reliable, most expensive way to test software. Unfortunately, if applications are not designed to be testable, it can be the only means available. Applications written following the architectural principles laid out in chapter 4 should be unit testable, and ASP.NET Core applications support automated integration and functional testing as well.
There are many kinds of automated tests for software applications. The simplest, lowest level test is the unit test. At a slightly higher level there are integration tests and functional tests. Other kinds of tests, like UI tests, load tests, stress tests, and smoke tests, are beyond the scope of this document.
A unit test tests a single part of your application’s logic. One can further describe it by listing some of the things that it isn’t. A unit test doesn’t test how your code works with dependencies or infrastructure – that’s what integration tests are for. A unit test doesn’t test the framework your code is written on – you should assume it works or, if you find it doesn’t, file a bug and code a workaround. A unit test runs completely in memory and in process. It doesn’t communicate with the file system, the network, or a database. Unit tests should only test your code.
Unit tests, by virtue of the fact that they test only a single unit of your code, with no external dependencies, should execute extremely quickly. Thus, you should be able to run test suites of hundreds of unit tests in a few seconds. Run them frequently, ideally before every push to a shared source control repository, and certainly with every automated build on your build server.
If you’re testing methods that rely on other services, ideally defined as interfaces and injected as constructor or method arguments, you’ll likely use fake or mock implementations of these interfaces in your unit test. Remember, your goal is only to test the code in a single unit (ideally a single method) of your application, not code this unit references.
Although it’s a good idea to encapsulate your code that interacts with infrastructure like databases and file systems, you will still have some of that code, and you will probably want to test it. Additionally, you should verify that your code’s layers interact as you expect when your application’s dependencies are fully resolved. This is the responsibility of integration tests. Integration tests tend to be slower and more difficult to set up than unit tests, because they often depend on external dependencies and infrastructure. Thus, you should avoid testing things that could be tested with unit tests in integration tests. If you can test a given scenario with a unit test, you should test it with a unit test. If you can’t, then consider using an integration test.
Because the purpose of integration tests is to verify that multiple pieces work together, including real infrastructure code, you’ll rarely want to use fake or mock implementations in integration tests. If you do need fake or mock implementations, make sure you’re not accidentally testing these implementations, instead of the real behavior you’re trying to test.
Integration tests will often have more complex setup and teardown procedures than unit tests. For example, an integration test that goes against an actual database will need a way to return the database to a known state before each test run. As new tests are added, and the production database schema evolves, these test scripts will tend to grow in size and complexity. In many large systems, it is impractical to run full suites of integration tests on developer workstations before checking in changes to shared source control. In these cases, integration tests may be run on a build server.
The eShopOnWeb sample includes an OrderRepository that is responsible for fetching and saving Order data. It defines a GetById method. An example integration test for this method is shown below: