.Net Testing @ Novaloop

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!

Our philosophy of testing

When we write tests we follow these principles:

  • Integration tests for all good paths, as close as possible to the production environment.
  • Unit tests for more complex modules, but not necessarily a test coverage of 100%.
  • Unit-of-work testing over strict method testing
  • only as many mocks as necessary, but code that allows mocks
  • small changes in the code should cause as few adaptations to the tests as possible, code should be easy-to-change
  • Test-driven development where possible and reasonable

Libraries that belong in every Novaloop test project

Xunit

As a test driver we rely on Xunit. It is under active development and can be used with the latest .NET platform.

Fluent Assertions

Fluent assertions allow us to write the tests in a readable way and clearly express the author’s intention.

Verify

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.

FluentDocker

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

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

Verify

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.

  1. run the test
  2. save and analyze the result of the test
  3. if the result is as expected, it is marked as expected result and checked into the repo
  4. at the next test runs the result is compared with the accepted snapshot

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.

Fluent Docker

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);

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *