Anthony Fung
Posted on October 11, 2023
In last week's newsletter, we explored a tip for keeping tests with complex comparison data tidy. By moving larger expected values into separate resource files, we no longer need to express them in code; they can instead be read from disk during the test. In addition to making tests easier to navigate, the data becomes more readable and less prone to accidental formatting errors – a win-win scenario.
Unfortunately, this doesn’t always work as expected. There are cases where resource files might not be found at the expected path, even when set to be copied to the output directory. In this article, we’ll look at one reason why this could happen and see what we can do about it.
Where’s my Data?
As a quick refresher, here’s the test we wrote at the end of last week’s newsletter. Instead of putting the expected text directly in the test, we moved it to a separate file and pass its file name (CustomerWelcomeEmail.txt
) as a relative path into File.ReadAllText
to read it.
[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 works with the default test runner in Visual Studio, but not every host works in the same way. The built-in tools in Visual Studio 2022 (the latest version at time of writing) are excellent. But previous versions offered a more persuasive argument to use (the also excellent) JetBrains ReSharper to help fill the gaps.
ReSharper has its own test runner that copies tests to a temporary directory while running them (or at least that was the case in the version I had previously used). However, it didn’t copy any additional files along with them; this meant that the relative path was no longer valid. But more importantly, I’ve also seen this happen on build servers/CI pipelines too.
Unfortunately, we can’t (or shouldn’t) solve this by specifying an absolute path to the resource. It’d work on a local development machine but would most likely fail everywhere else on account of not having an identical file structure.
Bringing it Together
Luckily, there’s a way we can make it work. And we don’t have to find and copy files around either. Instead, we can make our resources an inseparable part of our test assemblies. To do this:
Right-click on the file.
Select Properties on the context menu.
Set the Build Action in the Properties tool window to Embedded resource.
Then we need to change our test to read resources from its own assembly rather than from disk. The code to do this is in the ReadEmbeddedResource
method in the following example.
[Test]
public void RegistrationEmailIsSent()
{
// Arrange
var emailService = Mock.Of<IEmailService>();
var registrationService = new
UserRegistrationService(emailService);
// Act
registrationService.RegisterNewUser("Test User");
// Assert
var expected = ReadEmbeddedResource(
"TestProject.CustomerWelcomeEmail.txt");
Mock.Get(emailService).Verify(s => s.SendEmail(expected));
}
private static string ReadEmbeddedResource(string resourceName)
{
var assembly = Assembly.GetExecutingAssembly();
using var stream =
assembly.GetManifestResourceStream(resourceName);
using var reader = new StreamReader(stream);
var result = reader.ReadToEnd();
return result;
}
Here we’ve passed TestProject.CustomerWelcomeEmail.txt
as the resource name:
TestProject
is our project’s name.CustomerWelcomeEmail.txt
is the name of our resource file.
In this example, our resource file was at the root level of our test project (alongside the file containing our test). If we had added more directories for structure, we’d need to include their names too. For example, if we added CustomerWelcomeEmail.txt
to a folder called Resources
, we would want to read TestProject.Resources.CustomerWelcomeEmail.txt
from our assembly.
Summary
Tests that load files from disk might pass during development but fail on other computers/CI pipelines if the files can’t be found. It’s safer to embed them instead so they can always be available.
You can do this by changing the Build Action of any file you want embedded to Embedded resource. Any tests that use them can then gain access through their own assembly. Adding a helper method for this can be helpful as it both keeps your code tidy, and lets you call it from multiple 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!
Posted on October 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.