Automating creation of source data for tests
I'm writing some tests for a synchronization job between two systems at the moment. Especially for these kinds of projects, but also most other, I need to have a fair amount of test-data to spin off useful tests.
The worst way of doing this is building up huge object graphs in setup methods. I guess an even worse way is to set up big object graphs using mocks, setting up each property. I've seen and probably did both, but it basically just makes you stop testing if you can't find another way.
So I've learned to move such test-data out into separate files. Often in a "TestData" folder or the like. Naming gets a bit random, but at least it's better.
But after starting to use ApprovalTests by Llewellyn Falco, I got inspired by the way it magically adds received and approved files named after specific tests. It's quite genious. And the best part is that NUnit more or less supports everything you need to do so in other situations.
Let's say I have this test and some test-data:
[TestFixture]
public class When_Updating
{
[Test]
public void Then_Posts_Data()
{
var arr = JsonConvert.DeserializeObject<JArray>(Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
@"TestData\some-test-file.json"
));
// ... test stuff
}
}
I could extract this into a helper and pass "TestData\some-test-file.json" perhaps. But it could be even better. NUnit has this TestContext
singleton that holds the current fixture and test names. (Amongst other things.) We can use this to our advantage. Have a look at this helper:
private static T ReadTestSource<T>()
where T : new()
{
var sourceFileName = Type.GetType(TestContext.CurrentContext.Test.ClassName)?.Name + "." + TestContext.CurrentContext.Test.MethodName + ".Source.json";
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..", sourceFileName);
if (!File.Exists(filePath))
using (var file = File.CreateText(filePath))
file.Write(JsonConvert.SerializeObject(new T()));
return JsonConvert.DeserializeObject<T>(File.ReadAllText(filePath));
}
Given this, we can replace the entire test-data getter to a single parameterless (generic, tho) method:
[TestFixture]
public class When_Updating
{
[Test]
public void Then_Posts_Data()
{
var arr = ReadTestSource<JArray>();
// ... test stuff
}
}
If I did this in a completely fresh test, I would actually get a JSON file on disk, called "When_Updating.Then_Posts_Data.Source.json". Since I passed JArray
as the generic parameter, the file will already contain []
. If it were an object, it'd have all the keys. I can just start filling it out. In this case, I've got another slower integration test I can copy some data from, and I'll have my source data at hand.
There's more magic that could be done, but I found these 9 lines of code so cool and powerful not to go ahead and share it at once. :) One important thing missing is finding the tests folder, but I'm sure you're up for that challenge if you find this useful.
Happy testing!
[Edit] I really, really needed that namespace/test-folder pretty soon, so here's a non-refactored method that does just that. :)
private static T ReadTestSource<T>()
where T : new()
{
var rootPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\.."));
var type = Type.GetType(TestContext.CurrentContext.Test.ClassName);
var path = type?.Namespace?.Replace(type.Assembly.GetName().Name + ".", "").Replace(".", "\\");
var testFolder = Path.Combine(rootPath, path ?? "");
if (!Directory.Exists(testFolder))
throw new Exception("Need to keep namespace and folder in sync to create test-source file");
var sourceFileName = type?.Name + "." + TestContext.CurrentContext.Test.MethodName + ".Source.json";
var filePath = Path.Combine(testFolder, sourceFileName);
if (!File.Exists(filePath))
using (var file = File.CreateText(filePath))
file.Write(JsonConvert.SerializeObject(new T()));
return JsonConvert.DeserializeObject<T>(File.ReadAllText(filePath));
}