C# How to Write a Source Generator Part 4/5: Testing and Debugging
4 min readDec 15, 2022
To properly test a Source Generator, we need to simulate the compilation process.
First Test Case
Remember the Set Up in Part 2? We have created a Unit Test project, linked it with the main projects. Now is time for the very first test case.
// In UnitTest project, BindablePropTest.cs
namespace UnitTest
{
public class BindablePropTest
{
// Our generator is created before test run
IIncrementalGenerator generator = new BindablePropSG();
// Mark the function with [Fact] so that Visual Studio know this is a test case
// And we can run or debug it
[Fact]
public void ShouldGenerateBasicCode()
{
// Read sample code in TestData/BindablePropTestSimpleUsage.txt
// I will put its content below
var sourceCode = File.ReadAllText(
Path.Combine(
Directory.GetCurrentDirectory(),
"TestData",
"BindablePropTest",
"SimpleUsage.txt"
)
);
// My helper function which parse text into a Syntax Tree and let generator process
var generatedCode = TestHelper.GetGeneratedOutput(sourceCode, generator);
// The actual result and expected result may have some small differences in spacing and indent
// I remove all white space so that the only thing matter is content and their order
generatedCode = TestHelper.RemoveAllWhiteSpace(generatedCode ?? "");
var expectedCode = File.ReadAllText(
Path.Combine(
Directory.GetCurrentDirectory(),
"TestData",
"BindablePropTest",
"SimpleUsage.expected.txt"
)
);
expectedCode = TestHelper.RemoveAllWhiteSpace(expectedCode);
// The generator is expected to output code similar to SimpleUsage.expected.txt
// If they look alike, the test will pass
generatedCode.Should().NotBeNullOrEmpty()
.And
.BeEquivalentTo(expectedCode);
}
}
}
// UnitTest project, TestHelper.cs
namespace UnitTest
{
internal class TestHelper
{
// It basically accepts 1 generator and source code
// Then, made the generator to process the source code
// Finally, return the generated result (if any)
// It may have many classes and methods you don't know. Just ignore them and focus into our business
public static string? GetGeneratedOutput(string sourceCode, IIncrementalGenerator generator)
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.IsDynamic)
.Select(assembly => MetadataReference
.CreateFromFile(assembly.Location))
.Cast<MetadataReference>();
var compilation = CSharpCompilation.Create("SourceGeneratorTests",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
// Notice the data-type of 'generator' is IIncrementalGenerator
// So we can re-use this function as long as our generator implement IIncrementalGenerator interface
CSharpGeneratorDriver.Create(generator)
.RunGeneratorsAndUpdateCompilation(compilation,
out var outputCompilation,
out var diagnostics);
diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)
.Should().BeEmpty();
return outputCompilation.SyntaxTrees.Skip(1).LastOrDefault()?.ToString();
}
public static string RemoveAllWhiteSpace(string content)
{
return Regex.Replace(content, @"\s+", string.Empty);
}
}
}
// IMPORTANT: Don't paste this comment into your file
// TestData/BindablePropTest/SimpleUsage.txt
// --------------------END------------------------
namespace UnitTest.TestData.BindablePropTest
{
public partial class SimpleUsage
{
[BindableProp]
string title;
[BindableProp]
string description = "To the moon";
}
}
// IMPORTANT: Don't paste this part into your file
// TestData/BindablePropTest/SimpleUsage.expected.txt
// --------------------END------------------------
// <auto-generated/>
namespace UnitTest.TestData.BindablePropTest
{
public partial class SimpleUsage
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title),
typeof(string),
typeof(SimpleUsage),
default,
(BindingMode)0,
null,
(bindable, oldValue, newValue) =>
((SimpleUsage)bindable).Title = (string)newValue,
null,
null,
null
);
public string Title
{
get => title;
set
{
OnPropertyChanging(nameof(Title));
title = value;
SetValue(SimpleUsage.TitleProperty, title);
OnPropertyChanged(nameof(Title));
}
}
public static readonly BindableProperty DescriptionProperty = BindableProperty.Create(
nameof(Description),
typeof(string),
typeof(SimpleUsage),
"To the moon",
(BindingMode)0,
null,
(bindable, oldValue, newValue) =>
((SimpleUsage)bindable).Description = (string)newValue,
null,
null,
null
);
public string Description
{
get => description;
set
{
OnPropertyChanging(nameof(Description));
description = value;
SetValue(SimpleUsage.DescriptionProperty, description);
OnPropertyChanged(nameof(Description));
}
}
}
}
Once everything is set up, you can try to run the test.
If the test is fail, we may go to BindablePropSG.cs, create a breakpoint and debug.
Conclusion
In small project, we don’t write unit test too often because the effort to write tests may bigger than the actual code. In this case , however, unit test probably is the best way to go.