前書き
アドカレ4日目です。
UnityTestRunnerについてちょっと書いてみます。
テスト、いいですよね。たくさんテストを書くと仕事が進んでいるような気がしてきます。
僕はそんなにテスト書かない人間ですけど、テストを書くのが嫌いでは無いです。
最近、UnityTestRunnerとその裏側であるNUnitに触れていたら、TestAttribute
の仕組みが気になって、自作のAttributeとかを噛ませてみたくなったので、試してみました。
シンプルな自作TestAttribute
まずはじめに [SimpleTest] public void テストメソッド() { }
こんな感じでTestRunnerに認識されるAttributeを自作してみます。
実装と使用方法
TestAttribute定義
usingSystem;usingNUnit.Framework;usingNUnit.Framework.Interfaces;usingNUnit.Framework.Internal;[AttributeUsage(AttributeTargets.Method,AllowMultiple=true)]publicclassSimpleTestAttribute:NUnitAttribute,ISimpleTestBuilder,IImplyFixture{publicTestMethodBuildFrom(IMethodInfomethod,Testsuite)=>newTestMethod(method,suite);}
使用方法
usingNUnit.Framework;publicclassSimpleTestSample{[SimpleTest]publicvoidTest(){}[SimpleTest]publicvoidTestFail()=>Assert.Fail();}
上記のテストケースであれば、UnityTestRunner上では次のスクリーンショットのように表示されます。
仕組み
UnityTestRunnerに認識させるためにやったことはAttributeの定義(とテストメソッドへの付与)だけです。
継承元について
NUnit.Framework.NUnitAttribute
を継承していますが、これは System.Attribute
を直接継承してもUnityTestRunner上では動作に違いはありませんでしたが、その場の雰囲気でNUnitAttributeを採用しました。
他にもNUnit標準のAttributeはいくつかあるのでNUnit内のAttributeを眺めるのもいいかもしれません。
例えば、UnityTestAttribute
なんかはCombiningStrategyAttribute
を継承していたりします。
ISimpleTestBuilderについて
NUnit.Framework.Interfaces.ISimpleTestBuilder
は次のように定義されています。
publicinterfaceISimpleTestBuilder{TestMethodBuildFrom(IMethodInfomethod,Testsuite);}
今回はTestMethodのコンストラクタ TestMethod(IMethodInfo method, Test suite)
にそのまま流し込んで完了としました。
もう少しイロイロしてくれる便利クラスとして NUnit.Framework.Internal.Builders.NUnitTestCaseBuilder
というものも存在していて、標準の TestAttribute
などはこのビルダーを使っているようです。
IImplyFixtureについて
NUnit.Framework.Interfaces.IImplyFixture
自体は何のメソッドも持たないマーカーインターフェースですが、最低限これだけ付いていればテストとして認識させることができます。ただし前述の ISimpleTestBuilder.BuildFrom
のようなTestMethodを提供するインターフェースが存在しないと、何のテストも実行されません。
シーンを指定してTestを実行する属性の実装(その1)
テストメソッドを実行する直前に任意の処理を挟み込むAttributeを実装してみます。
今回の例ではテスト実行前に EditorSceneManager.OpenScene("hoge.unity");
を叩いてみます。
実装と使用方法
TestAttribute定義
usingSystem;usingSystem.Reflection;usingNUnit.Framework;usingNUnit.Framework.Interfaces;usingNUnit.Framework.Internal;usingUnityEditor.SceneManagement;[AttributeUsage(AttributeTargets.Method,AllowMultiple=true)]publicclassUnityTestSceneAttribute:NUnitAttribute,ISimpleTestBuilder,IImplyFixture{publicreadonlystringscenePath;publicUnityTestSceneAttribute(stringscenePath)=>this.scenePath=scenePath;publicTestMethodBuildFrom(IMethodInfomethod,Testsuite)=>newTestMethod(newMethodProxy(method,()=>EditorSceneManager.OpenScene(scenePath)),suite);}// NUnit.Framework.Internal.MethodWrapperをさらに包むものclassMethodProxy:IMethodInfo{privatereadonlyIMethodInfo_methodInfo;privatereadonlyAction_beforeTest;publicMethodProxy(IMethodInfomethodInfo,ActionbeforeTest){_methodInfo=methodInfo;_beforeTest=beforeTest;}publicobjectInvoke(objectfixture,paramsobject[]args){_beforeTest.Invoke();return_methodInfo.Invoke(fixture,args);}publicITypeInfoTypeInfo=>_methodInfo.TypeInfo;publicMethodInfoMethodInfo=>_methodInfo.MethodInfo;publicstringName=>_methodInfo.Name;publicboolIsAbstract=>_methodInfo.IsAbstract;publicboolIsPublic=>_methodInfo.IsPublic;publicboolContainsGenericParameters=>_methodInfo.ContainsGenericParameters;publicboolIsGenericMethod=>_methodInfo.IsGenericMethod;publicboolIsGenericMethodDefinition=>_methodInfo.IsGenericMethodDefinition;publicITypeInfoReturnType=>_methodInfo.ReturnType;publicT[]GetCustomAttributes<T>(boolinherit)whereT:class=>_methodInfo.GetCustomAttributes<T>(inherit);publicboolIsDefined<T>(boolinherit)=>_methodInfo.IsDefined<T>(inherit);publicIParameterInfo[]GetParameters()=>_methodInfo.GetParameters();publicType[]GetGenericArguments()=>_methodInfo.GetGenericArguments();publicIMethodInfoMakeGenericMethod(paramsType[]typeArguments)=>_methodInfo.MakeGenericMethod();}
使用方法
usingNUnit.Framework;usingUnityEngine;publicclassUnityTestSceneTest{[UnityTestScene("Assets/Scenes/CameraAru.unity")]publicvoidAruScene()=>Assert.IsNotNull(Object.FindObjectOfType<Camera>());[UnityTestScene("Assets/Scenes/CameraNai.unity")]publicvoidNaiScene()=>Assert.IsNull(Object.FindObjectOfType<Camera>());}
次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
仕組み
NUnitのIMethodInfoを実装してMethodProxyクラスを定義し、BuildFromに渡ってくるIMethodInfoのプロキシとしてInvokeに割り込んでいます。
なんとなくActionを渡していますが、直接Invokeメソッド内に書いちゃっても問題ないですね。
なお、今回のBuildFromメソッド実装では、色々実装が足りないのでValuesやRange属性と組み合わせることができませんが、そこはご愛嬌ということで。
シーンを指定してTestを実行する属性の実装(その2)
前述した「シーンを指定してTestを実行する属性の実装(その1)」の場合、色々な属性と組み合わせるとボロが出始めました。
そこで、もう少しボロが出にくいUnityTestRunnerライクな実装をしてみます。
使い方は一緒なので割愛します。
実装
usingNUnit.Framework;usingNUnit.Framework.Interfaces;usingNUnit.Framework.Internal;usingUnityEditor.SceneManagement;usingUnityEngine.TestTools;[System.AttributeUsage(System.AttributeTargets.Method,AllowMultiple=true)]publicclassUnityTestSceneAttribute:NUnitAttribute,ISimpleTestBuilder,IImplyFixture,IOuterUnityTestAction{publicreadonlystringscenePath;publicUnityTestSceneAttribute(stringscenePath)=>this.scenePath=scenePath;publicTestMethodBuildFrom(IMethodInfomethod,Testsuite)=>newTestMethod(method,suite);publicSystem.Collections.IEnumeratorBeforeTest(ITesttest){EditorSceneManager.OpenScene(scenePath);// 1frameくらい間を置いた方が安心yieldreturnnull;}publicSystem.Collections.IEnumeratorAfterTest(ITesttest){yieldbreak;}}
仕組み
都合よくUnityTestRunnerのテストの前後に処理を挟み込める IOuterUnityTestAction
というインターフェースが用意されているので、これを自作Attributeに実装してあげるだけです。
テスト属性に密接に関わる処理は UnitySetUp
や UnityTearDown
よりこちらのインターフェースを使った方が簡単なケースもあるかもしれません。
TestCaseSourceのUnityTest版
NUnitには TestCaseSourceAttribute
というものがいて、このAttributeにイテレータを返すメンバー名を渡してあげると1つのテストから複数のテストケースを量産することができます。TestAttribute
に対するUnityTestRunnerの UnityTestAttribute
のように、 TestCaseSourceAttribute
に対して UnityTestCaseAttribute
があるかなーっと思ったのですが、現状見つからなかったので自作してみました。
以下に実装を残します。
実装と使用方法
TestAttribute定義
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Reflection;usingNUnit.Framework;usingNUnit.Framework.Interfaces;usingNUnit.Framework.Internal;usingNUnit.Framework.Internal.Builders;[AttributeUsage(AttributeTargets.Method,AllowMultiple=true)]publicclassUnityTestCaseSourceAttribute:TestCaseSourceAttribute,ITestBuilder{privatereadonlyNUnitTestCaseBuilder_builder=newNUnitTestCaseBuilder();// コンストラクタpublicUnityTestCaseSourceAttribute(stringsourceName):base(sourceName){}publicUnityTestCaseSourceAttribute(TypesourceType,stringsourceName):base(sourceType,sourceName){}publicUnityTestCaseSourceAttribute(TypesourceType):base(sourceType){}publicUnityTestCaseSourceAttribute(TypesourceType,stringsourceName,object[]methodParams):base(sourceType,sourceName,methodParams){}// ITestBuilderpublicnewIEnumerable<TestMethod>BuildFrom(IMethodInfomethod,Testsuite){varcases=(IEnumerable<ITestCaseData>)typeof(TestCaseSourceAttribute).InvokeMember("GetTestCasesFor",BindingFlags.Instance|BindingFlags.NonPublic|BindingFlags.InvokeMethod,null,this,newobject[]{method});returncases.OfType<TestCaseParameters>().Select(p=>BuildFromImpl(method,suite,p));}privateTestMethodBuildFromImpl(IMethodInfomethod,Testsuite,TestCaseParameterscaseParam){caseParam.ExpectedResult=newobject();caseParam.HasExpectedResult=true;vart=_builder.BuildTestMethod(method,suite,caseParam);if(t.parms!=null)t.parms.HasExpectedResult=false;returnt;}}
使用法
usingSystem.Collections;usingNUnit.Framework;usingUnityEngine.Networking;publicclassSimpleTestEx{publicstaticobject[][]urls={newobject[]{"https://google.co.jp",200},newobject[]{"https://yahoo.co.jp",200},};[UnityTestCaseSource("urls")]publicIEnumeratorUnityTestExのテスト(stringurl,intresponseCode){using(varreq=UnityWebRequest.Get(url)){varope=req.SendWebRequest();while(ope.isDone==false)yieldreturnnull;Assert.AreEqual(responseCode,req.responseCode);}}}
次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
仕組み
TestCaseSourceAttribute
を継承して、ITestBuilder
を実装し直してみました。TestCaseSourceAttribute
にはvirtualなメソッドなんて存在しないので、BuildFromメソッドをnewして隠蔽し、GetTestCasesFor
メソッドをリフレクションでこじ開けるという、気合いと根性に満ちた実装になっています。TestCaseSourceAttribute
を写経するのも選択肢としてはありですが、少々複雑だったので横着してみました。
ぱっと見はそこそこ素直な実装に見えるんじゃないでしょうか。
アスキーアートだって
結構なんでもできるので、
[AATest]publicvoidAA表示したい(){}
こんな感じでアスキーアートを出すことだって自由です。
それではみなさん楽しいテストライフを!