Open sandboxFocusImprove this doc

Testing compile-time helper code

When building complex aspects, it's advisable to shift the intricate compile-time logic, such as code that queries the code model, to compile-time helper classes that are not aspects. Unlike aspects, these compile-time classes can be subjected to unit tests.

Benefits

Unit-testing compile-time classes offers the following advantages:

Creating unit tests for your compile-time code

Step 1. Disable pruning of compile-time code

In the project defining the compile-time code, set the MetalamaRemoveCompileTimeOnlyCode property to False:

<PropertyGroup>
    <MetalamaRemoveCompileTimeOnlyCode>False</MetalamaRemoveCompileTimeOnlyCode>
</PropertyGroup>

Failing to follow this step will result in an exception whenever any compile-time code is called from a unit test.

Step 2. Create an Xunit test project

Proceed to create an Xunit test project as you usually would.

It's strongly recommended to target .NET 6.0 or later, as temporary files cannot be automatically cleaned up with lower .NET versions.

Disable Metalama for the test project by defining the following property:

<PropertyGroup>
   <MetalamaEnabled>false</MetalamaEnabled>
</PropertyGroup>

Step 3. Reference the Metalama.Testing.UnitTesting package

<ItemGroup>
    <PackageReference Include="Metalama.Testing.UnitTesting" Version="CHANGE ME" />
</ItemGroup>

Step 4. Create a test class derived from UnitTestClass

Create a new test class that derives from UnitTestClass.

public class MyTests : UnitTestClass { }

Step 5. Create test methods

Each test method must call the CreateTestContext and must dispose of the context at the end of the test method.

Your test would typically call the context.CreateCompilation method to obtain an ICompilation.

Note

Some APIs (such as ExpressionFactory) require the execution context to be set and assigned to your compilation. To set the execution context in a test, use the testContext.WithExecutionContext method.

public class MyTests : UnitTestClass
{
    [Fact]
    public void SimpleTest()
    {
        // Create a test context and dispose of it at the end of the test.
        using var testContext = this.CreateTestContext();

        // Create a compilation.
        var code =
            """
            class C
            {
                void M1 () {}

                void M2()
                {
                    var x = 0;
                    x++;
                }
            }
            """;

        var compilation = testContext.CreateCompilation( code );

        // Switch the execution context to this compilation.
        using ( testContext.WithExecutionContext( compilation ) )
        {
            // Query the code model.
            var type = compilation.Types.OfName( "C" ).Single();
            var m1 = type.Methods.OfName( "M1" ).Single();

            // Here you can also call your helper classes.

            // Perform any assertion. Typically, your compile-time code would be called here.
            Assert.Equal( 0, m1.Parameters.Count );

            Assert.True( m1.ReturnType.Is( typeof(void) ) );
        }
    }
}