C#でprivateメソッドをテストする時の便利な書き方(実装の都合をテストコードから排除)

# はじめに
C#でprivateメソッドに対してMSTestで単体テストを作成する時の便利な書き方の紹介です。
privateメソッドをテストするには、実装の都合で面倒なコードを書く必要がありますが、それを解消します。

# 前提知識
C#のMSTestにて、privateのメソッド、プロパティ、フィールドを呼び出したい場合は、PrivateObjectクラスを用います。
また、staticでprivateのメンバーは、PrivateTypeクラスを用います。
以下に例を記します。

```c#
	// テスト対象のクラス
	public class MyClass
	{
		// privateメソッド
		private int AddValue(int additionValue)
		{
			return _Value + additionValue;
		}

		// privateプロパティ
		private bool IsZero => _Value == 0;

		// privateフィールド
		private int _Value = 0;

		// staticなprivateメソッド
		private static int GetTotalValue(int value1, int value2)
		{
			return value1 + value2;
		}
	}

	// MyClassのprivateなメンバーをテストするためのテストコード
	[TestClass]
	public class MyClassTest
	{
		// privateメソッドのテスト
		[TestMethod]
		public void AddValueTest()
		{
			// PrivateObjectを作成して、AddValueメソッドを呼び出す
			var myClass = new MyClass();
			var privateObject = new PrivateObject(myClass);
			var value = (int) privateObject.Invoke("AddValue", 1);
			Assert.AreEqual(1, value);
		}

		// privateプロパティとprivateフィールドのテスト
		[TestMethod]
		public void IsZeroTest()
		{
			// PrivateObjectを作成して、_ValueフィールドとIsZeroプロパティを呼び出す
			var myClass = new MyClass();
			var privateObject = new PrivateObject(myClass);

			// privateフィールドを取得
			var value = (int) privateObject.GetField("_Value");
			Assert.AreEqual(0, value);
			
			// privateプロパティを取得
			var isZero = (bool) privateObject.GetProperty("IsZero");
			Assert.IsTrue(isZero);
		}

		// staticなprivateメソッドのテスト
		[TestMethod]
		public void GetTotalValueTest()
		{
			// PrivateTypeを作成して、GetTotalValueメソッドを呼び出す
			var privateType = new PrivateType(typeof(MyClass));
			var value = (int) privateType.InvokeStatic("GetTotalValue", 1, 1);
			Assert.AreEqual(2, value);
		}
	}

```
詳細は以下を参照ください。
[MSTestでprivateメソッドをテストする | 山本隆の開発日誌](https://www.gesource.jp/weblog/?p=7742)

# 便利な書き方
そして便利な書き方の紹介です。
上記の通り、PrivateObjectとPrivateTypeのインスタンスをテストコードで毎回インスタンス化するのは面倒です。また、PrivateObjectなどを使うことは実装の都合であり、そのテストの関心事ではないため、テストコードからは排除したいです。
そこで、テストするメソッドと同じ名前で同じシグネチャの拡張メソッドを作成し、その拡張メソッドの中で、PrivateObjectを作成するようにします。
具体例を以下に記します。

```c#
		// MyClassの以下のメソッド用の拡張メソッドを定義
		// private int AddValue(int additionValue)

		public static int AddValue(this MyClass myClass, int additionValue)
		{
			return (int) new PrivateObject(myClass).Invoke("AddValue", additionValue);
		}
```
上記のように、拡張メソッドを定義しておけば、各テストコードは、シンプルに記載できます。
privateメソッドである AddValue(int additionValue) は、上記で定義した拡張メソッドにより、まるでprivateのメソッドをそのまま呼び出しているかように実行できます(実際にはpublicな拡張メソッドが呼び出されています)。もちろんインテリセンスも効きます。

```C#
			var myClass = new MyClass();
			var value = myClass.AddValue(1);
```

privateなプロパティとフィールドについても同様です。
ただ、C# 8.0時点の構文には「拡張プロパティ」はまだ無いため、IsZeroプロパティは IsZero() という拡張メソッドで呼び出す必要があります。() を付ける必要があるため、全く同じ呼び出し方にはなりませんが、それでも元のテストコードよりシンプルになります。
(拡張プロパティは、当初は C# 8.0 の候補だったので、C# 9.0くらいで実現されるかもしれません)
また、staticなprivateメソッドに関しては、拡張メソッドで代替できないため、別のクラス(ここではMyClassExtensionsクラス)で、同じ名前とシグネチャのstaticメソッドを定義します。

上記の方法をすべて行うことで、実装の都合であるPrivateObjectとPrivateTypeをテストコードから排除できます。
以下に、冒頭に記載したMyClassのテストを改善した例を記載します。

```C#
	// 拡張メソッドの定義(テストプロジェクト内で定義)
	public static class MyClassExtensions
	{
		// privateメソッド用の拡張メソッド
		public static int AddValue(this MyClass myClass, int additionValue)
		{
			return (int) new PrivateObject(myClass).Invoke("AddValue", additionValue);
		}

		// privateプロパティ用の拡張メソッド
		public static bool IsZero(this MyClass myClass)
		{
			return (bool) new PrivateObject(myClass).GetProperty("IsZero");
		}

		// privateフィールド用の拡張メソッド
		public static int _Value(this MyClass myClass)
		{
			return (int) new PrivateObject(myClass).GetField("_Value");
		}

		// staticなprivateメソッド用のstaticメソッド
		public static int GetTotalValue(int value1, int value2)
		{
			return (int) new PrivateType(typeof(MyClass)).InvokeStatic("GetTotalValue", value1, value2);
		}
	}

	// テストコードは、PrivateObjectとPrivateTypeを用いずに、シンプルに記載
	[TestClass]
	public class MyClassTest
	{
		// privateメソッドのテスト
		[TestMethod]
		public void AddValueTest_Simple()
		{
			var myClass = new MyClass();
			var value = myClass.AddValue(1);
			Assert.AreEqual(1, value);
		}
		
		// privateプロパティとprivateフィールドのテスト
		[TestMethod]
		public void IsZeroTest_Simple()
		{
			var myClass = new MyClass();

			// privateフィールドを取得
			var value = myClass._Value();
			Assert.AreEqual(0, value);
			
			// privateプロパティを取得
			var isZero = myClass.IsZero();
			Assert.IsTrue(isZero);
		}

		// staticなprivateメソッドのテスト
		[TestMethod]
		public void GetTotalValueTest_Simple()
		{
			var value = MyClassExtensions.GetTotalValue(1, 1);
			Assert.AreEqual(2, value);
		}
	}
```

# まとめ
C#でprivateなメソッド、プロパティ、フィールドに対してテストコードを書く際に、拡張メソッドを用いることで、シンプルに書ける方法を紹介しました。
本稿の内容を活用して [こちら](https://www.lightning-review.com/) のツールを作ったりしています。