C# How to Write a Source Generator Part 4/5: Testing and Debugging

Kafka Wanna Fly
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.

Run Test and See the Result

If the test is fail, we may go to BindablePropSG.cs, create a breakpoint and debug.

Run Step by Step

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.

--

--