Simplify Unit Tests by Storing Complex Data in Resource Files
Hi Friends.
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));
}
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"));
}
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."));
}
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));
}
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.
Bonus Developer Tip
When debugging in Visual Studio, it’s natural to jump to different frames on the call stack and view other bits of code. Use the shortcut Alt+*
(*
on the numeric pad) to quickly return to the place where the program has paused. This works even if you’re currently in another file too.
These tips are exclusively for my Substack subscribers and readers, as a thank-you for being part of this journey. Some may be expanded into fuller articles in future, depending on their depth.
But you get to see them here, first.