Harmony Core Office Hours, January 2020
January 23, 2020Visual Studio Adaptive Formatting
January 24, 2020Until recently, web developers had two basic options for backend testing: unit tests and integration tests. Starting with ASP.NET Core 2.1, Microsoft introduced another testing mechanism, functional testing, which we’ll discuss in this article. But first, let’s consider the two basic long-standing options.
Unit tests work best for small chunks of isolated functionality. Incorporating unit-test-friendly architecture patterns is a good way to promote the health of your code base. (A mocking library can help with this.) However, some planning is required to get good coverage with unit tests. And even with great coverage, components may interact in unexpected ways, and the behavior of your test harness may vary from the behavior of the real running application.
The traditional alternative to unit tests is integration tests. Integration tests run externally, enabling them to test the behavior of a running application. This can take the form of IIS or IIS Express running in a test environment, with Selenium or an HttpClient instance making requests to the web server.
Integration tests can capture the exact behavior of a running application, but they generally run slower. Furthermore, environment setup for this type of testing is prone to failure, it takes considerable effort to run these tests, and you’ll need to interact with the networking stack in Windows to get at the code running on your machine.
Enter the Microsoft.AspNetCore.Mvc.Testing NuGet package. This package, which is available in ASP.NET Core 2.1 onward, introduces a third testing option: functional testing. Functional testing lies somewhere between the other two testing options we’ve discussed and can replace integration tests. With functional testing, unit tests interact with a web server emulator (a Microsoft.AspNetCore.TestHost.TestServer instance). This enables your application to make requests as though it were hosted. In reality, though, it all runs directly in memory and never touches the network layer.
Let’s take a look at a simple example. In the real world, you would already have startup and controller classes (required for AspNetCore applications), but the code below includes simple examples of these for the sake of completeness. The interesting parts of this example are SimpleTestClass.SimpleTestClass and SimpleTestClass.SimpleTest. See the code comments for information on how this all works, and note that the project for this was created for AspNetCore 2.2 running on the .NET Framework using the Synergy/DE Unit Test Project template in Visual Studio. Visual Studio doesn’t currently include a unit test project template for .NET Core, but you can create a .NET Core class library and then add Nuget references to the latest versions of MSTest.TestAdapter, MSTest.TestFramework, and Microsoft.NET.Test.Sdk. With those references, you now have the .NET Core equivalent of a unit test project. To run unit tests in .NET Core, open a command prompt from within Visual Studio, navigate to the folder where your unit test project exists, and type
dotnet test
That will run your unit tests and report back the results to the command window.
namespace UnitTestSample
{TestClass}
public class SimpleTestClass
server, @TestServer
public method SimpleTestClass
proc
;;The type being passed to UseStartup in the following line of code
;;is your Startup class. By convention the name will always be
;;'Startup', but because Microsoft makes it easy to test multiple
;;applications from a single unit test class library, you may need
;;to fully qualify the type name here.
server = new TestServer(new WebHostBuilder().UseStartup<Startup>())
endmethod
{TestMethod}
public method SimpleTest, void
proc
;;Some samples call server.CreateClient in the constructor rather
;;than in each test method, but this can lead to failures at
;;runtime and should be avoided.
data client = server.CreateClient()
data request = "/api/MyTest/"
;;The client looks like an HttpClient, so all the methods and
;;operations it offers are available to you here. For example, if
;;you need to make a post, use client.PostAsync(request, postData).
data response = client.GetAsync(request).Result
response.EnsureSuccessStatusCode()
;;Now that we've made the request, we can just grab the string
;;contents of the response and ensure that it has returned what
;;we expected.
data result = response.Content.ReadAsStringAsync().Result
Assert.AreEqual("Hello world", result)
endmethod
endclass
{Route("api/[controller]")}
public class MyTestController extends ControllerBase
{HttpGet}
public async method Get, @Task<IActionResult>
proc
mreturn Ok("Hello world")
endmethod
endclass
public class Startup
public method ConfigureServices, void
services, @IServiceCollection
proc
services.AddMvcCore()
endmethod
public method Configure, void
app, @IApplicationBuilder
env, @IHostingEnvironment
proc
app.UseMvc()
endmethod
endclass
endnamespace
There are two things to keep in mind about this code. The first is that the project needs matching reference versions between Microsoft.AspNetCore.Mvc.Testing and your other AspNetCore packages. Prior to 3.0, this means explicit package references, while on 3.0 and higher, the AspNetCore packages are locked to the version of the .NET Core runtime that you use.
We can now write performant functional tests for ASP.NET Core controllers, tests that can run through your systems and use your actual configuration. This can be an excellent replacement for integration testing when used alongside unit tests.