ここ数ヶ月くらい、ずっと VRChat 向けの Roslyn Analyzer を作って得られた知見を放出していく系私です。
今回は、ユニットテストのお話。
Roslyn Analyzer のテストは、多くの場合、ソースコードをインプットとして、どの Diagnostic が、どういった場所に、どのようなメッセージで報告されるのか、についてテストを行います。
そのときに個人的に面倒だと思うのが、「どの場所に」レポートが報告されるのかです。
例えば、以下のような入力コードがあった場合。
using UdonSharp; namespace UdonRabbit { public class TestBehaviour : UdonSharpBehaviour { private int? _i; private bool? TestMethod(bool? b) { return null; } } }
この状態で、 WithSpan(10, 34, 10, 39)
みたいに期待値を書かれても、いったいどこだといった感じになります。
私はなりました。
また、コード上と内部情報とメッセージとで、なんか +1 されてたりそのままだったりしてわけわからん、ってなるので、テストを修正しようにもちょっとわからん、ってなります。
ということで、私は以下のようにテストコードを入力するようにしました。
using UdonSharp; namespace UdonRabbit { public class TestBehaviour : UdonSharpBehaviour { private [|int?|] _i; private [|bool?|] TestMethod([|bool?|] b) { return null; } } }
診断レポートが表示されるべき場所を、 [|...|]
で囲い、テスト用のプロジェクト実行時に該当部分を WithSpan
で渡すようにします。
また、このままでは C# の文法的に Valid ではないので、[|
と |]
についても取り除きます。
上で貼ったリポジトリの場合には、以下のようにして Analyzer のテストを書くことが出来ます。
[Fact] public async Task UdonSharpBehaviourNullableTypeHasDiagnosticsReport() { var diagnostic = ExpectDiagnostic(NotSupportNullableTypes.ComponentId) .WithSeverity(DiagnosticSeverity.Error); const string source = @" using UdonSharp; namespace UdonRabbit { public class TestBehaviour : UdonSharpBehaviour { private [|int?|] _i; private [|bool?|] TestMethod([|bool?|] b) { return null; } } } "; await VerifyAnalyzerAsync(source, Enumerable.Repeat(3).Select(_ => diagnostic)); }
VerifyAnalyzerAsync
の内部では、以下のような処理を行っています。
protected async Task VerifyAnalyzerAsync(string source, param DiagnosticResult[] expected) { var testProject = new TestProject(...); // テスト用 Unity プロジェクトの生成 ParseInputSource(testProject, source, expected); await testProject.RunAsync(CancellationToken.None); // 各種 Assertion } private void ParseInputSource(TestProject testProject, string source, DiagnosticResult[] expected) { var sb = new StringBuilder(); var diagnostics = expected.ToList(); var line = 1; var column = 1; var expectedLine = 0; var expectedColumn = 0; var isReading = false; var i = 0; using var sr = new StringReader(source); while (sr.Peek() > -1) { var c = sr.Read(); switch (c) { case '\n': sb.Append((char) c); line++; column = 1; break; case '[' when sr.Peek() == '|': sr.Read(); expectedLine = line; expectedColumn = column; isReading = true; break; case '|' when isReading && sr.Peek() == ']': sr.Read(); diagnostics[i] = diagnostics[i].WithSpan(expectedLine, expectedColumn, line, column); i++; isReading = false; break; default: sb.Append((char) c); column++; break; } } testProject.ExpectedDiagnostics.AddRange(diagnostics); testProject.SourceCode = sb.ToString(); }
あとは、通常通り、位置が正しいかどうかを Assert するコードを書いてあげれば OK です。
個人的には、これでどこにレポートが報告されるべきか、書くのも見るのもわかりやすくなったかな、と思っています。
ちなみに、おとなしく自動生成された VerifyCS
コードを使えば、上記と同じ事が出来ます。
が、今回の場合、カスタムしたものを使っているので、自前で実装しています。
ということで、メモでした。