主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ
書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。
2021/06/07
ここ数ヶ月くらい、ずっと 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
コードを使えば、上記と同じ事が出来ます。
が、今回の場合、カスタムしたものを使っているので、自前で実装しています。
ということで、メモでした。