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

UnityTestRunner向けTest属性を自作してみる

$
0
0

前書き

アドカレ4日目です。
UnityTestRunnerについてちょっと書いてみます。

テスト、いいですよね。たくさんテストを書くと仕事が進んでいるような気がしてきます。
僕はそんなにテスト書かない人間ですけど、テストを書くのが嫌いでは無いです。
最近、UnityTestRunnerとその裏側であるNUnitに触れていたら、TestAttributeの仕組みが気になって、自作のAttributeとかを噛ませてみたくなったので、試してみました。

シンプルな自作TestAttribute

まずはじめに [SimpleTest] public void テストメソッド() { }こんな感じでTestRunnerに認識されるAttributeを自作してみます。

実装と使用方法

TestAttribute定義

SimpleTestAttribute.cs
usingSystem;usingNUnit.Framework;usingNUnit.Framework.Interfaces;usingNUnit.Framework.Internal;[AttributeUsage(AttributeTargets.Method,AllowMultiple=true)]publicclassSimpleTestAttribute:NUnitAttribute,ISimpleTestBuilder,IImplyFixture{publicTestMethodBuildFrom(IMethodInfomethod,Testsuite)=>newTestMethod(method,suite);}

使用方法

SimpleTestSample.cs
usingNUnit.Framework;publicclassSimpleTestSample{[SimpleTest]publicvoidTest(){}[SimpleTest]publicvoidTestFail()=>Assert.Fail();}

上記のテストケースであれば、UnityTestRunner上では次のスクリーンショットのように表示されます。
スクリーンショット 2019-11-25 午後4.49.33.png

仕組み

UnityTestRunnerに認識させるためにやったことはAttributeの定義(とテストメソッドへの付与)だけです。

継承元について

NUnit.Framework.NUnitAttributeを継承していますが、これは System.Attributeを直接継承してもUnityTestRunner上では動作に違いはありませんでしたが、その場の雰囲気でNUnitAttributeを採用しました。
他にもNUnit標準のAttributeはいくつかあるのでNUnit内のAttributeを眺めるのもいいかもしれません。
例えば、UnityTestAttributeなんかはCombiningStrategyAttributeを継承していたりします。

ISimpleTestBuilderについて

NUnit.Framework.Interfaces.ISimpleTestBuilderは次のように定義されています。

ISimpleTestBuilder.cs
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定義

UnityTestSceneAttribute.cs
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で実行実行した結果です。
スクリーンショット 2019-11-25 午後4.56.49.png

仕組み

NUnitのIMethodInfoを実装してMethodProxyクラスを定義し、BuildFromに渡ってくるIMethodInfoのプロキシとしてInvokeに割り込んでいます。
なんとなくActionを渡していますが、直接Invokeメソッド内に書いちゃっても問題ないですね。

なお、今回のBuildFromメソッド実装では、色々実装が足りないのでValuesやRange属性と組み合わせることができませんが、そこはご愛嬌ということで。


シーンを指定してTestを実行する属性の実装(その2)

前述した「シーンを指定してTestを実行する属性の実装(その1)」の場合、色々な属性と組み合わせるとボロが出始めました。
そこで、もう少しボロが出にくいUnityTestRunnerライクな実装をしてみます。
使い方は一緒なので割愛します。

実装

UnityTestSceneAttribute.cs
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に実装してあげるだけです。
テスト属性に密接に関わる処理は UnitySetUpUnityTearDownよりこちらのインターフェースを使った方が簡単なケースもあるかもしれません。


TestCaseSourceのUnityTest版

NUnitには TestCaseSourceAttributeというものがいて、このAttributeにイテレータを返すメンバー名を渡してあげると1つのテストから複数のテストケースを量産することができます。
TestAttributeに対するUnityTestRunnerの UnityTestAttributeのように、 TestCaseSourceAttributeに対して UnityTestCaseAttributeがあるかなーっと思ったのですが、現状見つからなかったので自作してみました。
以下に実装を残します。

実装と使用方法

TestAttribute定義

UnityTestCaseSourceAttribute.cs
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;}}

使用法

SimpleTestEx.cs
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で実行実行した結果です。
UnityTestCaseSourceAttribute

仕組み

TestCaseSourceAttributeを継承して、ITestBuilderを実装し直してみました。
TestCaseSourceAttributeにはvirtualなメソッドなんて存在しないので、BuildFromメソッドをnewして隠蔽し、GetTestCasesForメソッドをリフレクションでこじ開けるという、気合いと根性に満ちた実装になっています。
TestCaseSourceAttributeを写経するのも選択肢としてはありですが、少々複雑だったので横着してみました。
ぱっと見はそこそこ素直な実装に見えるんじゃないでしょうか。


アスキーアートだって

結構なんでもできるので、

[AATest]publicvoidAA表示したい(){}

たったこれだけのテストから
スクリーンショット 2019-12-02 午後6.32.54.png

こんな感じでアスキーアートを出すことだって自由です。

それではみなさん楽しいテストライフを!


Viewing all articles
Browse latest Browse all 8905

Trending Articles