簡易的にテスト駆動するためのUnity用コンポーネント

#この記事を書いた人

積木製作( http://tsumikiseisaku.com/ )という
VRなどのコンテンツを作っている会社でエンジニアをやってます。
もくもく会( https://kinmokukai.peatix.com/view )の主催とかもしています。

#概要

18/4/28に開催された「xRもくもく会 vol.02( https://hololens.connpass.com/event/82130/ )」で
あんまり進捗があがらなかった事を隠すためにちょろっとご紹介した、自作のTDD用コンポーネントを晒します。

# このコードを書いた理由
社内にテストの文化がなく、非エンジニアからスクリプトを触るようになった人が多い会社です。
そのため、まずはテストを楽しんでもらおうと考え、
「特別なアトリビュートなどを使わず、ぱっと試せる」ことを目的として用意しました。



#コード
```TestComponent.cs
namespace BasicExtends {
    using UnityEngine;
    using System.Text;

    /// <summary>
    /// 作った計算処理のテストを簡易的に行う。
    /// すべてのテストはStartタイミングで処理される。
    /// 通常のAssertを使ったテストにしないのは、
    /// TDDをまわしやすくするため。
    /// リズムのためにエラー以外の情報が必要だった。
    /// 
    /// このコンポネントを使うには
    /// このクラスを継承する形でxxxTestという関数を実装するだけ。
    /// ただし関数はstringを返し、publicであることが必要。
    /// </summary>
    public class TestComponent: MonoBehaviour {

        private int mSize = 0;
        private int mOk = 0;

        protected virtual void Init() { }

        /// <summary>
        /// テスト処理をメソッドの名前からリフレクションで呼び出しています
        /// </summary>
        private void TestProcess(ref StringBuilder sb)
        {
            var methods = this.GetType().GetMethods();
            foreach (var m in methods)
            {
                var name = m.Name;
                if (name.IndexOf("Test") == -1) { continue; }
                sb.Append(Print(name, (string)m.Invoke(this, null)));
            }
        }

        /// <summary>
        /// テスト結果を連結し、まとめて成形出力しています
        /// </summary>
        private void TestResult(ref StringBuilder sb)
        {
            var t_name = GetType().Name;
            if (t_name.IndexOf("1") > -1)
            {
                t_name = t_name.Substring(0, t_name.Length - 2);
            }

            var format = " Test [ {0} ] end ({1}/{2})\n{3}\n ";
            var str = string.Format(format, t_name, mOk, mSize, sb.ToString());
            var type = mOk - mSize == 0 ? DebugLog.Log : DebugLog.Error;
            type.Print(str);
        }

        /// <summary>
        /// 簡易用なのでまとめて処理を呼び出し、そのまま出力しています
        /// </summary>
        private void Start () {
            Init();
            StringBuilder sb = new StringBuilder();
            TestProcess(ref sb);
            TestResult(ref sb);
        }

        private string Print ( string name, string ret ) {
            ++mSize;
            var based = "{0}に{1}しました。{2}{3}{4}\n";

            if (ret.Length != 0) {
                return string.Format(
                    based, name, "失敗",
                    "(", ret, ")");
            }
            ++mOk;
            return string.Format(
                based, name, "成功",
                "", "", "");
        }

        /// <summary>
        /// 成功時に返す値を提供しています
        /// </summary>
        protected string Pass () {
            return "";
        }

        /// <summary>
        /// 失敗時に返す値を提供しています
        /// </summary>
        protected string Fail () {
            return "Fail";
        }

        protected string Fail(int data)
        {
            return "Fail : " + data;
        }

        protected string Fail(float data)
        {
            return "Fail : " + data;
        }

        protected string Fail ( string str ) {
            return "Fail : " + str;
        }

        protected string Fail ( string str, params object [] obj ) {
            return "Fail : " + string.Format(str, obj);
        }
    }
}

/*
  これをテスト用クラスにコピペするだけです
  public string Test () {
    return Fail();
  }
 */
```

```Logger.cs
namespace BasicExtends {
    using System;
    using System.Collections.Generic;
    using UnityEngine;

    public enum DebugLog { Log, Error }

    public static class Logger {
        public static long mStart = 0;
        public static long mPrev = 0;
        private static Action<string> [] LogFunc
            = new Action<string> [] { Debug.Log, Debug.LogError };

        private static string LogString ( string str ) {
            int look = 3;
            System.Diagnostics.StackFrame stack = null;
            while (stack == null) { stack = new System.Diagnostics.StackFrame(look--); }
            if (mStart == 0) { mStart = DateTime.Now.Ticks; }
            var now = (DateTime.Now.Ticks - mStart) / 1000;
            var dt = now - mPrev;
            mPrev = now;
            var dic = new Dictionary<string, string> {
            { "msg", str },
            { "passed", dt +"/" + now},
            { "stack", stack.GetMethod() != null ? stack.GetMethod().Name : "(property?)" }
        };
            return dic.ToJson();
        }

        public static void Print ( this DebugLog type, string str ) {
            var log_str = LogString(str);
            LogFunc[(int)type](log_str);
        }

        public static void Print ( this DebugLog type, string str, params object [] args ) {
            Print(type, string.Format(str, args));
        }
    }
}

```


# 利用サンプル

・テストをしたいコード

```Singleton.cs
public class Singleton<T> where T : class {
    private static T mInstance = null;
    public static T Instance
    {
        get
        {
            if (mInstance == null) {
                mInstance = (T) Activator.CreateInstance(typeof(T), true);
            }
            return mInstance;
        }
    }
}
```

・テストをするコード

```SingletonTest.cs 
public class SingletonTest : TestComponent {
    public class TestClass  : Singleton<TestClass> {
        public int i = 0;
        private TestClass () {
        }
    }

    public class TestClass2: Singleton<TestClass> {
        public TestClass2 () {
            Instance.i++;
        }
    }

    public string ActivateTest () {
        var t = TestClass.Instance;
        if (t.GetType().Name != "TestClass") { return "インスタンス失敗"; }
        return "";

    }

    public string ActivateTest2 () {
        var t = TestClass2.Instance;
        if (t.GetType().Name != "TestClass") { return "インスタンス失敗";  }
        return "";
    }
}
```