LoginSignup
2
2

More than 3 years have passed since last update.

【C#】ローカル関数をテストする。

Last updated at Posted at 2021-03-31

前書き

ここ数日 Twitter で private メンバーに対するテストについての議論を見かけました。
自分のスタンスは
「基本的に public メンバーに対するテストがあれば十分だけど、やんごとなき理由があるなら private メンバーのテストも書けば良い」という感じです。

考えうるやんごとなき理由 :thinking:
  • チームの文化 地獄か?
  • public メンバーからのテストだとめちゃ Assert し辛い
  • そのメンバーが業務上超重要なのでマジ絶対どうしてもテスト書きたい

議論の内容や自分の主張は本題ではないので置いておくのですが、一連の TL を見ていてふと「private メソッドはリフレクションでテスト出来るけどローカル関数はテスト出来るのか?」と気になりました。
ローカル関数のテストは今まで試したことが無かったので知見がありませんでした。

この記事は「ローカル関数のテストは可能か否かが気になったので試してみて結果どうだったのか」という記事です。
決して private メンバーやローカル関数のテストを推奨するものではありません。

ローカル関数のテストは出来るのか?どうやってやるのか?

結論から書くとリフレクションで出来ます。
対象のローカル関数を取得する際には MSIL になった時点のメソッド名などを指定する必要があるので、その点に注意する必要があります。

クラス、MSIL、テストコード の例を以下に示します。

クラス

namespace AnyProject
{
    public sealed class Class1
    {
        private int InstancePropertyValue => 2;

        private int PrivateMethodIncludeLocalFunction()
        {
            int LocalFunction(int arg)
            {
                return arg * this.InstancePropertyValue;
            }

            return LocalFunction(2);
        }

        private int PrivateMethodIncludeStaticLocalFunction()
        {
            int StaticLocalFunction(int arg)
            {
                return arg * 4;
            }

            return StaticLocalFunction(2);
        }
    }
}

上記のクラスから生成される MSIL

.class public sealed auto ansi beforefieldinit
  AnyProject.Class1
    extends [System.Runtime]System.Object
{

  .method private hidebysig specialname instance int32
    get_InstancePropertyValue() cil managed
  {
    .maxstack 8

    // [11 42 - 11 43]
    IL_0000: ldc.i4.2
    IL_0001: ret

  } // end of method Class1::get_InstancePropertyValue

  .method private hidebysig instance int32
    PrivateMethodIncludeLocalFunction() cil managed
  {
    .maxstack 8

    // [15 7 - 15 31]
    IL_0000: ldarg.0      // this
    IL_0001: ldc.i4.2
    IL_0002: call         instance int32 AnyProject.Class1::'<PrivateMethodIncludeLocalFunction>g__LocalFunction|2_0'(int32)
    IL_0007: ret

  } // end of method Class1::PrivateMethodIncludeLocalFunction

  .method private hidebysig instance int32
    PrivateMethodIncludeStaticLocalFunction() cil managed
  {
    .maxstack 8

    // [22 7 - 22 37]
    IL_0000: ldc.i4.2
    IL_0001: call         int32 AnyProject.Class1::'<PrivateMethodIncludeStaticLocalFunction>g__StaticLocalFunction|3_0'(int32)
    IL_0006: ret

  } // end of method Class1::PrivateMethodIncludeStaticLocalFunction

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
    IL_0006: ret

  } // end of method Class1::.ctor

  .method private hidebysig instance int32
    '<PrivateMethodIncludeLocalFunction>g__LocalFunction|2_0'(
      int32 arg
    ) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [17 37 - 17 69]
    IL_0000: ldarg.1      // arg
    IL_0001: ldarg.0      // this
    IL_0002: call         instance int32 AnyProject.Class1::get_InstancePropertyValue()
    IL_0007: mul
    IL_0008: ret

  } // end of method Class1::'<PrivateMethodIncludeLocalFunction>g__LocalFunction|2_0'

  .method assembly hidebysig static int32
    '<PrivateMethodIncludeStaticLocalFunction>g__StaticLocalFunction|3_0'(
      int32 arg
    ) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [24 50 - 24 57]
    IL_0000: ldarg.0      // arg
    IL_0001: ldc.i4.4
    IL_0002: mul
    IL_0003: ret

  } // end of method Class1::'<PrivateMethodIncludeStaticLocalFunction>g__StaticLocalFunction|3_0'

  .property instance int32 InstancePropertyValue()
  {
    .get instance int32 AnyProject.Class1::get_InstancePropertyValue()
  } // end of property Class1::InstancePropertyValue
} // end of class AnyProject.Class1

テストコード

using System;
using System.Reflection;
using AnyProject;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AnyProjectTest
{
    [TestClass]
    public class Class1Tests
    {
        [TestMethod]
        public void TestLocalFunction()
        {
            // Arrange
            Type targetType = typeof(Class1);
            MethodInfo  method = targetType.GetMethod(
                "<PrivateMethodIncludeLocalFunction>g__LocalFunction|2_0", 
                BindingFlags.Instance | BindingFlags.NonPublic);
            object instance = Activator.CreateInstance(targetType);
            var parameters = new object[] { 2 };

            // Act
            var actual = (int)method.Invoke(instance, parameters);

            // Assert
            Assert.AreEqual(4, actual);
        }

        [TestMethod]
        public void TestStaticLocalFunction()
        {
            // Arrange
            Type targetType = typeof(Class1);
            MethodInfo method = targetType.GetMethod(
                "<PrivateMethodIncludeStaticLocalFunction>g__StaticLocalFunction|3_0", 
                BindingFlags.Static | BindingFlags.NonPublic);
            object instance = Activator.CreateInstance(targetType);
            var parameters = new object[] { 2 };

            // Act
            var actual = (int)method.Invoke(instance, parameters);

            // Assert
            Assert.AreEqual(8, actual);
        }
    }
}

注意点

前述の通りリフレクションでローカル関数を取得する際は MSIL 上でのメソッド名を指定する必要があります。
ローカル関数の MSIL 上でのメソッド名は自動的に決まるため、クラス内のメンバー定義が変わるとメソッド名も変更される場合があります。
※メソッド名の末尾についている 2_03_0 などの値がコロコロ変わる。

また、ローカル関数を定義する際に static が明示されていなくても静的ローカル関数として取り扱うことが出来る場合は自動的に static で定義されます。
※ローカル関数 StaticLocalFunction(int) は static を明示していないにも関わらず MSIL 上では static で定義されている。

そのため

  1. クラスのメンバー定義が変わる
  2. MSIL 上のメソッド名が変わる
  3. メソッドが取得出来ずテストが通らない

といった事態や

  1. 今まで静的ローカル関数でなかったものが意図せず静的ローカル関数になる
  2. 指定すべき BindingFlags が変わる
  3. メソッドが取得出来ずテストが通らない

といった事態に注意する必要があります。
テストコードの保守性は控えめに言って劣悪だと思うのでローカル関数のテストは基本的に避けるべきだと言えます。

以上です。

余談

C# には Internal メンバーを特定のアセンブリに公開する仕組みとして InternalsVisibleToAttribute が有りますが、似たような感じで PrivatesVisibleToAttribute が欲しいです。

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2