Intro
In my previous post, I explained how to write a C# Source Generator from scratch. It’s advised to read that article to get a heads-up for this blogpost.
Recap from previous blog post…
Source generation is possible using T4 templating, however using Roslyn provides more possibilities and you can easily generate multiple source files, which is not possible using T4. Another thing to keep in mind is that analyzing existing source code files is done using Roslyn, which is released some time ago and had proven itself, so if you are familiar with Roslyn, then writing a Source Generator should be doable.
Steps to create a source generator
1️⃣ Create a library
:Create a .NET Standard 2.0
library, and add the following NuGet references:
|
|
2️⃣ Add a new Source Generator class
Add a new C# class which defines the Source Generator.
- The class must implement the ISourceGenerator interface
- The class must be annotated with the Generator-attribute to specify that this a source generator.
An example source generator class looks like this:
|
|
3️⃣ Use the Source Generator project
Create a simple ConsoleApp project, and reference the ExampleClassCodeGenerator project.
When referencing the project, make sure to add the OutputItemType="Analyzer"
and ReferenceOutputAssembly="false"
attributes like this:
|
|
Now you can use the ExampleClass just like this:
|
|
Continuation
In this blog I’ll focus on how to unit-test a source generator and describe the challenges and a few possible scenarios on how to do this.
💡 Challenges
Unit testing a source generator is not straightforward. There is no easy way to just ’new’ the Source Generator because two contexts are needed:
How to Unit-Test a source generator ?
In the next chapter, I’ll go into detail on the possible scenarios, challenges and present a better and easier solution.
1️⃣ Mocking
The first option would be to use a Mocking framework like Moq to setup all required methods and properties from Microsoft CodeAnalysis interfaces to ensure that you can test the internal business logic from your source generator and excluding the Microsoft CodeAnalysis logic.
Challenges & Solutions
The GeneratorExecutionContext is passed to the Execute
method from your source generator. This Execute
method represents the main generation step.
This GeneratorExecutionContext
can also be used to add new classes (which will be generated) and to get a reference to the ISyntaxReceiver
which is used to determine which objects (e.g. classes or interfaces) in your project comply to your rules to be analyzed. This is mostly done by annotating a class or interface with an custom defined attribute.
The problem is that the GeneratorExecutionContext
is a real class, so not suitable for easy mocking using Moq.
A possible solution for this could be to use Microsoft Fakes, however this is only supported in Visual Studio Enterprise.
A workaround for this issue, is to wrap the GeneratorExecutionContext
in a custom interface and wrapper code so that this can be mocked by Moq.
Example for the new interface:
|
|
Example for the wrapper class:
|
|
With this workaround, mocking can be setup like this:
|
|
But in addition to this GeneratorExecutionContext
, more interfaces from the Microsoft CodeAnalysis namespace (e.g. INamespaceSymbol
, ITypeSymbol
) need to be mocked in order to run a simple unit test.
This quickly gets very extensive, error-prone and complex, so this is not preferred.
In case you are still interested how this could be achieved, see an example here.
2️⃣ Simulate the Execution
The second option would to simulate the main Execute
method from your source generator so that you just need to provide this simulated method with the following information:
The source file which needs be be analyzed
The attribute which needs to be added to the source class or source interface which is used by your own implementation from the ISyntaxReceiver to determine if this class or interface needs to be processed by the business logic.
📓 This is actually a common pattern in most source generators because you only want to analyze a specific set of classes in your project, and not all classes.
The next chapters describe a possible scenario and the explanation how the execution can be simulated and what steps are needed.
Example scenario
Let’s suppose you have a class which needs to analyzed by your Source Generator and depending on the business logic from your Source Generator, some new C# files are generated.
This can look like this:
The class which needs to analyzed:
|
|
The unit test:
The business logic from a Source Generator (e.g. FluentBuilder) is tested and a verification is done if the correct output is generated.
This unit-test can look like the code below, and I’ll explain in detail all the steps (1 to 5) in that test.
|
|
step 1: Define the location from the class
Define the relative path to the C# class you want to process by your source generator.
(e.g. "./DTO/SimpleClass.cs"
)
And make sure you set Copy to Output Directory
to Copy if newer
:
|
|
step 2: Define inputs for the Execute method
Create a new instance from the SourceFile
class and provide the following properties:
Path: The path to the C# class you want to process
Text: The code from the C# class you want to process as text (string)
AttributeToAddToClass1: A string 2 which defines the attribute-name which needs to be dynamically added to your C# class to make sure that you mark your class with the correct attribute so that it’s processed by the source-generator.
📓 Notes:
- 1 This
AttributeToAddToClass
is optional, and in case you want to add an attribute to an interface, set the propertyAttributeToAddToInterface
. - 2 It’s also possible to provide a
ExtraAttribute
instead of a string, when using this option, you can also add arguments to that attribute (in case that attribute allows arguments). For more information on how this is achieved, see the project AnyOf.
Example:
1 2 3 4 5 6 7 8 9 10
var sourceFile = new SourceFile { Path = path, Text = File.ReadAllText(path), AttributeToAddToClass = new ExtraAttribute { Name = "AutoGenerateBuilder", ArgumentList = "typeof(AppDomain)" // The AutoGenerateBuilder attribute allows a Type argument, so provide that argument here. } }
For more details see the implementation from SourceFile and ExtraAttribute.
- 1 This
step 3: Call the Execute method
Call the extension method Execute
and provide the namespace and the SourceFile
.
The purpose from this Execute
method is to run the source generator and capture the output so that the output (path + c# file content) can be verified in the unit-test.
The internal logic follows these steps:
a. A SyntaxTree is created by parsing the source file as text.
b. In case the AttributeToAddToClass or AttributeToAddToInterface is defined, a new SyntaxTree with that new attribute is generated so that this updated class is found suitable to be processed by your custom ISyntaxReceiver
(required later in the process).
This means that the following example class-under-test is changed from this:
|
|
Into this:
|
|
c. Now a new CSharpCompilation is created from scratch based on the updated SyntaxTree(s).
d. Then a new CSharpGeneratorDriver is created and the RunGeneratorsAndUpdateCompilation
method is called to run all generator(s) and update the CSharpCompilation
object which was created in the previous step.
e. The previous step returns a new Compilation object which is an immutable representation of a single invocation of the compiler. This object contains the newly generated SyntaxTrees, where each SyntaxTree is an unique output which is generated by your generator.
f. The generated SyntaxTrees are converted to an helper result object which can be analyzed and verified in your unit-test.
step 4 & 5: Assert
The ExecuteResult contains a list of FileResult objects.
This FileResult
contains the SyntaxTree, source code as string and the path which can be asserted by the unit-test.
Conclusion
With the help of this custom helper code (NuGet Project: CSharp.SourceGenerators.Extensions) it’s now very easy to unit-test your Source Generator without having to resort on Mocking a lot of interfaces or writing complex testing code yourself.
📚 References
- Previous Post: Source Generators - Part 1
- GitHub project: FluentBuilder + CSharp.SourceGenerators.Extensions
- GitHub project: ProxyInterfaceGenerator
- C# Source Generators CheatSheet
- A list of C# Source Generators