Functional Testing for Synergy Web Services with ASP.NET Core

by Jeff Greene, Principal Software Engineer

Until 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.