やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

Unity Automated QA攻略ガイド

UnityのUIテスト補助ライブラリであるAutomated QAパッケージの解説本を、 コミックマーケット99の2日目(金曜日)東ト38b*1いか小屋」で頒布します。

f:id:nowsprinting:20211216041250j:plain:w400

Automated QAパッケージは、今年4月にアルファ版としてv0.2.0がリリースされた後もバージョンアップが重ねられている実験的(Experimental)パッケージです。 本書は最新のv0.8.1での各機能の紹介からTips、実験的パッケージならではのトラブルシューティングまで盛り込みました。

B5判40ページで頒布価格は1,000円。 J-Coin PayおよびKyashでのお支払いに対応します。 C99は物理本のみで、電子版は来年1/22開催の技術書典12での頒布を予定しています。

BOOTHにて物理本の通販を開始しました[1/1追記]。電子版は1/22に追加予定です[1/18追記] ikagoya.booth.pm

技術書典マーケットはこちらです。1/22販売開始予定です[1/18追記] techbookfest.org

目次の紹介

各章の内容は次のとおりです。

第1章 Automated QA概要

Automated QAパッケージの概要、ユースケース(用途ごとに第2章以降で紹介している機能をどう使うか)、セットアップ方法を解説しています。

第2章 Recorded Playback

Automated QAパッケージの主要機能である、Unityエディター上でUI操作を記録し、再生する機能を解説しています。 基本機能のほか、ランダムな操作を行なうGame Crawler機能、HTMLレポート機能についても解説しています*2

第3章 Test Generation

Recorded Playback機能で記録した操作から、Unity Test Frameworkのテストコードを生成する機能を解説しています。 テストコードの生成方法は3パターン提供されており、それぞれの特徴を紹介しています。 また、生成されたテストによる検証のメカニズムと、より厳密な検証を行なうためのアサーションの書きかたを解説しています。

第4章 Test Driver API

Recorded Playback機能によらず、直接テストコードやランタイムからUI操作を行なうためのAPIを紹介しています。 ただし、現時点では制約も多く、またテストコードのメンテナンスを考えるとお勧めしづらい機能のため、ごく簡単に紹介するにとどめています。

第5章 Automators

複数の操作記録の再生を連続実行できる機能を解説しています。 また、記録の再生のほか、ランダムな操作を行なうGame CrawlerやSceneのロードを行なうAutomatorの紹介、カスタムAutomatorを自作する方法も紹介しています。

第6章 Unity Test Framework Tips

Test Generation機能で生成されたテストコードはUnity Test Framework上で実行されます。 拙著『Unity Test Framework完全攻略ガイド』(下参照)から、直接関連するTipsを一部抜粋して紹介しています。

www.nowsprinting.com

付録A トラブルシュート

Automated QAパッケージは実験的パッケージです。フォーラムで不具合報告などしてきましたが、機能開発を優先されているようで修正されていない問題も残っています。 Automated QAパッケージを使うにあたって遭遇するであろうエラーについて、その原因と回避方法を紹介しています。

付録B UPMパッケージにパッチをあてる

Automated QAはUPM(Unity Package Manager)パッケージとして提供されています*3。 これに対し、手元の開発環境で独自のパッチを当てて運用する方法について紹介しています。

付録C Test Runnerウィンドウの使いかた

UnityエディターのTest Runnerウィンドウの使いかたを紹介しています。 本章は『Unity Test Framework完全攻略ガイド』からの抜粋です。v1.1.0で追加しました。

最後に

C99はチケット抽選制で、すでに二次販売も終了しています。 参加したかったがチケット確保できなかったという方もいらっしゃるでしょう(私も1日目のチケット取れませんでした)。 物理本の売れ残りはBOOTHで頒布予定ですので、ショップをフォローしてお待ちいただけますと幸いです。

ikagoya.booth.pm

*1:日本Androidの会Unity部さんのお隣です。UNIBOOK13のついでにお立ち寄りください

*2:Input Systemサポート機能については検証しきれておらず、紹介のみ

*3:しかも、2022年後半まで開発保留と発表されたので、その間バグフィックスされません

メソッド分割の意義とTips

この記事は DeNA Advent Calendar 2021 16日目の記事です。

ユニットテストを書こうとしたとき、テスト対象のクラスやメソッドが大きく複雑なため断念した経験は誰しもあるのではないでしょうか。

ひとつのメソッドや関数に様々な処理・責務を持たせてしまうとテスタビリティ*1だけでなく、保守性や可読性も損なわれてしまいます。 これは「ロボット掃除機が掃除するためには部屋が片付いていなければならない」こと*2と似ています。

本記事では、メソッドを適切な単位に分割することの意義と、そのためのTipsを紹介します。

メソッド分割のPros and Cons

はじめに、メソッド分割の代表的なPros/ Consを整理してみます。

Pros

  • 再利用性の向上
    • 同一もしくは類似するロジックや計算式が複数箇所で必要になるケースは珍しくありません。メソッドを細かいロジック単位に分割することで、再利用が進むことはイメージしやすいはずです
    • 逆に言うと、再利用が少なく複数のメソッドに同じロジックが重複して存在している状態では、そのロジックに変更が入るときに修正箇所が多くなり修正漏れの恐れも生じます
  • テスタビリティの向上
    • 責務の大きなメソッドは、必然的に入力(引数のほかインスタンスフィールドの参照も含みます)が多くなり、その組み合わせすべてをテストすることが困難になります
  • 可読性の向上
    • 大きなメソッドより小さなメソッドのほうが読みやすい
    • メソッドの名前と責務を明確に定義しやすい

Cons

  • メソッド呼び出し回数が増え、オーバーヘッドがかかる
    • メソッド呼び出しには、処理ステップの増加とスタック操作を伴います。呼び出し回数が増えれば無視できない性能劣化に繋がります

メソッド呼び出しの増加に対しては、コンパイラによる「インライン化(inlining)」と呼ばれる最適化によって緩和できます。 インライン化は必ず行われるとは限らず、またバイナリサイズが大きくなるという副作用*3もありますが、本記事では考えないことにします。

Unityにおけるインライン化については、次の記事で検証していますので参考にしてください。

www.nowsprinting.com

メソッド分割Tips

続いて、メソッドや関数の分割(抽出)をスムーズに行なうためのTipsを紹介します。

Divide and Conquer, Name and Conquer

クラスやメソッドを分割することで、その関心や責務をシンプルに理解しやすく、扱いやすくする手法を、征服・統治になぞらえて「分割統治(Divide and Conquer)法」と呼びます。 チームのコーディング規約などで、メソッドの長さや複雑度の指標(後述)を基準に、人間が理解できる粒度に分割する、という考え方です。

一方で、必要なものにまず名前を付けることで扱いやすくする手法が「Name and Conquer」です。うまく対比した日本語訳が無いのですが、命名(定義)による統治(征服)といった意味です。

たとえば Fizz Buzz の一部を次のように切り出してみます。

private static bool IsFizz(int i)
{
    return i % 3 == 0;
}

これはやや過剰な例ですが、マジックナンバーである 3 を「Fizzとすべき割り切れる数」として定数に定義することは自然に行われているはずです。 上例のようなシンプルなメソッドは、定数と大きな差はありません。 3 を定数とするよりも、判定ロジックを切り出して名前を付けるという選択です。

複雑度を指標にした分割

メソッドの複雑さを測る指標として、サイクロマティック複雑度(cyclomatic complexity)やコグニティブ複雑度(cognitive complexity)が知られています。

例えばJetBrains IDEsには、エディタタブ上にこれらの指標を表示してくれるプラグインが提供されています。 プラグインを導入することで、コーディング中に複雑なメソッドに気づくことができます。

詳しくは次の記事で紹介していますので参考にしてください。

www.nowsprinting.com

IDEリファクタリング機能を利用する

JetBrains IDEsには、リファクタリングの補助機能があります。 別メソッドに分割したいコードを選択した状態でクイックアクションから "Refactor This..." を選択、続いて "Extract Method" で次のダイアログが表示されます。

f:id:nowsprinting:20211215084228p:plain:w400

メソッド名やアクセス修飾子などを指定して実行すると、メソッドを分割できます。

参考

C#のインライン化についての参考記事。

ufcpp.net

Name and Conquerについて。ヨシュア・トゥリーの話もおすすめです。

objectclub.jp

分割したメソッドに命名するのが面倒に感じることは多々あります。またPull Request (Merge Request) レビューで命名についての指摘を遠慮したこともあるのではないでしょうか。 その点、ペアプロやモブプロで、その場であれこれ名前の候補を出し合えるのは良い体験だと思っています。 モブプロについては次の記事で紹介していますので参考にしてください。

www.nowsprinting.com

PR 1

C99(冬コミ)でUnity Automated QAパッケージの解説本を頒布します。 2日目(金曜日)東ト-38b*4でお待ちしております!

f:id:nowsprinting:20211216041250j:plain:w300

PR 2

この記事は DeNA Advent Calendar 2021 16日目の記事です。

DeNAでは今年、2021年度新卒エンジニア・2022年度新卒内定エンジニアの Advent Calendar もあります! 本 Advent Calendar とは違った種類、違った視点での記事をぜひお楽しみください!

DeNA 2021年度新卒エンジニア・2022年度新卒内定エンジニアによる Advent Calendar 2021 https://qiita.com/advent-calendar/2021/dena-21x22

また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく様々な登壇の資料や動画も発信してます。ぜひフォローして下さい!

*1:ISTQB GLOSSARY Ver3.2(日本語版)では「試験性」と称しています

*2:「ルンバビリティ」と呼ばれます

*3:これはそもそもメソッド分割前の状態に戻ることなので本記事では無視していいでしょう

*4:日本Androidの会Unity部さんのお隣です。UNIBOOKお買い求めのついでにお立ち寄りください!

Unityにおけるインライン化

テスタビリティ*1など内部品質の文脈で、クラスやメソッドの責務を分けるべし、メソッド分割すべし、という話をよくします。

メソッド分割には、メソッド呼び出しが増えることによる実行時オーバーヘッド増加という問題があります。 これに対しては、

という方針できましたが、Unityにおいて根拠が乏しかったため、簡単に検証してみました。

インライン化

メソッド分割に起因するオーバーヘッドの増加は、コンパイラによる「インライン化(inlining)」または「インライン展開(inline expansion)」と呼ばれる最適化によって緩和できます。

インライン化とは、文字通り呼び出し先のメソッドを呼び出し元メソッド内に直接展開する最適化手法です。 インライン化にはメソッドのバイト数などの条件があるため必ず行われるとは限らず、またロジックのコピーが作られるためバイナリサイズが大きくなるデメリットがあります。

C#で書かれたコードは中間言語(IL)に翻訳されます。 .NETの場合はILを.NETランタイムで実行するときにJITコンパイラにより最適化されますが、UnityではILがIL2CPPによってC++コードに変換された後、AOTのC++コンパイラによってネイティブコードとなる際に最適化が行われます。

インライン化について、Unityマニュアルの 特殊な最適化 ページには次のように書かれています。

ここで主な問題は、 Unity がメソッドのインライン化をほとんど行わないことです。 IL2CPP 下であっても、多くのメソッドは現在、適切なインライン化を行いません。これは特にプロパティに関して言えることです。さらに、 virtual および interface メソッドのインライン化は一切行えません。

「ほとんど」とありますが、テスタビリティや可読性向上を目的としたメソッド分割の範囲で最適化されるのかを確認してみます。

検証

検証に使用した環境は

  • Unity 2020.3.21f1
  • プラットフォーム: macOS/x64

です。

次の2通りのコードを実行してみました。

public int Calculate(int count)
{
    var sum = 0;
    for (var i = 0; i < count; i++)
    {
        sum += i;
    }

    return sum;
}
private static int DividedMethod(int i)
{
    return i;
}

public int Calculate(int count)
{
    var sum = 0;
    for (var i = 0; i < count; i++)
    {
        sum += DividedMethod(i);
    }

    return sum;
}

count に100,000,000を与えたときの実行時間は次の通りです。なお、IL2CPPだけは精度が出なかったため少し呼び出し負荷を増しています。

Unityエディタ Mono IL2CPP
メソッド分割なし (No Divide) 約 270 [ms] 約 49 [ms] 約 3.5 [ms]
メソッド分割あり (Divided) 約 660 [ms] 約 65 [ms] 約 23 [ms]

メソッド分割有無であきらかな性能差が出ています。

続いて、別のアセンブリに定義されたメソッドを呼ぶケースも検証するため*3、3パターン追加します。

  • アセンブリにあるメソッドを呼ぶ (Another Assembly)
  • アセンブリにあるメソッドに MethodImpl(MethodImplOptions.AggressiveInlining) 属性を付ける (Aggressive Inlining)
  • アセンブリにあるメソッドに MethodImpl(MethodImplOptions.NoInlining) 属性を付ける (No Inlining)

AggressiveInlining は、その名の通り積極的にインライン化を行なうように指示するものです。 .NETのJITコンパイラが解釈する属性ですが、UnityのIL2CPPもこれを解釈してインライン化されやすいC++コードを出力してくれるようです。

結果は次のようになりました。

Unityエディタ

f:id:nowsprinting:20211215074846j:plain:w400

Mono

f:id:nowsprinting:20211215050856p:plain:w400

IL2CPP

f:id:nowsprinting:20211216062846p:plain:w400

No Inliningのケースではアセンブリを超えてメソッドコールが行われるため、IL2CPPでも時間がかかっています。

Another Assemblyのケースは AggressiveInlining を指定していないのですが、指定したときと同様に呼び出し元のcppファイルにメソッドのコピーが作られており、コンパイル時にインライン化(もしくは類する最適化)されているようです。

Dividedのケースでは同一cppファイルにメソッドはあるものの、最適化はされていないか、限定的であることがうかがえます。

まとめ

  • 実行性能が気になる場合はプロパティでなくメソッドを使用する
  • インライン化してほしいメソッドには MethodImpl(MethodImplOptions.AggressiveInlining) 属性を付ける

検証コードは下記リポジトリに置いてあります。

github.com

参考

ufcpp.net

notnullvariable.com

lpha-z.hatenablog.com

learning.unity3d.jp

*1:ISTQB GLOSSARY Ver3.2(日本語版)では「試験性」と称しています

*2:ボトルネックに当たったときに対処するという方針。「早すぎる最適化は諸悪の根源(Premature optimization is the root of all evil)」という言葉は有名です

*3:IL2CPPにおいては、アセンブリ単位にcppファイルが生成されます。通常、別のcppファイルに定義されたメソッドはインライン化されません

カスタムRoslyn AnalyzerをUPMパッケージとして配布する

Unity 2020.2でRoslynアナライザによる静的解析を行えるようになりましたが*1、NuPkgやDLLで配布してUnityプロジェクトに導入するのはやや面倒です。

そこで、アナライザをUnity Package Manager (UPM) パッケージとして配布する方法を紹介します。アナライザ単体での配布はもちろん、自作ライブラリに関連するアナライザをライブラリのUPMに同梱して配布できます。

前提は、Unity 2020.3.4以降です。UnityバージョンごとのRoslynアナライザまわりの機能制限については次の記事にまとめています(Unity 2021.2の差分まで反映済)。

www.nowsprinting.com

アナライザ入りUPMパッケージの作成

アナライザをNuGetから取得もしくはビルドしたDLLは、一旦Assetsフォルダ下に置いて*2 Inspectorウィンドウを開き、次のようにmeta情報を設定します。

  • Select platform for pluginをすべてoff
  • Asset Labelsに RoslynAnalyzer を設定

f:id:nowsprinting:20211101080608p:plain:w300

"Apply"をクリックして適用したら、アナライザDLLを配布するUPMパッケージ下に移動します。 このとき必ず、Assembly Definition File (.asmdef) の影響下に配置してください*3

例えば次のようなディレクトリ構成にします。 ライブラリに同梱するときは、パスの途中に他の.asmdefを置いてしまうと別アセンブリ扱いになりますので注意してください。

Packages/your.library/
├── Editor
│   └── YourLibrary.Editor.asmdef
├── Runtime
│   ├── Analyzers
│   │   ├── your.library.analyzers.dll
│   │   └── your.library.analyzers.dll.meta
│   ├── YourLibrary.Runtime.asmdef
│   └── YourLibraryCode.cs
├── Tests
│   └── YourLibrary.Tests.asmdef
├── package.json
└── package.json.meta

これで、このUPMパッケージを使用するプロジェクトでは、アナライザを含んだアセンブリ(上例では YourLibrary.Runtime )を"Assembly Definition References"に追加することで(そのアセンブリ内でのみ)アナライザが有効になります。

※ただし、Unity Editor上では。

IDEでの振る舞い[1/4追記]

Unityに対応しているIDE(JetBrains Rider, Visual Studio, Visual Studio Code)には、Unityエディターのメニューから "Open C# Project" を実行したときに.slnおよび.csprojファイルを生成してくれるプラグインパッケージが提供されています。

プラグインパッケージが生成する.csprojファイルが上記Unityエディターと同じ振る舞いをするのは、次のバージョン以降です。

  • Unity 2020.3.6以降、もしくは Unity 2021.1.2以降

かつ

IDEプラグインパッケージ向けのAnalyzer Importerスクリプト

本記事執筆時点で、各IDE(JetBrains Rider, Visual Studio Code, Visual Studio)のプラグイン*4はRoslynアナライザに正しく対応していません。

例えばJetBrains Rider Editor package v3.0.7では、Assets下および、Packages直下に実体のある組み込みパッケージ (Embedded package) のみが、.asmdefの依存関係に関係なく適用されます。

IDE側の対応がなされるまでは 上記バージョンの組み合わせを満たせないプロジェクトでは、次のスクリプトをUPMパッケージに同梱して配布するとよいでしょう。 パス設定など不要で、そのままアナライザと同じ.asmdef下に放り込めば動くようになっています。

gist.github.com

ただし、以下の制限事項があります。

  • .asmdefのファイル名(拡張子は除く)と、アセンブリ名(Inspectorウィンドウの"Name")を一致させてください
  • .asmdefの参照の連鎖には対応していません。アセンブリA(ここにアナライザ) ← アセンブリB ← アセンブリC と参照した場合、アナライザが有効になるのはアセンブリBまでです

UPMパッケージに組み込んだもの(テスト付き)はこちら。デフォルトブランチでないので注意。

github.com

また、NUnit.Analyzersを同様にUPMパッケージ化したものを作ってみました。 ただし、Unity Editor上でテストアセンブリに対してアナライザが動作するのはUnity 2021.2以降です。

github.com

アナライザの有効範囲についての補足

アナライザをインポートしたプロジェクト側では、Unity Editorでは.rulesetファイル、Riderでは.editorconfigファイルによって診断の重大度 (Severty) を制御できます。 従って、Severtyをnoneにすることで実質的にアナライザを無効化することはできます。

しかし、.rulesetは影響範囲をアセンブリつまり.asmdef単位で指定する必要があり、アナライザおよびアセンブリが多数あるプロジェクトでは管理が難しくなってきます。

.editorconfigではパスで影響範囲を指定できるため.rulesetよりは楽ですが、やはり.asmdefの依存関係による有効範囲制御が好ましいと思います。

UPMパッケージの対応Unityバージョンについて[11/1追記]

UPMパッケージのマニフェストファイル (package.json) には、パッケージが動作する最低Unityバージョンを指定できます。

アナライザのみを配布するパッケージであれば、次のようにUnity 2020.3.6f1*5と各IDEsプラグインパッケージのバージョンを指定することで「インポートしたけれど動作しない」といった混乱を回避できます。 unityRelease 属性はUnity 2020.3で追加されたものですが、それ以前のバージョンは unity 属性で除外できるので問題ないでしょう。

{
  "name": "your.analyzer.name",
  "version" "1.0",
  "unity": "2020.3",
  "unityRelease": "6f1",
  "dependencies": {
    "com.unity.ide.rider": "3.0.9",
    "com.unity.ide.visualstudio": "2.0.11",
    "com.unity.ide.vscode": "1.2.4"
  }
}

ただ、「Unity Editorでは動作しなくていいがIDEで使いたい」というニーズに反してしまうのが悩みどころです。

また、ライブラリにアナライザを同梱する場合には、特に最低Unityバージョンを変更する必要はないでしょう。 Unity 2020.3.3以前であっても、アナライザが動作しないだけで悪影響はありません。

参考

docs.unity3d.com

docs.unity3d.com

qiita.com

github.com

github.com

*1:Roslynアナライザの作成・導入方法はこちらの記事にまとまっています https://swet.dena.com/entry/2021/05/25/100000

*2:Packages下に置くとInspectorウィンドウでAsset Labelsが設定できません

*3:もし、UPMパッケージをインポートしたプロジェクトの全コードに対して無条件に適用してよいアナライザであるなら、アナライザを.asmdef下に置かないで配布することもできます

*4:Unityのメニューから"Open C# Project"を実行したとき、.slnおよび.csprojファイルを生成する役割

*5:ただしこの設定では2021.1.0f1〜2021.1.2f1未満が漏れるため、安全に倒したければ2021.1.2f1以降としてください

Unity Test Framework完全攻略ガイド

Unityの標準テストフレームワークであるUnity Test Frameworkパッケージ*1の解説本を技術書典11向けに上梓しました。 ギリギリ審査が通って閉幕までに間に合いました(間に合ってない

techbookfest.org

BOOTHにも置いています。

ikagoya.booth.pm

内容は、Edit Mode/ Play Modeテストの特性、非同期テスト、アサーション(NUnit3の制約モデル)、カスタムアサーション、テストダブル、パラメタライズドテスト、Sceneやアセットのテスト、その他Tips。 と、ユニットテストとその周辺だけに絞った*2のですが135ページもあるこわい。

対象バージョンは Unity Test Frameworkパッケージ v1.1.27、Unity 2020.3.12f1、JetBrains Rider 2021.1です。 とはいえ、中心はUnity Test Frameworkパッケージですので、本書で紹介するほとんどの機能はUnity 2018・2019・2021でも動作するはずです。

また、Riderについてはテスト実行方法やTipsの紹介程度ですので必須ではなく、Visual StudioVisual Studio CodeVS Code)、もしくはVimなど、使い慣れたIDEをお使いいただけば大丈夫です。

ソフトウェアテストに関する用語は、原則としてJSTQB技術委員会による『ISTQBテスト技術者資格制度 Foundation Level シラバス 日本語版 Version 2018V3.1.J03』および『ISTQB GLOSSARY Ver3.2(日本語版)』から解説を引用しつつ使用しています。 またテストダブルなどユニットテストの技法に関しては『xUnit Test Patterns』(xUTP)に準じています。

サンプルコードは下記リポジトリで公開しています。NUnit3の制約全網羅したりと、書籍よりむしろこっちが本体かもしれない。

github.com

目次の紹介

各章の内容は次のとおりです。

第1章 テストとは何か

「テスト」と、ゲーム業界で使われる「デバッグ」という言葉の違いから、テストのターゲットに応じた考えかた、どのようなテストを自動化すべきか等。

第2章 Unity Test Frameworkの基本

Unity Test Frameworkパッケージを使ってユニットテストを書き、実行する手順を、順序立てて説明しています。 フォルダ(アセンブリ)構成や命名の指針も紹介しています。

第3章 Edit Modeテスト

主にロジックのテストに利用できるEdit Modeテストの特徴と、Edit Modeテスト固有の機能について。

第4章 Play Modeテスト

よりUnityプレイヤーに近い環境で動作させられるPlay Modeテストの特徴、Unityエディターおよびプレイヤーでの実行方法について。

第5章 非同期処理のテスト

コルーチンやUniTaskによるasyncメソッドのテストについて。

第6章 アサーション

NUnit3の制約モデル(Assert.That)によるアサーション(テスト実行結果の検証)について、検証対象や目的に応じた最適な書きかたやTips、アンチパターンを紹介。

第7章 テストダブル

テスト対象が何らかのコンポーネントに依存しているとき(内部で生成した疑似乱数やサーバからのレスポンスに応じて結果が変わるなど)、その依存コンポーネントを偽物(スタブ、スパイ、モックなど)に置き換えてテストするテクニックの紹介。

第8章 パラメタライズドテスト

ひとつのメソッドを引数を様々に変えてテストしたいとき、NUnit3のTestCase属性・TestCaseSource属性・Values属性・ValueSource属性を使ってこれを実現する方法。

第9章 Unity Test Framework Tips

Unity Test Frameworkに備わる機能の紹介。 許容誤差付きのアサーション(EqualityComparer)、GCメモリアロケーション検知(AllocatingGCMemory)、ログ出力の検証(LogAssert)、テストの前処理・後処理(SetUp/TearDown属性, OneTimeSetUp/TearDown属性, PrebuildSetup/Cleanup属性, ICallbacks/IErrorCallbacksインタフェース)、アセットのロード、実行プラットフォーム制限(UnityPlatform属性)、カテゴライズ(Category属性)、タイムアウトの指定(Timeout属性)、MonoBehaviourのテストなど。

第10章 Sceneを使用するテスト

Edit Modeテスト・Play Modeテスト(エディター実行)・Play Modeテスト(プレイヤー実行)それぞれで、Sceneファイルをロードしてテストに使用する方法。 "Scenes in Build"に含まれないSceneをロードする方法も紹介しています。

第11章 UPMパッケージのテスト

Unity Package manager(UPM)パッケージのテストを実装・実行する方法。

第12章 テストの実行方法

UnityエディターのTest Runnerウィンドウ、Jet Brains Rider、コマンドラインからのテスト実行方法・オプションの紹介。

第13章 コードカバレッジ

Unity Code Coverageパッケージを用いてコードカバレッジ(テストがプロダクトコードのどの部分をカバーしているかの指標)を採取する方法。 また、RiderのdotCoverプラグインの紹介。

第14章 テスト実装のヒント

壊れやすい「実装のテスト」ではなく、変更に強い「仕様のテスト」を書くためのアプローチを紹介しています。 テストケースを導出するためのテスト技法(同値分割法・境界値分析・デシジョンテーブル・組み合わせテスト)、再現テスト、テスト駆動開発(TDD)。

付録A JetBrains Rider Tips

IDEにRiderを使う場合の、コグニティブ複雑度の計測(CognitiveComplexityプラグイン)、テスト用Live Templateの紹介。

付録B OpenUPM-CLI

本書で紹介しているUPMパッケージをUnityプロジェクトにインポートするのに便利なopenupm-cliコマンドの紹介。

付録C NuGetForUnity

本書で紹介しているNuGetパッケージをUnityプロジェクトにインポートするのに便利なNuGetForUnityの紹介。

執筆を支える技術

以下、執筆に使用したツール類です。感謝しかない。

Re:VIEW

github.com

github.com

書名が長いのでこの記事を参考に大扉の折返しを調整しています。 kyabe.net

textlint

github.com

今回使用したルールは以下。

$ npm install --save-dev textlint \
textlint-rule-preset-ja-spacing \
textlint-rule-preset-ja-technical-writing \
textlint-filter-rule-comments \
textlint-filter-rule-allowlist \
textlint-rule-prh \
textlint-rule-abbr-within-parentheses \
textlint-rule-common-misspellings \
textlint-rule-spellchecker \
textlint-plugin-review

prh

github.com

prh (proofreading helper) は、表記ゆれなど校正を支援してくれるツールです。 ビルトインで十分に実績のあるルールが提供されていますが、今回執筆を進めながら、C#、Unity、JetBrains、ISTQB日本語版、xUTPに関する用語を追加しました。

追加したルールは下記リポジトリで公開しています。本書の範囲だけなので網羅にはほど遠いのですが、今後も育てていければと。

github.com

スクリーンショット用にウィンドウサイズを整えるAppleScript

スクショのサイズが統一されていないのは嫌なので、次のスクリプトでリサイズして撮影しました。

Unity用

Rider用

所感

元々はC98(2020年GW)に出そうと着手したのが、コミケ流れたり忙しかったりで1年半かかってしまいました。 当時は制限事項として書こうとしていたものが今は修正されていたり、Unity Test Frameworkも日々進歩していることをここ数週間の追い込みで実感したりできました。

本の執筆は5年ぶり、同人誌は初でした。 Re:VIEWやtextlint、prhなどツールが整っていて、手軽にそれなりに見られるものがレンダリングできてしまうのは素晴らしいですね。 もちろん、カスタマイズやレイアウトなど凝りたくなるとそこはプロの技が必要なわけですが。

ちなみに、審査に数日と聞いていたので開催期間中に間に合うか不安だったのですけれど、半日くらいで爆速審査していただけました。 技術書典スタッフの方々、本当にありがとうございました。

技術書典11は本日いっぱい開催です。電子書籍は明日以降も購入できますが、よい機会なので色々見てまわってはいかがでしょうか。

techbookfest.org

*1:パッケージ化される前の名称はUnity Test Runner

*2:テスト戦略や統合テスト向けツールやらCIや静的解析などのドロップしたネタは改めて供養したい

PLATEAU 3D都市モデルの水ぜんぶ抜く大作戦

国土交通省 Project PLATEAU で、横浜市の3D都市モデルが公開されました。 このデータを使用する際、水面にあたる部分に面が張られており使いづらかったので*1Blenderで水面をすべて削除して、改めて水面用のメッシュを用意しました。 本記事はこの手順メモです。

3D都市データファイルをBlenderにインポート

今回使用したのは、横浜市(2020年度)のOBJデータです*2。 こちらをダウンロード・解凍して、目当てのエリアのデータをBlenderにインポートします。今回使用したのはBlender 2.93です。

www.geospatial.jp

Project PLATEAUのOBJデータは以下の4つに別れています。それぞれフォルダ内にエリアごとのファイルがありますが、今回は、みなとみらいエリアを含む 533915 のみ使用します。

  • bldg(建築物)
    • LOD1(全域)
    • LOD2(一部のみ)
  • dem(地形)
  • tran(道路)

なお、インポート時はTransformを Y Forward, Z Up に設定します。 また、原点が離れた位置に設定されているので移動しておくと便利です*3

今回加工するdemは次のように水面まですべて面が張られています。

f:id:nowsprinting:20210612010420p:plain

ランドマークタワー近辺を拡大すると、地面には細かく頂点があり、水面は地面のキワから対岸などに長い辺が伸びて面が張られていることがわかります。

f:id:nowsprinting:20210612010547p:plain

水面を削除する

地面と水面のキワにあたる頂点は、実際には測量された地面の端と思われ、このあたりでZ = 2m〜2.2mほどあります。 頂点の高さで水面を判断することは難しいので、「辺の長さが20m以上のものは水面」と判断して削除するPythonスクリプトを書いて処理します。

該当部分だけ抜粋すると次のコードです。

def expose_water_surface(target_pattern=r"^\d+_dem_\d+$", edge_length=20.0):

(snip)

    pattern = re.compile(target_pattern)
    for obj in bpy.context.scene.objects:
        if not pattern.match(obj.name):
            continue

(snip)

        for edge in mesh.edges:
            if edge.calc_length() >= edge_length:
                mesh.edges.remove(edge)

(snip)

これをBlenderのScriptsワークスペースで開き、実行します(詳しくは後述)。

処理後はこうなりました。左上のくぼみはドックヤードガーデンなので、削除されなくて正解です。

f:id:nowsprinting:20210612011159p:plain

この時点で俯瞰して見ると、海上に格子状に頂点が残っています。

f:id:nowsprinting:20210612012408p:plain

これはエリアの切れ目に置かれた頂点ですので、面積0の面および孤立した頂点というルールで削除します。 この処理は、次のブログ記事にあるスクリプトをほぼそのまま使用しました。

bluebirdofoz.hatenablog.com

削除後がこちら。まだ細かいゴミは残っていますが、自動処理はここまでとしました*4

f:id:nowsprinting:20210612012425p:plain

このデータをFBX形式でエクスポートしてUnityにインポート、clusterワールドとして公開したのがこちらです。水色は改めて追加した水面メッシュです*5

f:id:nowsprinting:20210612021746p:plain

cluster.mu

今回使用したスクリプトはこちらに公開しています。 Blender初心者の書いたコードなので無駄なステップがあるかもしれませんし事故を起こす恐れもあります。ご利用は自己責任で、バックアップを必ず取ってから使ってください。

github.com

Blender上でPythonスクリプトを実行する方法

以下は補足です。

Blender上でPythonスクリプトを実行するには、まずScriptingワークスペースを選択し、中央のエディタ上部にあるOpen Textアイコンで実行するPythonスクリプトを開きます。 次いで、Run Scriptアイコンで実行します。

f:id:nowsprinting:20210612014203p:plain

macOSの場合はGUI上にログが出ないため*6、実行ログを参照するためにはコマンドプロンプトからBlenderを実行する必要があります。

PyCharmでBlender向けスクリプトを書く

新規プロジェクト作成の際、Base interpreterにはBlenderが内包しているPythonを指定します。 Homebrewでインストールした場合、次のパスにあるはずです。

/Applications/Blender.app/Contents/Resources/2.90/python/bin/python3.7m

続いて、BlenderAPIであるbpyパッケージのFakeをインストールします。Fakeなので実行はできませんが、コード補完が効くようになります。

Preferences... | Python Interpreter を開き、 fake-bpy-module-x.xx を追加します。末尾はBlenderバージョンと合わせてください。

github.com

リダクションとFBXエクスポート設定

前述のように地表には約5m間隔で頂点が配置されており、特にみなとみらい地区のような平地では贅沢です。 今回は、demにのみDecimateモディファイアを追加、Ratioは0.01に設定して、△約3,000,000から30,000に削減しました*7

モディファイアを含めたFBXエクスポート設定は以下のようにしています。

  • Object Types: Meshのみ選択
  • Transform
    • Apply Scalings: FBX All
    • -Z Forward, Y Up(FBXのデフォルト)
  • Geometry
    • Apply Modifiers: on
  • Armature
  • Bake Animation: off

ファイルサイズは、モディファイアなしで約110MBだったところ、42.3MBになりました。

参考

bluebirdofoz.hatenablog.com

zenn.dev

*1:横浜市の3D都市モデルを基準にしています。他の都市にはあてはまらない可能性がありますのでご注意ください

*2:先に公開されていた東京都23区はFBX形式のデータが提供されていましたが、横浜市ではCityGMLとOBJのみ提供されています

*3:エリアの中央もしくはVRChat/clusterなどのワールドで使用するのであればスポーン地点など

*4:本来はここから温かみのある手作業で消していきますが、今回は手作業なしでフィニッシュしました

*5:正しく処理されたはずのドックヤードガーデンに水面が張られているのは、一律平面の水面メッシュを追加したためです。そのうち対処しなければ

*6:調べた範囲では

*7:元町方面を見るとだいぶカクカクしているのがわかります

Unity Automated QAのRecorded Testing機能 トラブルシュート

Unity Automated QA パッケージのRecorded Testing機能を試していて遭遇したトラブルと解消方法をまとめます。 バージョンは 0.2.0-preview.3 および 0.3.0-preview.8*1 で確認しています。

Recorded Testing機能とは、UnityエディタのPlay modeでuGUIの操作を記録したjsonファイルをPlay modeテストとして実行できる機能です。

Unity Automated QAパッケージ全体については先のエントリ参照。

www.nowsprinting.com

[12/26追記]v0.8.1対応の『Unity Automated QA攻略ガイド』をコミックマーケット99で頒布します。詳しくはこちらの記事を参照してください

www.nowsprinting.com

FileNotFoundException: Could not find config.json

テスト実行時、下記スタックトレースが出て失敗するケース。

FileNotFoundException: Could not find file "/Users/***/Library/Application Support/DefaultCompany/UnityTestFramework/config.json"
System.IO.FileStream..ctor (System.String path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, System.Int32 bufferSize, System.Boolean anonymous, System.IO.FileOptions options) (at <fb001e01371b4adca20013e0ac763896>:0)
(snip)
System.IO.File.ReadAllText (System.String path) (at <fb001e01371b4adca20013e0ac763896>:0)
Unity.RecordedTesting.Manager.GetDeviceFarmConfig (Unity.RecordedTesting.DeviceFarmOverrides dfOverrides) (at Library/PackageCache/com.unity.automated-testing@0.2.0-preview.3/RecordedTesting/Runtime/Manager.cs:31)
Unity.RecordedTesting.TestResults.RunStarted (NUnit.Framework.Interfaces.ITest testsToRun) (at Library/PackageCache/com.unity.automated-testing@0.2.0-preview.3/RecordedTesting/Runtime/TestTools/TestResults.cs:33)

これは、ローカル環境でテスト実行しようとしているのに、AWS Device Farmで実行するルートに入っているため発生しています。

Edit | Project Settings... を開き、Playerタブ、Other Settings下にあるScripting Define Symbolsの中に CLOUD_TEST_UTF シンボルが定義されていれば、それを削除すれば解消します。

Cloud Test Runnerウィンドウを開いて操作しようとした際にシンボルが設定されたままになっていたようです。

SetUp : System.Reflection.ReflectionTypeLoadException

テストのSetupですべてのアセンブリから型情報を取り出している処理があるのですが、型を含まないアセンブリでこの例外が出ています。

CanPlayToEnd (3.571s)
---
SetUp : System.Reflection.ReflectionTypeLoadException : Exception of type 'System.Reflection.ReflectionTypeLoadException' was thrown.
---
--SetUp
  at (wrapper managed-to-native) System.Reflection.Assembly.GetTypes(System.Reflection.Assembly,bool)
  at System.Reflection.Assembly.GetTypes () [0x00000] in <fb001e01371b4adca20013e0ac763896>:0
  at Unity.RecordedTesting.RecordedTesting.GetAllRecordedTests () [0x00023] in /Users/****/Documents/UnityTestWorkspace/Library/PackageCache/com.unity.automated-testing@0.2.0-preview.3/RecordedTesting/Runtime/RecordedTesting.cs:55
(snip)

遭遇したアセンブリMicrosoft.CodeAnalysis.Scripting で、これはImmediate Window (com.unity.immediate-window) パッケージの依存先になっています。 今回はImmediate WindowパッケージをPackage Managerから削除することで解消しましたが、Automated QA側で対応されるまでは他のアセンブリで発生する恐れもあります。

Scene couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded

Recorded Testingでは、再生しようとするjsonに記録されているSceneをロードしてから操作を開始します。 しかし対象のSceneがビルド設定のScenes in Buildに入っていない場合、この例外が発生します。

SetUp : System.NullReferenceException : Object reference not set to an instance of an object
SetUp : Unhandled log message: '[Error] Scene '***' couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded.
To add a scene to the build settings use the menu File->Build Settings...'. Use UnityEngine.TestTools.LogAssert.Expect

ビルドに含めるSceneであればScenes in Buildに含めればよいのですが、テスト専用やアセットバンドルに格納するものは別の手段でロードする必要があります。

Unityエディタでの実行であれば、SceneManager.LoadSceneAsyncでなくEditorSceneManager.LoadSceneAsyncInPlayModeを使うことで回避できます。

Standalone playerでの実行では、テスト実行前のビルドにフックするITestPlayerBuildModifierを実装して、ビルド対象Sceneを書き換えることで回避できます。

実装例はこちらを参考にしてください。 github.com

いずれの問題もForumで報告済みなので、そのうち対応されるはず。

参考

docs.unity3d.com

forum.unity.com

*1:本稿執筆時点でPackage Managerからはインポートできますが、マニュアルサイトには未反映