MSBuildでは、AfterTargets属性を使用することで、既存のターゲット(<Target>)の後に実行させたい処理を追加することができ、これにより動作を拡張することができる。

ここでは、dotnet testでテストの実行が完了した後に、独自の処理(ターゲット)を実行させる場合を考える。

テストが成功する場合のみに実行させたい場合は、単にAfterTargets属性を使用することで実現できる。 (§.AfterTargets属性を使用する)

一方、AfterTargets属性に指定されているターゲットでエラーが発生した場合には実行されないため、テストが失敗した場合は実行されなくなる。 そのため、AfterTargets属性以外の方法をとる必要がある。 ここでは、VSTestターゲットをオーバーライドする方法を挙げる。

AfterTargets属性を使用する

実行したい処理を記述したターゲットにおいてAfterTargets属性を指定すれば、そのターゲットの正常終了時に処理を実行させることができる。 dotnet testコマンドでは、.NET SDKから読み込まれるVSTestターゲットが実行されるので、AfterTargets属性にVSTestを指定すればよい。

プロジェクトファイル
<Project Sdk="Microsoft.NET.Sdk">
    中略

  <!-- VSTestターゲット(dotnet test)の実行が終わった時に実行したい処理を記述するターゲット -->
  <Target Name="AfterVSTest" AfterTargets="VSTest">
    <Message Importance="high" Text="すべてのテストが終了しました" Condition=" '$(TargetFramework)' == '' " />
  </Target>

</Project>

ここではVSTestターゲットの実行が終わったあとに独自に定義したターゲットAfterVSTestを実行するようにする。 AfterVSTestでは単にメッセージを表示する。

VSTestターゲットが正常終了する場合(失敗したテストがない場合)は、次のように期待どおりにターゲットが実行される。

テストが成功するときの出力
$dotnet test --nologo --no-restore --no-build
/home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (.NETCoreApp,Version=v6.0) のテスト実行
テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。

成功!   -失敗:     0、合格:     1、スキップ:     0、合計:     1、期間: 22 ms - /home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (net6.0)
  すべてのテストが終了しました

一方で、VSTestターゲットが失敗する場合(失敗するテストがある場合)は、AfterVSTestは実行されない。

テストが失敗するときの出力
$dotnet test --nologo --no-restore --no-build
/home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (.NETCoreApp,Version=v6.0) のテスト実行
テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
  失敗 Test [33 ms]
  エラー メッセージ:
   失敗しました
  スタック トレース:
     at Tests.Test() in /home/smdn/Projects/ExtendVSTestTarget/UnitTest1.cs:line 6


失敗!   -失敗:     1、合格:     0、スキップ:     0、合計:     1、期間: 33 ms - /home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (net6.0)
                             
    ^--- ターゲットが実行されないため、メッセージは出力されない

VSTestターゲットをオーバーライドする

VSTestターゲットの結果によらず終了時にターゲットを実行させるために、VSTestをオーバーライドする。

まず、VSTestをオーバーライドするために、Directory.Build.targetsに以下の内容を記述する。

Directory.Build.targets
<Project>
  <!--
    VSTestターゲットをオーバーライドするための.targetsファイルをインポートする。

    ここで、「プロパティIsVSTestTargetOverriddenが空の場合のみ」インポートするという条件を
    加えることにより、オーバーライドしたVSTestターゲットの呼び出しが循環しないようにする。
    (オーバーライドされていない状態のVSTestターゲットが実行されるようにする)
  -->
  <Import Project="OverrideVSTest.targets" Condition=" '$(IsVSTestTargetOverridden)' == '' " />
</Project>

ここでは、実際にオーバーライドを行う.targetsファイルのインポートを行う。 また、オーバーライドによって呼び出しが循環しないよう、インポートの際にフラグとして用意したIsVSTestTargetOverriddenプロパティをチェックする。

この内容をプロジェクトファイル内に記述するとオーバーライドされないため注意。 VSTestターゲットは、.NET SDKにより暗黙的に読み込まれる.targetsファイルにて定義されているので、それより後に読み込まれるDirectory.Build.targetsなどに記述する必要がある。


続いて、実際にオーバーライドを行うために、OverrideVSTest.targetsに以下の内容を記述する。

OverrideVSTest.targets
<!--
  VSTestターゲットをオーバーライドする.targetsファイル
-->
<Project>
  <!--
    VSTestターゲットを再定義することにより、VSTestターゲットをオーバーライドする。
  -->
  <Target Name="VSTest">
    <!--
      MSBuildタスクを用いて、現在のプロジェクトに対してVSTestターゲットを実行する。
    -->
    <MSBuild
      Projects="$(MSBuildProjectFullPath)"
      Targets="VSTest"
      Properties="IsVSTestTargetOverridden=true"
      ContinueOnError="ErrorAndContinue"
    />
    <!--
      ここで、プロパティにIsVSTestTargetOverridden=trueを追加して実行することにより、
      このtargetsファイルのインポートを行わないようにする。
      これによって、オーバーライドされていない状態のVSTestターゲットが実行されるようになる。

      また、ContinueOnErrorにErrorAndContinueを指定することにより、
      VSTestターゲットでエラーが発生した場合でも処理を継続させるようにする。
      これによって、上記のVSTestターゲット呼び出しがエラーで失敗した場合でも、
      これ以降のタスクは実行される。
    -->

    <!--
      VSTestAfterTargetsプロパティで定義されているターゲットをすべて呼び出す。

      成功・失敗に関わらず指定されたターゲット実行するために、AfterTargets属性の代わりに
      CallTargetタスクを用いて、指定されたターゲットを呼び出す。
    -->
    <CallTarget Targets="$(VSTestAfterTargets)" />
  </Target>
</Project>

このファイルでの要点は以下のとおり。

  • VSTestと同名のターゲットを定義することでオーバーライドする
  • MSBuildタスクで現在のプロジェクトを再起呼び出しして、オーバーライドされていないVSTestを呼び出す
    • このとき、オーバーライドされていることを表すプロパティIsVSTestTargetOverriddenを与えた上で呼び出すことで、再度オーバーライドされないようにする
    • また、ContinueOnError属性にErrorAndContinueを指定することにより、エラーが発生しても後続のタスクの実行を継続させる
  • AfterTargets属性の代わりに、CallTargetタスクを使ってVSTestターゲットの後に実行するターゲットを呼び出す

ここまでの内容で、VSTestが終了したらVSTestAfterTargetsプロパティに代入されているターゲットが呼び出されるようになる。

最後に、プロジェクトファイルにて実行したいターゲットの定義と、VSTestAfterTargetsプロパティへの設定を行う。

プロジェクトファイル
<Project Sdk="Microsoft.NET.Sdk">
    中略

  <PropertyGroup>
    <!--
      VSTestターゲットの実行が終わった時に実行したいターゲットの名前の一覧を定義するプロパティ。
      ここではAfterVSTestを実行させる。
    -->
    <VSTestAfterTargets>AfterVSTest</VSTestAfterTargets>
  </PropertyGroup>

  <!-- VSTestターゲットの実行が終わった時に実行したい処理を記述するターゲット -->
  <Target Name="AfterVSTest">
    <Message Importance="high" Text="すべてのテストが終了しました" Condition=" '$(TargetFramework)' == '' " />
  </Target>

</Project>

実際にdotnet testを実行すると、次のようにテストが失敗した場合でもターゲットが実行される。

テストが失敗するときの出力
$dotnet test --nologo --no-restore --no-build
/home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (.NETCoreApp,Version=v6.0) のテスト実行
テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
  失敗 Test [37 ms]
  エラー メッセージ:
   失敗しました
  スタック トレース:
     at Tests.Test() in /home/smdn/Projects/ExtendVSTestTarget/UnitTest1.cs:line 6


失敗!   -失敗:     1、合格:     0、スキップ:     0、合計:     1、期間: 36 ms - /home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (net6.0)
  すべてのテストが終了しました

また、テストが成功する場合も同様にターゲットが実行される。

テストが成功するときの出力
$dotnet test --nologo --no-restore --no-build
/home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (.NETCoreApp,Version=v6.0) のテスト実行
テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。

成功!   -失敗:     0、合格:     1、スキップ:     0、合計:     1、期間: 22 ms - /home/smdn/Projects/ExtendVSTestTarget/bin/Debug/net6.0/Test.dll (net6.0)
  すべてのテストが終了しました

以上の内容は.NET 6.0/7.0 SDKにて動作確認済み。 またdotnet testコマンドでは動作確認したものの、Visual Studio等のIDE経由でも同様に動作するかどうかは未確認。