DEV Community

Cover image for Simplify Unit Tests by Storing Complex Data in Resource Files
Anthony Fung
Anthony Fung

Posted on • Originally published at webdeveloperdiary.substack.com

Simplify Unit Tests by Storing Complex Data in Resource Files

The structures of test projects are usually simpler than those of the projects they validate. As each test is concerned with verifying its own unique use case, they can generally exist without sharing too many components. And because of this independence, it’s tempting to make each test fully self-contained by writing all related data into it.

That said, there’s no reason why we shouldn’t keep things tidy where possible. Last week, we touched on the DRY principle while exploring a tip to help minimise test initialisation code. This week, we’ll look at something we can do when making comparisons against larger pieces of data such as paragraphs of text.

A Review of Simpler Test Cases

To help pinpoint errors, we generally aim to minimise both scope and complexity in tests. The following example shows one of the simpler scenarios we might encounter: we have a service that takes an input, transforms it, and outputs it as a single value. Comparing this against an expected value is straightforward.

[Test]
public void CalculationResultIsCorrect()
{
    // Arrange

    var service = new ComplexCalculationService();

    // Act

    var result = service.RunCalculation(5);

    // Assert

    Assert.That(result, Is.EqualTo(17));
}
Enter fullscreen mode Exit fullscreen mode

While the logic in RunCalculation may (or may not) be complex, the test and its assertion are very simple: the calculation result must equal 17.

With more complex data types, a typical test might look like the following example. Here, we have a repository and want to verify that we can retrieve all fields for the user corresponding to the given ID. We need to perform more checks to verify each of the additional dimensions in the data. But the checks themselves aren’t any more complex, and it’s still easy to see the expected values for each property.

[Test]
public void RepositoryCanGetUserData()
{
    // Arrange

    var repository = new UserRepository();

    // Act

    var user = repository.GetUser(3);

    // Assert

    Assert.That(user.Id, Is.EqualTo(3));
    Assert.That(user.Name, Is.EqualTo("Test User"));
    Assert.That(user.Address, Is.EqualTo("Sample Address"));
}
Enter fullscreen mode Exit fullscreen mode

Making More Complex Comparisons

Things become trickier when our data values (and not necessarily their data types) become more complex. Consider the example of an e-commerce system. When a new customer signs up, we want to send them a welcome email with a discount code for their first purchase. If we wanted to write a test for this, we might end up with something that looks like the following. It’s admittedly brittle, but it’ll give immediate feedback if anything changes.

[Test]
public void RegistrationEmailIsSent()
{
    // Arrange

    var emailService = Mock.Of<IEmailService>();
    var registrationService = new UserRegistrationService(emailService);

    // Act

    registrationService.RegisterNewUser("Test User");

    // Assert

    Mock.Get(emailService).Verify(s => s.SendEmail(
        "Welcome Test User!\r\n" +
        "\r\n" +
        "Thank you for signing up with us. You should receive a separate email " +
        "shortly, detailing the next steps in the registration process. " +
        "Please be sure to read this carefully and follow the instructions to " +
        "get up and running as soon as possible.\r\n" +
        "\r\n" +
        "In the meantime, here's a discount code for 10% off your first purchase.\r\n" +
        "\r\n" +
        "1234-ABC\r\n" +
        "\r\n" +
        "If you have any questions, please do not hesitate to contact our friendly " +
        "customer care team using our support email address."));
}
Enter fullscreen mode Exit fullscreen mode

We’re verifying more text now than in the previous examples. And to complicate things, formatting matters too (e.g. line break positioning). I’ve come across two issues when comparing multiline values like this in the past:

  • Files become more difficult to navigate when they contain multiple tests with large amounts of expected data.

  • The formatting doesn’t necessarily correspond to how it is represented in code. This can make layout issues difficult to spot. Things become even more challenging when trying to compare csv (Comma-separated values) data.

Taking the Problem Outside

One of the simplest ways to address this is to move expected texts into their own files (where it makes sense to). We can then read from them while our tests are running. Let’s start by creating a new text file in our test project called CustomerWelcomeEmail.txt and adding the following content to it.

Welcome Test User!

Thank you for signing up with us. You should receive a separate email shortly, detailing the next steps in the registration process. Please be sure to read this carefully and follow the instructions to get up and running as soon as possible.

In the meantime, here's a discount code for 10% off your first purchase.

1234-ABC

If you have any questions, please do not hesitate to contact our friendly customer care team using our support email address.

For simplicity, we’ve created this at the same folder-level as our test code file. But we could add it to a dedicated folder instead if we wanted more structure. Once created, we need to right-click on the file and select Properties. In the tool window that appears, we should check that Copy to Output Directory is set to either Copy always, or Copy if newer (though Copy always is usually safer).

We can then modify our test to make it read the expected value from the file we just created. As it was in the same directory, specifying its filename (CustomerWelcomeEmail.txt) is enough as a relative path. However, we should adjust the path accordingly if it was created elsewhere.

[Test]
public void RegistrationEmailIsSent()
{
    // Arrange

    var emailService = Mock.Of<IEmailService>();
    var registrationService = new
        UserRegistrationService(emailService);

    // Act

    registrationService.RegisterNewUser("Test User");

    // Assert

    var expected = File.ReadAllText(
        "CustomerWelcomeEmail.txt");

    Mock.Get(emailService).Verify(s => s.SendEmail(expected));
}
Enter fullscreen mode Exit fullscreen mode

This method works with Visual Studio’s test runner and is one of the simpler ways to use resources in tests. However, there are some cases where it will fail; we’ll look at what we can do about that next week.

Summary

Writing Assert statements can be tricky with complex values. When expressed in code, errors can be difficult to spot in longer texts and csv data. By moving them into separate resource files, you gain two benefits:

  • You can spot errors in your expected data more easily.

  • Your test files remain lighter and easier to navigate.

After creating files for your data, you need to make sure they’re copied to your test output directory. After that, you should be able to access them from within your tests.


Thanks for reading!

This article is from my newsletter. If you found it useful, please consider subscribing. You’ll get more articles like this delivered straight to your inbox (once per week), plus bonus developer tips too!

Top comments (0)