【Unity】【Rider】Unity向けIncremental Source Generatorのテストを書く

RiderでUnity向けIncremental Source Generatorのテストを書く方法です。

Rider 2024.3.6
Unity 2022.3.28f1

はじめに

先日、以下の記事で Unity 向けの Incremental Source Generator の作り方をまとめました。

light11.hatenadiary.com

上記の記事では動作確認環境を用意しましたが、この環境では生成が失敗した時のデバッグが行いづらいので、本記事では Incremental Source Generator のテストの書き方をまとめます。

なお本記事は上記の記事の内容を前提とし、上記の記事内で作成した Incremental Source Generator に対するテストを書く形で進めます。

テスト用プロジェクトを作る

まず上述の記事で作成したソリューションに対して、新しくテスト用のプロジェクトを追加します。
下図のようにテンプレートには Unit Test Project を選択し、Type には xUnit を設定します。

Unit Test Project

次に必要なパッケージを追加します。
作ったプロジェクトを右クリック > Manager NuGet Packages を選択し、Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit をインストールします。

パッケージを追加したらプロジェクトをエディタ部分にドラッグ&ドロップし、プロジェクトの設定を行います。

D&D

上述の記事で作成した Incremental Source Generator である ExampleSourceGenerator への ProjectReference を追加します。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <IsPackable>false</IsPackable>
        <IsTestProject>true</IsTestProject>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="coverlet.collector" Version="6.0.0"/>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
        <PackageReference Include="xunit" Version="2.5.3"/>
        <PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
    </ItemGroup>

    <ItemGroup>
        <Using Include="Xunit"/>
    </ItemGroup>

    <ItemGroup>
        <ProjectReference Include="..\ExampleSourceGenerator\ExampleSourceGenerator.csproj"/>
    </ItemGroup>

</Project>

これでプロジェクトの準備は完了です。

テストを書く

プロジェクトの準備が完了したらテストを書きます。

適当なファイルに以下を記述します。

using ExampleSourceGenerator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace ExampleSourceGeneratorTest;

public sealed class ExampleGeneratorTests
{
    [Fact]
    public void Test_GeneratedProperty_IsGenerated()
    {
        // テスト対象としての入力コードを定義
        var testSourceCode = @"
using System;
namespace TestNamespace
{
    [Example]
    public partial class TestClass
    {
    }
}";
        // ExampleAttributeを定義(↑とまとめてもOK)
        var attributeCode = @"
using System;
namespace TestNamespace
{
    [AttributeUsage(AttributeTargets.Class)]
    public class ExampleAttribute : Attribute
    {
    }
}";
        // 入力コードと属性コードをパースしてSyntaxTreeを作成
        var testSyntaxTree = CSharpSyntaxTree.ParseText(testSourceCode);
        var attributeSyntaxTree = CSharpSyntaxTree.ParseText(attributeCode);

        // 必要なアセンブリ参照を取得
        var references = AppDomain.CurrentDomain
            .GetAssemblies()
            .Where(assembly => !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location))
            .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
            .Cast<MetadataReference>()
            .ToArray();

        // コンパイルを作成
        var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
        var compilation = CSharpCompilation.Create(
            "TestAssembly", // 名前はなんでもOK
            new[] { testSyntaxTree, attributeSyntaxTree }, // 入力としてのコードを設定
            references,
            compilationOptions);

        // SourceGeneratorのテストはこのようにCSharpGeneratorDriverを使って行う
        var generator = new ExampleGenerator();
        var driver = CSharpGeneratorDriver.Create(generator);

        // ジェネレーターの実行結果を取得します
        var runResult = driver.RunGeneratorsAndUpdateCompilation(
                compilation,
                out var outputCompilation,
                out var generateDiagnostics)
            .GetRunResult();

        // 正常に生成されていることを確認
        Assert.Single(runResult.GeneratedTrees);
        Assert.Empty(runResult.Diagnostics);

        // 生成されたソースコードを取得
        var generatedTree = runResult.GeneratedTrees.First();
        var generatedCode = generatedTree.GetText().ToString();

        // 期待する出力
        var expectedCode = @"
using System;
namespace TestNamespace
{
    public partial class TestClass
    {
        public string GeneratedProperty { get; set; } = ""Hello, World!"";
    }
}
";
        // テスト
        Assert.Equal(NormalizeCode(expectedCode), NormalizeCode(generatedCode));
    }

    // コードのホワイトスペースと改行を正規化
    private static string NormalizeCode(string code)
    {
        return string.Concat(code.Where(c => !char.IsWhiteSpace(c)));
    }
}

説明はコメントに記載した通りです。

Source Generator のテストはこの例のように、実際にジェネレータを実行し、成果物の文字列が期待するものと一致するかを比較します。

Roslyn による構文解析の知識が必要になりますが、これについては以下の記事にまとめているので必要に応じて参照してください。

light11.hatenadiary.com

テストを実行する

テストが書けたら Unit Tests タブからテストを実行し、成功することを確認します。

テストを実行

関連

light11.hatenadiary.com

light11.hatenadiary.com