2025-04-27

F# で Cmdlet を書いてる pt.66

from FsUnit.xUnit to Expecto

krymtkts/pocof 開発をした。 krymtkts/SnippetPredictor の開発で得た知識を展開している。

FSharpLint に代わる FSharp.Analyzer.SDK Ionide.Analyzers の導入は簡単に済んだ。 #340 良い。 bool Partial active Pattern のような F# 9 で増えた feature はチェックできてないみたい?なのでそこは追って調べていくつもり。

次に念願であり一番の大物である FsUnit.xUnit v6.xExpecto へ置き換えを始めた。 これはちょっと時間がかかりそうだ。 前にも触れたが、 FsUnit.xUnit v7 は xUnit v3 に依存してるが FsCheck は現状 v2 に依存している。 これによって依存関係が塩漬けされているので、この際乗り換えようという算段だ。

まずひとつ学んだのは、 FsUnit.xUnit(というか xUnit v2) と Expecto は共存できるということ。 ただし Expecto は YoloDev.Expecto.TestSdk を導入して Microsoft Testing Platform で動かす前提だ。 Expecto のテスト実行を以下のように EntryPoint に含めてもだめだった。 ちゃんと調べてないが xUnit v2 のテスト検出の仕組みでは EntryPoint はただの compile を通すだけのハリボテて対象に含まれないのだと思われる。

module Program

open Expecto

[<EntryPoint>]
let main argv =
runTestsInAssemblyWithCLIArgs [] argv // こうしても Expecto のテストが検出されない

先述の通り YoloDev.Expecto.TestSdk を使い以下の構成してみたらうまく両方のテストが実行されて素晴らしかった。 testing framework の共存・乗り換えってあまり人生でも起こらないし、いい経験になった。

  <!-- 略 -->
<ItemGroup>
<!-- ここから-->
<PackageReference Include="Expecto" Version="10.2.3" />
<PackageReference Include="YoloDev.Expecto.TestSdk" Version="0.15.3" />
<!-- ここまでが増えた-->
<!-- NOTE: FsCheck.Xunit does not support xUnit.v3 yet. -->
<PackageReference Include="FsCheck.Xunit" Version="3.2.0" />
<PackageReference Include="FsUnit.xUnit" Version="6.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<!-- NOTE: Replace xunit with xunit.v3 from FsUnit.xUnit 7. -->
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- 略 -->
</ItemGroup>
<!-- 略 -->

この辺なんでうまくいったのか理解できるような Microsoft Testing Platform の解像度を持ち得ていないので、そこを掘り下げるのは今後の宿題としたい。

これで FsUnit.xUnit 形式のテストを Expecto で書き換え始めたわけで、 test case の書き換えを始めた。 #343

変更に関しては、これまで以下の前者の形式だったやつを、後者の通りにするだけ。

    // FsUnit.xUnit
module Option =
// 略

[<Fact>]
let ``shouldn't call Dispose if Some.`` () =
// 略

// Expecto
[<Tests>]
let tests_Option =
testList
"Option"
// このカッコの書き方は行を余計に確保するが、この方がテストケースを足すとか先頭末尾のカッコの重なりを回避できてよろしい
[

test "When Some." {
// 略
mock.disposed |> Expect.isTrue "should call Dispose"
}

]

だがご覧の通り、 FsUnit.xUnit では module name と let で記述していたテストの内容を Expecto では name なり message に分割して記述する必要がある。 これまでデタラメ English で書いてた test case の説明を校正するような感じ。

人力でやるの辛いなと思い GitHub Copilot に託してみた。 最初 GitHub Copilot は Chat も Agent mode でもイマイチだったが、一度お手本を書いてやると良い感じに動き出した。恐ろしい学習力やな。 Copilot Agent はこの説明を校正するのを初手ではうまくやれなくて重複した文章を取り除いたりできなかったが、 session 中で回を重ねるとどんどん良くなる。恐ろしい子。

[<Fact>][<Test>] の書き換えは簡単にできるとわかったから、ざーっと全体を直してしまえそう。 その後は FsUnit.xUnit の FsCheck 統合 [<Property>] を Expecto 形式に直すイメージで進める。