dotnet.exeでC#の単体テスト(MSTest)プロジェクトの作り方

C# コンピュータ
C#

個人でプログラミングを楽しむ分にはどのような方法で開発しても構わないと思いますが、すこし規模が大きくなると、クラス単位に機能や役割を分割することになり、それらを単体テストする必要性を感じることがあります。

今回は、テストプロジェクトの作成~単体テストの実行までの流れをざっくり把握したいと思います。

プロジェクト作成

単体テストのプロジェクトを作るにあたって、複数のプロジェクトを扱うことに成りますのでソリューションを作成します。

PowerShell

mkdir  MSTestSample01
cd  MSTestSample01
dotnet new sln

” MSTestSample01″がソリューション名に成ります。
ソリューションに単体テストを含むプロジェクトを登録する形になります。またソリューション用のディレクトリを作成しましたので、こちらにサブディレクトリとしてプロジェクト用のディレクトリを作成していくことになります。

被テスト対象となるプロジェクトを作成します。多分プロジェクトの種類は何でも良いのだと思うのですが、WinFormやWPFのViewに相当する部分はMSTestによる単体テストは難しいと思うので、テストしやすそうなシンプルなクラスということでClassLib(クラスライブラリ)でプロジェクトを作成します。

PowerShell

mkdir MSTestSample01.Infra
cd MSTestSample01.Infra
dotnet new classlib

プロジェクト名は”MSTestSample01.Infra”になります。
プロジェクトをソリューションに登録します。

cd ..
dotnet sln add MSTestSample01.Infra/MSTestSample01.Infra.csproj

次に単体テスト(MSTest)のプロジェクトを作成します。

PowerShell

mkdir MSTestSample01Tests
cd MSTestSample01Tests
dotnet new mstest
cd ..
dotnet sln add MSTestSample01Tests/MSTestSample01Tests.csproj

プロジェクト名は”MSTestSample01Tests”になります。

ディレクトリツリーは以下の様になりました。

フォルダー パスの一覧
MSTestSample01
│  MSTestSample01.sln
│  tree.txt
│  
├─MSTestSample01.Infra
│  │  Class1.cs
│  │  MSTestSample01.Infra.csproj
│  │  
│  └─obj
│          MSTestSample01.Infra.csproj.nuget.dgspec.json
│          MSTestSample01.Infra.csproj.nuget.g.props
│          MSTestSample01.Infra.csproj.nuget.g.targets
│          project.assets.json
│          project.nuget.cache
│          
└─MSTestSample01Tests
    │  MSTestSample01Tests.csproj
    │  UnitTest1.cs
    │  
    └─obj
            MSTestSample01Tests.csproj.nuget.dgspec.json
            MSTestSample01Tests.csproj.nuget.g.props
            MSTestSample01Tests.csproj.nuget.g.targets
            project.assets.json
            project.nuget.cache
            

テストをするため、MSTestSample01TestsからMSTestSample01.Infraを参照する設定を行います。

PowerShell

cd MSTestSample01Tests
dotnet add reference ../MSTestSample01.Infra/MSTestSample01.Infra.csproj

これでテストを行うプロジェクトが作成されました。

単体テスト

続いてテストコードを書いていきます。

ファイル名:MSTestSample01.Infra/Class1.cs

namespace MSTestSample01.Infra;

public class Class1
{
    // 加算
    public static int Add(int a, int b)
    {
        return a + b;
    }
}

こちらは、テストされる対象のクラスで、テストしやすいようにstaticな加算するメソッドを用意しました。インスタンスを作成する必要が無いですし、メソッド内で完結している点がテストしやすいと考えます。

ファイル名:MSTestSample01Tests/UnitTest1.cs

using MSTestSample01.Infra;

namespace MSTestSample01Tests;

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void 加算のテスト()
    {
        int a = 2;
        int b = 3;

        int actual = Class1.Add(a, b); // ... 実際の結果
        int expected = 5; // ... 期待される値

        Assert.AreEqual(expected, actual);
    }
}

こちらはテストする側のコードに成ります。
UnitTest1のコンストラクタは?とか[TestMethod]属性はどんな意味がある?など疑問がわきますが、とりあえずこれでテストを実行することが出来ます。
あと人が呼び出す予定がないのでテストメソッドの名は日本語を使っています。

テストの実行はソリューションディレクトリで以下のコマンドを実行します。

dotnet test
  Determining projects to restore...
  H:\csharp\console\MSTestSample01\MSTestSample01Tests\MSTestSample01Tests.csproj を復元しました (428 ミリ秒)。
  2 個中 1 個の復元対象のプロジェクトは最新です。
  MSTestSample01.Infra -> H:\csharp\console\MSTestSample01\MSTestSample01.Infra\bin\Debug\net8.0\MSTestSample01.Infra.d
  ll
  MSTestSample01Tests -> H:\csharp\console\MSTestSample01\MSTestSample01Tests\bin\Debug\net8.0\MSTestSample01Tests.dll
H:\csharp\console\MSTestSample01\MSTestSample01Tests\bin\Debug\net8.0\MSTestSample01Tests.dll (.NETCoreApp,Version=v8.0) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。

成功!   -失敗:     0、合格:     1、スキップ:     0、合計:     1、期間: 43 ms - MSTestSample01Tests.dll (net8.0)

多分成功していると思うので、コードを書き替えてわざと失敗してみます。

変更したソース

using MSTestSample01.Infra;

namespace MSTestSample01Tests;

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void 加算のテスト()
    {
        int a = 2;
        int b = 3;

        int actual = Class1.Add(a, b); // ... 実際の結果
        int expected = 4; // ... 期待される値

        Assert.AreEqual(expected, actual);
    }
}

こちらでテストを実行してみます。

dotnet test
  Determining projects to restore...
  復元対象のすべてのプロジェクトは最新です。
  MSTestSample01.Infra -> H:\csharp\console\MSTestSample01\MSTestSample01.Infra\bin\Debug\net8.0\MSTestSample01.Infra.d
  ll
  MSTestSample01Tests -> H:\csharp\console\MSTestSample01\MSTestSample01Tests\bin\Debug\net8.0\MSTestSample01Tests.dll
H:\csharp\console\MSTestSample01\MSTestSample01Tests\bin\Debug\net8.0\MSTestSample01Tests.dll (.NETCoreApp,Version=v8.0) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
  失敗 加算のテスト [71 ms]
  エラー メッセージ:
   Assert.AreEqual に失敗しました。<4> が必要ですが、<5> が指定されました。
  スタック トレース:
     at MSTestSample01Tests.UnitTest1.加算のテスト() in H:\csharp\console\MSTestSample01\MSTestSample01Tests\UnitTest1.cs:line 17
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)


失敗!   -失敗:     1、合格:     0、スキップ:     0、合計:     1、期間: 148 ms - MSTestSample01Tests.dll (net8.0)

失敗したのでテストプロジェクトは機能しているようんです。

あとはテストコードを書いて、テストされるメソッドやプロパティを書いてを繰り返すことに成ります。

Assertメソッドの一覧

使いそうなAssertメソッドの一覧に成ります。

<

dl>

Assert.AreEqual(expected, actual)

等しい。expectedが期待される値、actualが実際の結果
Assert.AreNotEqual(notExpected, actual)

等しくない
Assert.IsTrue(condition)

Assert.IsFalse(condition)

Assert.AreSame(expected, actual)

expectedとactualが同じインスタンスか検証する。
Assert.AreNotSame(expected, actual)

expectedとactualが異なるインスタンスか検証する。
Assert.IsNull(value)

NULLである。
Assert.IsNotNull(value)

NULLでない。
Assert.IsInstanceOfType(value, expectedType)

valueがexpectedTypeで指定した型か検証する。型はtypeof(型名)で取得出来る。
Assert.IsNotInstanceOfType(value, wrongType)

valueがwrongTypeで指定した型でないことを検証する。
Assert.ThrowsException<T>(action)

actionでT型の例外が発生することを検証

感想

ここまでの試してみて、テストしやすいクラスとそうで無いクラスがある感じです。
最初に述べたGUIアプリのView部分は単体テストを行う方法が思いつきません。逆にコントロールなどのView特有の部分を切り離したViewModelやModel部分は単体テストが出来ると思います。
また、テスト用にデータベースなど構築することが難しいオブジェクトに依存したクラスをテストする場合、スタブ(データ)やモック(振る舞い)などのダミーオブジェクトを用意してテストを行うことが出来るようです。モックを作成するモジュールがあるのでそのうち試したいと思います。

コメント