'How to test a .NET 4.8 client running against a .NET 5.0 server in a developer-friendly way?

I don't know if this question is appropriate for this forum.

I am developing a C# ASP.NET Core webservice and a client-side library that uses this webservice. At this time, both the code for the server-side part and the client-side part live in the same Git repository and Visual Studio solution. The server-part is intended to run in Azure.

I have a combination of unit tests and integration tests, including end-to-end integration tests that test both client and server at the same time. I have written tests that can run the client either (depending on configuration) against an instance of the server in Azure (with a URI that gets injected) OR against a local in-memory server instance using Microsoft.AspNetCore.TestHost.

This works great. The in-memory server instance is very convenient when coding. Our Continuous Integration (Jenkins) server runs the integration tests against a cloud-deployed server (i.e., an instance of our ASP.NET Core webservice that has been deployed to Azure). This testing-against-the-cloud provides vital value but is too cumbersome to do while coding locally.

Here comes the problem: The server is intended to run on .NET 5. The client part needs to be able to run on both .NET Framework 4.8 and .NET 5. We want to test that a client running 4.8 can work correctly against a server running .NET 5 (i.e., an instance of our ASP.NET Core webservice that has been compiled against .NET 5).

It is of course possible to run integration tests using .NET 4.8 against a cloud-deployed server that uses .NET 5.

It is probably not possible - or at least not convenient - to run these tests locally (since the same test process would have to run both .NET 4.8 and .NET 5). But I guess we can live with that.

Of course I can build the server part against .NET 5 and deploy it to the server and run ONE integration test against that. But that doesn't solve my long-term problem. I want to be able to regularly run all my integration tests. More specifically, what I would like to achieve is:

  1. It should be easy to run the integration tests locally with a .NET 5 client against a locally hosted .NET 5 server (as today).
  2. The Jenkins server should run the integration tests with a .NET 5 client against a cloud-deployed .NET 5 server (as today).
  3. The Jenkins server should ALSO run the integration tests with a .NET 4.8 client against a cloud-deployed .NET 5 server.
  4. I don't want to have to maintain two sets of tests. I want it to be easy to run all the same tests in all 3 scenarios.

I can achieve 1-3 by making a copy of my integration test project and letting this new project target .NET 4.8 and always run against the cloud. But that would mean maintaining two sets of tests.

Do you have any advice for how I can achieve all 4 objectives?

Thanks in advance!

EDIT 1: Regarding platforms: Our developer machines and Jenkins hosts run Windows. The cloud-deployed server also runs on Windows.

EDIT 2: Simply multi-targeting the test project (i.e., letting the test project compile against both .NET 4.8 and .NET 5) is not possible. The test project has a reference to the server part because it needs to be able to run the server locally. The server part needs .NET 5; it cannot compile against .NET 4.8 (it relies on libraries that don't exist for .NET 4.8). Hence the test project cannot compile against .NET 4.8 either.



Solution 1:[1]

I ended up solving it with preprocessor directives as @vernou suggested.

I have the following projects:

  • ClientLibrary - a wrapper that calls the webservice.
  • WebAPI - server-side implementation of the webservice.
  • IntegrationTest - integration tests.

My integration test CSPROJ contains this:

  <PropertyGroup>
    <TargetFrameworks>netframework4.8;net5.0</TargetFrameworks>
  </PropertyGroup>
  
  <!-- NuGet packages that we always need -->
  <ItemGroup>
    <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
  </ItemGroup>
  
  <!-- NuGet packages that require .NET 5 -->
  <ItemGroup Condition=" '$(TargetFramework)' == 'net5.0'">
    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="5.0.3" />
  </ItemGroup>
  
  <!-- Project references that we always need -->
  <ItemGroup>
    <ProjectReference Include="ClientLibrary.csproj" />
  </ItemGroup>
  
  <!-- Project references that require .NET 5 -->
  <ItemGroup Condition=" '$(TargetFramework)' == 'net5.0'">
    <ProjectReference Include="WebAPI.csproj" />
  </ItemGroup>

My Jenkinsfile sets an environment variable to point to the Azure-deployed instance of my cloud application:

def deployed_uri = powershell (
    script: "pulumi stack output EndPoint",
    returnStdout: true
)
env.integrationtest_service_uri = deployed_uri

My tests do something like this:

private IService CreateService(Uri? uri)
{
    if (uri is null)
    {
// This is a built-in preprocessor directive: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives
#if NET5_0
        _output.WriteLine("Connecting to locally hosted service");
        return CreateLocallyHostedService();
#else
        throw new JenkinsOnlyIntegrationTestException(
            "Cannot start service because URI is null; we will assume that this test is only intended to run on Jenkins");
#endif
    }
    else
    {
        _output.WriteLine($"Connecting to service on URI: {uri}");
        return CreateServiceAgainstRemoteUri(uri);
    }
}

[SkippableFact(typeof(JenkinsOnlyIntegrationTestException))]
public void TestThatServiceWorks(){
    var uri = GetUriOrNullFromEnvironmentVariableSetByJenkins()
    var service = CreateService(uri);
    
    // Test the service.
}

That way:

  • If the environment variable integrationtest_service_uri is set, the integration tests will run against the (cloud-hosted) server instance at that address.
  • If the environment variable is not set and the framework is .NET 5, the integration tests will run against a locally hosted server using Microsoft.AspNetCore.TestHost.
  • If the environment variable is not set and the framework is .NET 4.8, the test will throw an exception, but because the test is marked as [SkippableFact], it will be counted as skipped rather than failed.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 filmor