docs/project/writing-tests.md
This document provides an overview of paradigm we are using in our tests.
In a variety of situations, it's useful to run some code in another process. Some examples:
To achieve, this we use RemoteExecutor.Invoke which is defined in Microsoft.DotNet.RemoteExecutor. It passes information about a static method to be executed and the arguments to be passed to it out to a spawned process that invokes the method. Lambdas / anonymous methods may be used, but they must not close over any state (including this); accidentally closing over state will likely result in strange errors. For additional information see https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.RemoteExecutor/ and https://xunit.github.io/docs/running-tests-in-parallel.html
Example (skipping additional usings):
using System.Diagnostics;
using Microsoft.DotNet.RemoteExecutor;
public class HttpWebRequestTest
{
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void DefaultMaximumResponseHeadersLength_SetAndGetLength_ValuesMatch()
{
RemoteExecutor.Invoke(() =>
{
const int NewDefaultMaximumResponseHeadersLength = 255;
HttpWebRequest.DefaultMaximumResponseHeadersLength = NewDefaultMaximumResponseHeadersLength;
Assert.Equal(NewDefaultMaximumResponseHeadersLength, HttpWebRequest.DefaultMaximumResponseHeadersLength);
return RemoteExecutor.SuccessExitCode;
}).Dispose();
}
}
When writing network related tests we try to avoid running tests against a remote endpoint if possible. We provide simple APIs to create a LoopbackServer and send responses. A high number of scenarios can be tested with it. For additional information see https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
Example (skipping additional usings and class scoping):
using System.Net.Test.Common;
[Fact]
public async Task Headers_SetAfterRequestSubmitted_ThrowsInvalidOperationException()
{
await LoopbackServer.CreateServerAsync(async (server, uri) =>
{
HttpWebRequest request = WebRequest.CreateHttp(uri);
Task<WebResponse> getResponse = request.GetResponseAsync();
await LoopbackServer.ReadRequestAndSendResponseAsync(server);
using (WebResponse response = await getResponse)
{
Assert.Throws<InvalidOperationException>(() => request.AutomaticDecompression = DecompressionMethods.Deflate);
}
});
}
This one is fairly simple but often used incorrectly. When running tests which depend on outside influences like e.g. Hardware (Internet, SerialPort, ...) and you can't mitigate these dependencies, you might consider using the [OuterLoop] attribute for your test.
With this attribute, tests are executed in a dedicated CI loop and won't break the default CI loops which get created when you submit a PR.
To run OuterLoop tests locally you need to set the msbuild property "OuterLoop" to true: /p:OuterLoop=true.
To run OuterLoop tests in CI you need to mention dotnet-bot and identify the tests you want to run. See @dotnet-bot help for the exact loop names.
This doesn't mean that you should mark every test which executes against a remote endpoint as OuterLoop. See below.
For network related tests which needs to contact a remote endpoint instead of a LoopbackServer, you can use our Relay Servers. We invest in Infrastructure to provide these "safe" remote endpoints. For more information see https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/System/Net/Configuration.Http.cs
Example:
public static readonly object[][] EchoServers = System.Net.Test.Common.Configuration.Http.EchoServers;
[Theory, MemberData(nameof(EchoServers))]
public async Task ContentLength_Get_ExpectSameAsGetResponseStream(Uri remoteServer)
{
HttpWebRequest request = WebRequest.CreateHttp(remoteServer);
...
}
To support our tests running on as many target frameworks as possible, we need to be cautious when it comes to system resource access. The best example is trying to access a file outside of an AppContainer (UWP). We should depend on APIs which are specifically designed for these scenarios to work, e.g. TempDirectory. If a test case needs to store data on the FileSystem, consider using TempDirectory and TempFile APIs.
Example (skipping additional usings and class scoping):
using System.IO;
[Fact]
public void FileSystemWatcher_File_Changed_LastWrite()
{
using (var testDirectory = new TempDirectory())
using (var file = new TempFile(Path.Combine(testDirectory.Path, "file")))
{
Directory.SetLastWriteTime(file.Path, DateTime.Now + TimeSpan.FromSeconds(10));
...
}
}