Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9571

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

$
0
0

前書き

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

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

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

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

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

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

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

クラス

namespaceAnyProject{publicsealedclassClass1{privateintInstancePropertyValue=>2;privateintPrivateMethodIncludeLocalFunction(){intLocalFunction(intarg){returnarg*this.InstancePropertyValue;}returnLocalFunction(2);}privateintPrivateMethodIncludeStaticLocalFunction(){intStaticLocalFunction(intarg){returnarg*4;}returnStaticLocalFunction(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

テストコード

usingSystem;usingSystem.Reflection;usingAnyProject;usingMicrosoft.VisualStudio.TestTools.UnitTesting;namespaceAnyProjectTest{[TestClass]publicclassClass1Tests{[TestMethod]publicvoidTestLocalFunction(){// ArrangeTypetargetType=typeof(Class1);MethodInfomethod=targetType.GetMethod("<PrivateMethodIncludeLocalFunction>g__LocalFunction|2_0",BindingFlags.Instance|BindingFlags.NonPublic);objectinstance=Activator.CreateInstance(targetType);varparameters=newobject[]{2};// Actvaractual=(int)method.Invoke(instance,parameters);// AssertAssert.AreEqual(4,actual);}[TestMethod]publicvoidTestStaticLocalFunction(){// ArrangeTypetargetType=typeof(Class1);MethodInfomethod=targetType.GetMethod("<PrivateMethodIncludeStaticLocalFunction>g__StaticLocalFunction|3_0",BindingFlags.Static|BindingFlags.NonPublic);objectinstance=Activator.CreateInstance(targetType);varparameters=newobject[]{2};// Actvaractual=(int)method.Invoke(instance,parameters);// AssertAssert.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 が欲しいです。


Viewing all articles
Browse latest Browse all 9571

Trending Articles