We like to write tests and we mean it.
To keep it that way for all of us we use a modern test stack and simplify our daily testing routine as much as possible. This also means that we don't have to provide multi-page documentation on the test setup for our projects. How do we do that? Like this!
When we write tests we follow these principles:
As a test driver we rely on Xunit. It is under active development and can be used with the latest .NET platform.
Fluent assertions allow us to write the tests in a readable way and clearly express the author's intention.
As nice as Fluent Assertions are, we don't want to overdo it. Comparing entire JSON documents is also rather time-consuming with conventional methods. This is where Snapshot Testing comes in handy. We use Verify for this.
There is even a Plugin for Jetbrains based IDEs.
We don't want to include more pages of documentation with our project for test setup. For easy on-boarding of new developers we decided to use FluentDocker.
Fluent assertions allow us to write elegant assertions that also clearly express the motivation for the assertion.
An example can be found on the website:
string actual = "ABCDEFGHI";
actual.Should()
.StartWith("AB")
.And.EndWith("HI")
.And.Contain("EF")
.And.HaveLength(9);
The intention can also be clearly stored in any error message for the assertion:
IEnumerable<int> numbers = new[] { 1, 2, 3 };
numbers.Should().OnlyContain(n => n > 0);
numbers.Should().HaveCount(4,
"because we thought we put four items in the collection");
In addition, Fluent Assertions produces useful and helpful error messages:
string username = "dennis";
username.Should().Be("jonas");
The error message then looks like this:
Expected username to be "jonas" with a length of 5,
but "dennis" has a length of 6, differs near "den" (index 0).
If you want to compare whole objects or JSON strings with each other, it makes sense to do this with an assertion library. Here so-called snapshot extensions help. The principle is simple.
Some proprties are dynmaic and cannot be tested using snapshots. Suppose we generate the id for an object continuously. The Verify library provides us with a Settings object for this purpose:
var myId = 123;
var myObject = new MyObject { Id = 123 };
var verifySettings = new VerifySettings();
var verifySettings.ModifySerialization(_ =>
{
_.IgnoreMember("Id");
});
...
<Do something important>
...
await Verifier.Verify(myObject, _verifySettings);
myObject.Id.Should.Be(myId);
Then when changes are made in the code, repeat steps 2, 3 and 4 and the tests are up to date again.
For end-to-end tests we use Fluent Docker. This way, the tests can be run on arbitrary servers (CI / CD) and computers (developers*) without any effort.
For this we create a docker-compose.yml file. In it, all necessary services and dependencies are defined. These can then be booted up before the effective test run and shut down again afterwards.
var dockerComposeFile = Path.Combine(Directory.GetCurrentDirectory(),
"docker-compose.yaml");
var hosts = new Hosts().Discover();
var dockerHost = hosts.FirstOrDefault(x => x.IsNative) ??
hosts.FirstOrDefault(x => x.Name == "default");
Service = new DockerComposeCompositeService(dockerHost,
new DockerComposeConfig
{
ComposeFilePath = new List<string> { dockerComposeFile },
ForceRecreate = true,
RemoveOrphans = true,
StopOnDispose = true,
}
);
Service.Start();
Assert.Equal(ServiceRunningState.Running, Service.State);
Assert.Equal(5, Service.Containers.Count);
Condition.Await(() =>
Service.Containers
.Any(c => c.State != ServiceRunningState.Running), 30 * 1000);