やらなイカ?

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

Unity Test Framework完全攻略ガイド 第3版

3年ぶりの改版となる『Unity Test Framework完全攻略ガイド 第3版』を、コミックマーケット107の2日目(水曜日)南i-17b「いか小屋」で頒布します。

第3版では、Unity Test Frameworkパッケージの最新APIへの追随だけでなく、本書で扱う範囲を広げています。 第1章は大幅にリライトし、ユニットテスト以外のテスト活動も含めた全体像や自動テストの保守性にも言及しています。 また追加した第14章「継続的インテグレーション」でCI環境の構築方法を解説しているほか、テストコードの保守・運用や実装テクニックについての一般知識も盛り込みました。

想定読者

UnityでC#スクリプトを書けるレベルのソフトウェアエンジニア(プログラマー)を想定しています。UnityやC#言語自体の解説はしていません。 テスト、特にユニットテストに関しては、入門レベルからカバーしています。

また読者の属する開発プロジェクトの規模を、個人から小規模と想定しています*1CEDECなどで発表されるエンドツーエンド(E2E)テストの自動化は難易度も高くコストもかかるものですが、開発者テストは個人や小規模チームでの開発でも効果を発揮します。むしろQA担当者の大量投入という選択肢のないプロジェクトでこそ有効活用すべき技術です。

目次の紹介

各章の内容(第2版からの差分)は次のとおりです。

第1章 テストとは何か

第2版から大幅にリライトしています。 「テスト」と、ゲーム業界で使われる「デバッグ」という言葉の違い、CEDEC2025のセッション『E2Eだけがテスト自動化じゃない! Unity製ゲームの開発者テスト チュートリアル』でお話したシフトレフト、階層化テスト戦略、また自動テストの保守性にも言及しています。

第2章 Unity Test Frameworkの基本

Unity Test Frameworkパッケージを使ってユニットテストを書き、実行する手順を、順序立てて説明しています。 第3版では「2.6 よいテストコードを書くヒント」を追加しています。

第3章 テストモード

Edit ModeテストとPlay Modeテストの違い、それぞれに固有のAPIを紹介しています。 第2版までは個別の章でしたが統合しました*2

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

第3版ではUnity Test Framework v1.3でサポートされたasync/awaitによる非同期テストを追加し、従来からあるUnityTest属性によるテストについても制限事項をアップデートしています。

第5章 アサーション

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

第3版では、制約モデルの記述例および失敗メッセージ例をいくつか本文中に記述するようにしました。 またTipsに分散していた特殊なアサーションに関する記述を本章にまとめました。

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

ひとつのメソッドを引数を様々に変えてテストしたいときに使用できるパラメタライズドテストの使いかたを紹介しています。

第3版では、Unity Test Framework v1.4で追加されたParametrizedIgnore属性および、Unity Test Frameworkの問題が解消されたTestFixture属性によるパラメタライズドテストについての解説を追加しました。

第7章 テストダブル

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

第8章 Unity Test Framework Tips

テストの前処理・後処理、実行プラットフォーム制限、カテゴライズ、タイムアウト時間の変更、MonoBehaviourのテスト方法などを紹介しています。

第9章 Scene・アセット・ファイルの使用

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

第3版では、Scene以外のテスト用アセットをプレイヤー実行で使う方法などを追加しています。

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

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

第11章 Unity Test Frameworkの拡張

第3版で追加した章です。 NUnitの属性、制約、比較子(Comparer)をゲームタイトルで拡張して使用する方法を紹介しています。

第12章 テストの実行方法

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

第13章 コードカバレッジ

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

第14章 継続的インテグレーション

第3版で追加した章です。 GitHub Actionsを用いたCI環境の構築方法、octocovによるコードカバレッジの可視化などを紹介しています。

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

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

第3版ではランダム性をもつテスト対象をどうテストするかのアプローチを追加しています。

付録A Test Helperパッケージ

第3版で追加した付録です。 筆者の公開しているUnity Test Framework拡張集であるTest Helperパッケージを紹介しています。

付録B Roslynアナライザー

第3版で追加した付録です。 Roslynアナライザーによる静的解析について紹介しています。

付録C JetBrains Rider Tips

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

付録D OpenUPM-CLI

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

表紙

いかの足が2本から3本に!

スペックと頒布価格

変更点を特記していない章も、前提バージョンの変更や記述の見直しなどを実施しています。 原稿の行ベースで見ると第2版の4,623行に対して、+5,296行、-2,612行、差し引き(純増)+2,684行、計7,307行。 ページ数は152から220と68ページ増。

しかしなんと、お値段据え置き ¥2,000 で頒布します。たぶん増刷はしません。 当日残ればBOOTHで通販します。

また電子版はこれまで同様、第2版以前を購入いただいていれば追ってアップデートします。

スペースの場所

2日目(水曜日)南i-17b「いか小屋」です。

既刊の在庫も持っていきます。

www.nowsprinting.com

www.nowsprinting.com

関連

www.nowsprinting.com

www.nowsprinting.com

*1:ユニットテストを書くにあたって規模は関係ないのですが、CI環境としてGitHub Actionsを紹介しているあたりが小規模を意識しています

*2:単に目次の都合で大きな意味はありません

Unity 6 で追加された Roslyn Analyzer 関連機能

Unity 6 のマニュアルを見ていたら、Roslyn Analyzer についての機能追加があったので紹介します。

docs.unity3d.com

Microsoft.CodeAnalysis.Csharp 4.3

アナライザおよびコードジェネレーターをビルドするときに使用する Microsoft.CodeAnalysis.Csharp のバージョンが4.3になったことが明記されました。

なお、実際は Unity 2022.3.12f1 から 4.3 に上がっていたことが知られています。

参考:neue cc - 2022年(2024年)のC# Incremental Source Generator開発手法

Report analyzer diagnostics

アナライザおよびコードジェネレーターの実行時間を Editor.log に出力する機能です。

Windows では Edit > PreferencesmacOS では Unity > Settings で Preferencesウィンドウを開き、Diagnostics タブを選択します。

最初は警告が表示されますが、I understand, show me the settings ボタンを押すと設定項目が表示されます。

Code を展開して EnableDomainReloadTimings をonにすると有効化できます。 この機能が有効な間、Consoleウィンドウに次の警告が出ます。

Diagnostic switches are active and may impact performance or degrade your user experience. Switches can be configured through the Diagnostics section in the Preferences window. EnableDomainReloadTimings: True

この状態でコンパイルが走ると、Editor.log に次のようにアナライザごとに使用した時間と割合が出力されます。 これが対象のアセンブリ(DLL)ごとに出ます。

[1134/1140  0s] Csc Library/Bee/artifacts/200b0aEDbg.dag/TestHelper.UI.Tests.dll (+2 others)

Total analyzer execution time: 1.084 seconds.
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.

Time (s)    %   Analyzer
   0.754   69   IDisposableAnalyzers, Version=4.0.4.0, Culture=neutral, PublicKeyToken=3feb74da3c492280
   0.258   23      IDisposableAnalyzers.FieldAndPropertyDeclarationAnalyzer (IDISP002, IDISP006, IDISP008)
   0.252   23      IDisposableAnalyzers.CreationAnalyzer (IDISP004, IDISP014)
   0.181   16      IDisposableAnalyzers.AssignmentAnalyzer (IDISP001, IDISP003, IDISP008)
   0.036    3      IDisposableAnalyzers.LocalDeclarationAnalyzer (IDISP001, IDISP007)
   0.009   <1      IDisposableAnalyzers.DisposeCallAnalyzer (IDISP007, IDISP016, IDISP017)
   0.008   <1      IDisposableAnalyzers.ReturnValueAnalyzer (IDISP005, IDISP011, IDISP012, IDISP013)
   0.006   <1      IDisposableAnalyzers.ArgumentAnalyzer (IDISP001, IDISP003)
   0.001   <1      IDisposableAnalyzers.MethodReturnValuesAnalyzer (IDISP015)
   0.001   <1      IDisposableAnalyzers.SuppressFinalizeAnalyzer (IDISP024)
   0.001   <1      IDisposableAnalyzers.ClassDeclarationAnalyzer (IDISP025, IDISP026)
  <0.001   <1      IDisposableAnalyzers.DisposeMethodAnalyzer (IDISP009, IDISP010, IDISP018, IDISP019, IDISP020, IDISP021, IDISP023)
  <0.001   <1      IDisposableAnalyzers.UsingStatementAnalyzer (IDISP007)
  <0.001   <1      IDisposableAnalyzers.FinalizerAnalyzer (IDISP022, IDISP023)
  <0.001   <1      IDisposableAnalyzers.SemanticModelCacheAnalyzer (SyntaxTreeCacheAnalyzer)

   0.330   30   nunit.analyzers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
   0.102    9      NUnit.Analyzers.TestCaseSourceUsage.TestCaseSourceUsesStringAnalyzer (NUnit1002, NUnit1011, NUnit1015, NUnit1016, NUnit1017, NUnit1018, NUnit1019, NUnit1020, NUnit1029, NUnit1030)
   0.051    4      NUnit.Analyzers.DisposeFieldsInTearDown.DisposeFieldsAndPropertiesInTearDownAnalyzer (NUnit1032)
   0.046    4      NUnit.Analyzers.TestMethodAccessibilityLevel.TestMethodAccessibilityLevelAnalyzer (NUnit1026)
   0.031    2      NUnit.Analyzers.ValueSourceUsage.ValueSourceUsageAnalyzer (NUnit1021, NUnit1022, NUnit1023, NUnit1024, NUnit1025)
   0.016    1      NUnit.Analyzers.TestMethodUsage.TestMethodUsageAnalyzer (NUnit1005, NUnit1006, NUnit1007, NUnit1012, NUnit1013, NUnit1014, NUnit1027)
   0.012    1      NUnit.Analyzers.StringConstraintWrongActualType.StringConstraintWrongActualTypeAnalyzer (NUnit2024)
   0.009   <1      NUnit.Analyzers.SameAsOnValueTypes.SameAsOnValueTypesAnalyzer (NUnit2040)
   0.008   <1      NUnit.Analyzers.ComparableTypes.ComparableTypesAnalyzer (NUnit2041, NUnit2042)
   0.007   <1      NUnit.Analyzers.SameAsIncompatibleTypes.SameAsIncompatibleTypesAnalyzer (NUnit2020)
   0.006   <1      NUnit.Analyzers.EqualToIncompatibleTypes.EqualToIncompatibleTypesAnalyzer (NUnit2021)
   0.004   <1      NUnit.Analyzers.WithinUsage.WithinUsageAnalyzer (NUnit2047)
   0.004   <1      NUnit.Analyzers.ParallelizableUsage.ParallelizableUsageAnalyzer (NUnit1008, NUnit1009, NUnit1010)
   0.004   <1      NUnit.Analyzers.NullConstraintUsage.NullConstraintUsageAnalyzer (NUnit2023)
   0.004   <1      NUnit.Analyzers.DelegateRequired.DelegateRequiredAnalyzer (NUnit2044)
   0.004   <1      NUnit.Analyzers.SameActualExpectedValue.SameActualExpectedValueAnalyzer (NUnit2009)
   0.003   <1      NUnit.Analyzers.MissingProperty.MissingPropertyAnalyzer (NUnit2022)
   0.003   <1      NUnit.Analyzers.IgnoreCaseUsage.IgnoreCaseUsageAnalyzer (NUnit2008)
   0.003   <1      NUnit.Analyzers.SomeItemsIncompatibleTypes.SomeItemsIncompatibleTypesAnalyzer (NUnit2026)
   0.003   <1      NUnit.Analyzers.ClassicModelAssertUsage.ClassicModelAssertUsageAnalyzer (NUnit2001, NUnit2002, NUnit2003, NUnit2004, NUnit2005, NUnit2006, NUnit2015, NUnit2016, NUnit2017, NUnit2018, NUnit2019, NUnit2027, NUnit2028, NUnit2029, NUnit2030, NUnit2031, NUnit2032, NUnit2033, NUnit2034, NUnit2035, NUnit2036, NUnit2037, NUnit2038, NUnit2039)
   0.002   <1      NUnit.Analyzers.ConstActualValueUsage.ConstActualValueUsageAnalyzer (NUnit2007)
   0.002   <1      NUnit.Analyzers.TestCaseUsage.TestCaseUsageAnalyzer (NUnit1001, NUnit1003, NUnit1004)
   0.002   <1      NUnit.Analyzers.UpdateStringFormatToInterpolatableString.UpdateStringFormatToInterpolatableStringAnalyzer (NUnit2050)
   0.001   <1      NUnit.Analyzers.ValuesUsage.ValuesUsageAnalyzer (NUnit1031)
   0.001   <1      NUnit.Analyzers.CollectionAssertUsage.CollectionAssertUsageAnalyzer (NUnit2049)
   0.001   <1      NUnit.Analyzers.StringAssertUsage.StringAssertUsageAnalyzer (NUnit2048)
  <0.001   <1      NUnit.Analyzers.DiagnosticSuppressors.NonNullableFieldOrPropertyIsUninitializedSuppressor ()
  <0.001   <1      NUnit.Analyzers.DiagnosticSuppressors.AvoidUninstantiatedInternalClassSuppressor ()
  <0.001   <1      NUnit.Analyzers.DiagnosticSuppressors.DereferencePossiblyNullReferenceSuppressor ()
  <0.001   <1      NUnit.Analyzers.UseAssertMultiple.UseAssertMultipleAnalyzer (NUnit2045)
  <0.001   <1      NUnit.Analyzers.ConstraintUsage.SomeItemsConstraintUsageAnalyzer (NUnit2014)
  <0.001   <1      NUnit.Analyzers.DiagnosticSuppressors.TypesThatOwnDisposableFieldsShouldBeDisposableSuppressor ()
  <0.001   <1      NUnit.Analyzers.NonTestMethodAccessibilityLevel.NonTestMethodAccessibilityLevelAnalyzer (NUnit1028)
  <0.001   <1      NUnit.Analyzers.ConstraintUsage.ComparisonConstraintUsageAnalyzer (NUnit2043)
  <0.001   <1      NUnit.Analyzers.ConstraintUsage.StringConstraintUsageAnalyzer (NUnit2011, NUnit2012, NUnit2013)
  <0.001   <1      NUnit.Analyzers.ConstraintUsage.EqualConstraintUsageAnalyzer (NUnit2010)
  <0.001   <1      NUnit.Analyzers.ContainsConstraintWrongActualType.ContainsConstraintWrongActualTypeAnalyzer (NUnit2025)
  <0.001   <1      NUnit.Analyzers.UseCollectionConstraint.UseCollectionConstraintAnalyzer (NUnit2046)
[           0s] Csc Library/Bee/artifacts/200b0aEDbg.dag/TestHelper.UI.Tests.dll (+2 others) [CacheWrite 00000000000000000000000000000004]

Tips: Editor.log は、Consoleウィンドウ右上の > Open Editor Log で開けます(macOSの場合コンソールappで開きます)。

Additional files

Assets フォルダ下に拡張子 .additionalfile のファイルを置くと、ファイルパスを Additional files としてアナライザに渡すことができます。 たとえば Foo.DemoAnalyzers.additionalfile というファイルを置くと、csproj ファイルに次のように追加されます。

<ItemGroup>
    <AdditionalFiles Include="Assets/Foo.DemoAnalyzers.additionalfile" />
</ItemGroup>

ただし、ファイル名は Filename.[Analyzer Name].additionalfile と決まっているため、たとえば BannedApiAnalyzers のように規定のファイル名しか受け付けないアナライザには使用できません。

参考:How to use Microsoft.CodeAnalysis.BannedApiAnalyzers

これについては BannedApiAnalyzers に Issue #78124 が立っていますが、Unity側でなんとかしてほしいというのはそれはそう*1

なおこの機能、マニュアルに記載されたのは Unity 6.0 ですが、APIリファレンスを見ると Unity 2021.3 からあったようです。

Roslyn global config file

記事タイトルから逸脱しますが、この機能は Unity 6.2 時点でもマニュアルには未記載、APIリファレンスには Unity 2021.3 から記載されていたものです。

拡張子 .globalconfig のファイルを置くと、アナライザの重大度(severity)を設定できます。つまり ruleset ファイル を置き換えるものです。

ファイルフォーマットは、先頭に is_global = true を書く以外は EditorConfig の重大度設定と同じ*2で、たとえば次のように書きます。

is_global = true
dotnet_diagnostic.IDISP001.severity = error

なお、置き場所やファイル名にはルールがあります。 Default.globalconfig(全アセンブリに効く)や Assembly-CSharp.globalconfig(Assembly-CSharpに効く)は Assets 直下に、 アセンブリ個々にはアセンブリ定義ファイル(.asmdef)と同じディレクトリに任意の名前で置きます。

また、csproj ファイルに GlobalAnalyzerConfigFiles として追加されるため、IDE でもUnityエディタと同じ設定を共有できます。 しかし、JetBrains Rider には本稿執筆時点で次のバグがあり、対応が待たれます。[10/7 追記]

参考:

*1:BannedApiAnalyzers 側で拡張子だけ許容すれば使えるようにはなるのですが、それを既存のアナライザ全部に対してやるのかという話

*2:ファイルタイプやパスによるセクション分けはできず、またコードスタイルを書いても無視されます

UI Test Framework パッケージ v1.0 ファーストインプレッション

先日リリースされたUnity 6.3から、UI Toolkit(旧UIElements)を使ったUIのテストをサポートするUI Test Frameworkパッケージ(com.unity.ui.test-framework)が使えるようになりました。

docs.unity3d.com

UI Toolkitは徐々に存在感を増しながらも、テスタビリティがとても低いという致命的な問題がありました。 UI Test Frameworkパッケージはまだ完全とは言えなませんが、ランタイムもUI Toolkitで作るという話が少し現実味を帯びたように思います*1

本記事は、少しさわってみた範囲のメモ書きです。

前提:UI Toolkitの何が問題なのか

UI ToolkitはuGUIと異なり、GameObjectと紐づくのはルート要素であるUIDocumentコンポーネントだけであり、配下のUI要素は別空間です。 EventSystemInputModuleでは干渉できません。

従って、UI要素を操作するテストを書くにはUI ToolkitのAPIに頼らざるを得ません。 そのAPI、UI要素の検索はQuery<T>で可能ですが、クリックなどのイベントを送るSendEventはテストコードから呼んでも動作しません*2。 ほか、ListViewchildCountselectedItemが取得できないといった問題もあります。

環境

  • Unity 6000.3.0b1
  • UI Test Frameworkパッケージ(com.unity.ui.test-framework)v1.0.0

UI Test Frameworkパッケージのインストール

UI Test Frameworkパッケージは、Unity 6.3以降でインストールできます。 Package Managerウィンドウを開き、Unity Registryから検索してインストールします。

UI Test Frameworkパッケージの概要

UI Test Frameworkパッケージには、 UnityEditor.UIElements.TestFrameworkUnityEngine.UIElements.TestFrameworkの2つの名前空間があります。 基本的なAPIは共通のベースクラスに実装されており、エディタ側にはコンテキストメニューやインスペクターウィンドウのサポートがあります。

テストクラスは、EditorUITestFixtureもしくはRuntimeUITestFixtureを継承*3することでPanelSimulatorなどシミュレータ系のAPIを使ったテストを書くことができます。

ボタンをクリックするテストの例

[TestFixture]
public class ButtonTest : RuntimeUITestFixture // 継承
{
    [Test]
    [LoadScene("../../../Scenes/Button.unity")]
    public async Task ボタンをクリック_ラベルの数字がインクリメントすること()
    {
        // Arrange
        var uiDocument = Object.FindAnyObjectByType<UIDocument>();
        var root = uiDocument.rootVisualElement;
        var button = root.Query<Button>("increment_button").First();
        var label = root.Query<Label>("counter_text").First();
        Assume.That(label.text, Is.EqualTo("0")); // ラベルの初期値は "0"

        // Act
        simulate.Click(button);

        // Assert
        Assert.That(label.text, Is.EqualTo("1")); // ボタンクリックでインクリメントされることを検証
    }
}

Arrangeでは、Sceneファイルをロードし、GameObjectに紐づいたUIDocumentを取得、配下のボタンとラベルを取得しています。 ここまでは通常のUI ToolkitのAPIです。

Actで使用しているsimulateRuntimeUITestFixtureのプロパティで、型はPanelSimulatorです。 Clickメソッドを使ってbuttonのクリック操作を行います。 PanelSimulatorには、クリックするスクリーン座標を渡すClickオーバーロードがあるほか、DragAndDropKeyDownなどのメソッドが実装されています。

なお、Clickは同期メソッドになっていますが、テストメソッドは非同期(async TestもしくはUnityTest属性)でないと動作しません。

UITestFrameworkRuntimeOptionsアセット

RuntimeUITestFixtureのテストを実行するには、UITestFrameworkRuntimeOptionsアセットファイルが必要です。 このファイルは、コンテキストメニューから Create > UI Toolkit > UI Tests Runtime Optionsで生成できます。これをResourcesフォルダ下に置きます。

UITestFixtureにrootVisualElementをセットする

正確には、RuntimeUITestFixtureが保持するPanelSimulatorが保持するrootVisualElementです。 TestFixture(テストクラス)がひとつのrootVisualElementからなるUI要素のツリーを保持するようになっています。

前述のClickメソッドなどはrootVisualElement未設定でも動作しますが、FrameUpdateメソッド系を使用するとき例外が出ます。

rootVisualElementは直接設定するのではなく、SetUIContentメソッドにUIDocumentを渡して設定します。 先のコードのArrangeフェーズを書き換えると次のようになります。

var uiDocument = Object.FindAnyObjectByType<UIDocument>();
SetUIContent(uiDocument);

var button = rootVisualElement.Query<Button>("increment_button").First();
var label = rootVisualElement.Query<Label>("counter_text").First();

所感

少し触っただけですが、クリックなどのイベントが動作するだけでも大きく前進したと言えます。 またランタイムでなくエディタに目を向けると、IMGUIはほぼテスト不能でしたのでUI Toolkitに乗り換えも検討しようかという気持ちになりました。

未検証ですが、MenuSimulatorPopupMenuSimulatorContextMenuSimulatorInspectorTestUtilityといったクラスは気になっています。

ただ全体に、ユニットテスト向けという印象を受けました。UIに対するユニットテストはROIが低くなるので避けたほうがよく、UI操作は画面遷移を伴う統合テストで使いたいのですが。

関連

www.nowsprinting.com

www.nowsprinting.com

*1:Unityさんが課題意識を持っててくれたことがわかっただけでも収穫ですね

*2:プレイヤーループでUI Toolkitが動作するフェーズが特殊なのが原因かと思うのですが未調査

*3:New Input SystemパッケージのTest Frameworkと同じ方式ですが、正直やめてほしい

UI Test Helper パッケージ v1.0.0 リリース

Unity Test Framework でUIを操作するテストを書くときに便利なライブラリ UI Test Helper パッケージのバージョン 1.0.0 をリリースしました。

元々モンキーテスト用のライブラリ(旧称 Monkey Test Helper)として開発していましたが、モンキーに限らずUIテストに使えるAPIが増えてきたので改名しました*1

github.com

インストールは、GitHubリポジトリ指定や、openupm.comから可能です。 openupm-cli であれば次のコマンドでインストールできます。

openupm add com.nowsprinting.test-helper.ui

以下、v1.0.0で実装している機能を簡単に紹介します*2

TOC

GameObjectの検索

GameObjectFinderクラスでScene上のGameObjectを検索できます。GameObject.Findと異なる次のような特徴があります。

  • GameObjectが見つからない場合、指定したタイムアウトまでポーリング*3
  • ユーザーが到達可能(カメラからのレイキャストが通ること)を条件にできる*4
  • 操作可能な状態を条件にできる*5
  • GameObjectのパスで検索するとき、globのワイルドカード書式が使用できる
  • Button を text および textureファイル名で検索できる(ButtonMatcherを使用)
  • ルーセルやスクロール領域内のオブジェクトも検索できる(uGUIのScrollRectであればUguiScrollRectPaginatorを使用)

使用例

using NUnit.Framework;
using TestHelper.UI;

[TestFixture]
public class MyIntegrationTest
{
    [Test]
    public async Task パスで検索()
    {
        var finder = new GameObjectFinder(5d); // 5 seconds timeout
        var result = await finder.FindByPathAsync("/**/Confirm/**/Cancel", reachable: true, interactable: true);
        var cancelButton = result.GameObject;
    }

    [Test]
    public async Task ボタンのテキストで検索()
    {
        var finder = new GameObjectFinder();
        var matcher = new ButtonMatcher(text: "Click Me");
        var result = await finder.FindByMatcherAsync(matcher, reachable: true, interactable: false);
        var button = result.GameObject;
    }

    [Test]
    public async Task ScrollRect内のボタンを検索()
    {
        var finder = new GameObjectFinder();
        var matcher = new NameMatcher("Button_10");

        var scrollView = GameObject.Find("Scroll View");
        var scrollRect = scrollView.GetComponent<ScrollRect>();
        var paginator = new UguiScrollRectPaginator(scrollRect);

        var result = await finder.FindByMatcherAsync(matcher, paginator: paginator);
        var button = result.GameObject;
    }
}

GameObjectの操作

操作に応じたオペレーターが定義・実装されており、uGUIのイベントシステムを使用してクリックなどのイベントをGameObjectに送って操作します。

たとえば UguiClickOperator であれば、次のイベントを再現します。

  1. OnPointerEnter
  2. OnSelect*6
  3. OnPointerDown
  4. OnInitializePotentialDrag
  5. OnPointerUp
  6. OnPointerClick
  7. OnPointerExit

使用例

using NUnit.Framework;
using TestHelper.UI;

[TestFixture]
public class MyIntegrationTest
{
    [Test]
    public async Task InputFieldに文字入力してSubmitButtonをクリック()
    {
        var finder = new GameObjectFinder();

        var message = await finder.FindByNameAsync("Message", interactable: true);
        var inputOperator = new UguiTextInputOperator();
        await inputOperator.OperateAsync(message.GameObject, "Hello, Hurry?");

        var submit = await finder.FindByNameAsync("SubmitButton", interactable: true);
        var clickOperator = new UguiClickOperator();
        await clickOperator.OperateAsync(submit.GameObject);
    }
}

モンキーテスト

画面上の操作可能なUI要素をでたらめに操作します。 テスト実行時間や操作間隔、また使用する操作の種類は MonkeyConfig で指定できます。

なお、モンキーテストで操作させたくないUI要素を無視させる IgnoreAnnotation のほか、操作する座標をずらしたり入力する文字種類の指定を行うアノテーションコンポーネントを利用できます。

使用例

using System;
using System.Threading.Tasks;
using NUnit.Framework;
using TestHelper.UI;

[TestFixture]
public class MyIntegrationTest
{
    [Test]
    public async Task モンキーテスト()
    {
        var config = new MonkeyConfig
        {
            Lifetime = TimeSpan.FromMinutes(2),
            DelayMillis = 200,
            SecondsToErrorForNoInteractiveComponent = 5,
        };

        await Monkey.Run(config);
    }
}

エディター拡張

パスのコピー

ヒエラルキーウィンドウでGameObjectを右クリックして Copy to Clipboard > Hierarchy Path でGameObjectのパスをクリップボードにコピーできます。

インスタンスIDのコピー

ヒエラルキーウィンドウでGameObjectを右クリックして Copy to Clipboard > Instance ID でGameObjectのインスタンスIDをクリップボードにコピーできます。

カスタムUIフレームワークへの対応

uGUIに準拠していないカスタムUIフレームワークを使用している場合、ビルトイン機能をそのまま使用できない場合があります。 UI Test Helperは次の拡張ポイントを用意しており、必要に応じてゲームタイトル側に実装することで紹介したAPI(モンキーテストも含め)をそのまま利用できます。

  • オブジェクトが操作可能かを判断する関数
  • オブジェクトを無視するべきかを判断する関数
  • ユーザーがオブジェクトに到達可能かを判断する関数
  • 検索に使用するMatcherの実装
  • 検索に使用するPaginatorの実装
  • Operatorの実装

関連

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

*1:前々から機会を伺っていたのですが、CEDEC2025登壇に合わせてやりました

*2:詳しくはリポジトリのREADMEを参照してください

*3:固定秒数待機するのはアンチパターンです

*4:スマートフォンゲームでよくある、別のオブジェクトで隠してタップを受け付けない状態では待つ

*5:uGUIであればInteractableで判断します

*6:オブジェクトがSelectableのときのみ。なお、OnDeselectはuGUI側で発行されるため再現していません

CEDEC2025『E2Eだけがテスト自動化じゃない! Unity製ゲームの開発者テスト チュートリアル』フォローアップ

CEDECはじめゲーム領域でもテスト自動化の事例紹介が増えましたが、従来の手動テストを自動テストに置き換えるアプローチが紹介されることが多く*1、ROIが高く小規模なタイトルであっても取り組める開発者テストの事例・情報は少ないのが現状です。

そんな中、CEDEC2025 で開発者テストについての招待講演のお話をいただき登壇してきました。 タイムシフト視聴は 8/4 10:00 AMまでなので、パスをお持ちの方はぜひご視聴ください。

cedec.cesa.or.jp

スライドはこちら(CEDiLと同じもの)

参考リンク

スライド中にも埋め込んでありますが、リンクを書き出します。

Automated Testing of Gameplay Features in 'Sea of Thieves' (GDC2019)

バグ数のグラフだけ引用しましたが、開発者テストの導入だけで得られた結果ではなく、テストの運用も徹底した成果です。 それも含め、参考になるはずです。

スライド

www.gdcvault.com

動画

www.youtube.com

Unity公式パッケージ

OSSパッケージ

参考書籍

ユニットテストと統合テストについて、ゲーム本体のテスタビリティとテストコードの保守性にフォーカスしてお話しました。 またAnjinによるゲームプレイテストについても、ごく一部のユースケースしか紹介できませんでした。 いずれも、詳しくは同人誌に書いておりますので参考にしてください。

セッションでも触れましたが、『Unity Test Framework完全攻略ガイド』は夏のコミックマーケット106で第3版に改版予定です。 電子版は今BOOTHに出ているものをご購入いただければアップデートされます。技術書典マーケットも同様にアップデートしますが、タイミングは次のイベント開催時期(11月)になりますのでご注意ください。

ikagoya.booth.pm

ikagoya.booth.pm

ikagoya.booth.pm

ユニットテストのセクションで紹介した各種パターンの名称は、xUnit Test Patterns (xUTP) で紹介されているものです。 やや古い書籍ですが、現在でも現役で使用しているパターン集です。

Ask the Speaker

多数の方に来ていただき、ほぼ時間いっぱい色々お話させていただきました。覚えている範囲かつ書けるものだけ紹介します。

Test RunnerウィンドウのPlayerタブが使いにくい件

Unity Test Frameworkパッケージ(以下UTF)v1.3.xまでは、Play Modeテストを名前やカテゴリで絞り込んでから、エディタ内で実行ボタンかプレイヤー実行ボタンで実行できました。つまり、同条件での実行が簡単でした。

UTF v1.4からプレイヤー実行はPlayerタブで実行するようになったため、開発(変更)した箇所のPlay Modeテストをエディタ実行した後、同条件でプレイヤー実行するには、タブを切り替え、名前やカテゴリの絞り込みを再度行なう必要があり不便になりました。

同じ不満を持った方によるフォーラムの投稿 Test Framework 1.4 / 1.5 feedback and bugs (IN-88016) がありますので、同様の感想をお持ちの方はLikeするなどしてほしいです。

また、タブを分けるのであればテストの有効/無効に UnityPlatform属性を反映してほしい(現状はPlayerタブでもEditorと扱われる)という要望かIssueを投げてリジェクトされた記憶があるのですが探し出せませんでした*2

開発者テストをどこから導入すべきか

テストが開発の役に立つという体験が先決だと思っています。2つアプローチがあります。

  1. Anjinで広く荒くカバーするテスト(アウトゲームの画面遷移やチュートリアルなど)をCIで定期実行し、「QA向けビルドが壊れた」ときに早く検知できるようにする
  2. 実際の開発プロジェクトではなく、ユニットテスト習得のための題材*3でワークショップを実施する

ワークショップで実際のプロジェクトを使わないのは、テスタビリティが低いところがあったり担当箇所によって体験に差が出るためです。 ワークショップ実施前に参加メンバーで『Unity Test Framework完全攻略ガイド』の読書会を実施するのもおすすめです。

統合テストにはどう取り組めばいいのか

統合テストのターゲット、粒度の設定などは、ゲームタイトルごとに異なります。ゲームジャンルによる差異だけでなく、そもそも工程の考え方もドキュメントの粒度も標準的なものがなくバラバラなので。

まず観点として「なにをテストするのか」(もしくは「どのようなバグを見つけたいのか」)から出発します。わかりやすいのは仕様書に書かれている粒度・精度(解像度)、機能実装のPull Requestに書いている確認内容をテストコードにするという考え方です。

たとえばアウトゲームであれば、UI操作を通して「〇〇画面に遷移して〇〇機能を実行すると何が起こるのか」といったテストが中心になるでしょう。 インゲームの例として前掲の Sea of Thieves では「敵キャラが索敵、プレイヤーを発見、接近、攻撃」するという一連の振る舞いをActorのテスト*4として実施したことが紹介されています。

バグが出たときに「自動テストがあれば見つかっていた」と言う話はすべきか

大いにすべきだと思っています。目的は将来もしくは次のプロジェクトでの改善で、すぐには着手できなくともチームの課題意識を高め解像度を上げていく効果が見込めます。 ただし漠然とした「テスト」という表現では意味がなく、具体的にどのような観点のテストであれば発見できたかまで言及しないと机上の空論となり無意味です。

CEDEC AWARDS 2025

招待講演のほか、CEDEC AWARDS 2025 エンジニアリング部門で 『Unity開発におけるシフトレフトの推進、オープンソース化による技術の伝搬』 ということで優秀賞をいただきました!

開発者テストという地味な取り組みを拾い上げていただいて感謝です。テストに興味を持ってくれる人が増えるきっかけになれば幸いです。

cedec.cesa.or.jp

C106

コミックマーケット106に出ます! 2日目(日曜)東地区 “U”ブロック-37a、東7ホール入口(MAPの下側)から比較的近い島です。 お隣は『UNIBOOK16』の日本Androidの会Unity部さんです。

新刊は3年ぶりの改版『Unity Test Framework完全攻略ガイド 第3版』*5。 既刊Anjin本とTAROMAN本の在庫も持っていきます。

お近くまでお越しのついでにでも寄っていただけると嬉しいです。お待ちしております!

*1:見栄えしますし難しい取り組みだからこそ講演の題材になり得るためだと思われます

*2:このissue https://unity3d.atlassian.net/servicedesk/customer/portal/2/IN-72066 でテスト実行結果だけは反映されるよう修正してもらえらのですが

*3:時期は未定ですが、サイバーエージェント様で使用しているものを公開できる予定です

*4:Unreal EngineにおけるIntegration testsはかなり重いようで、その手前に独自にActor testsのレイヤーを定義して実装されています

*5:間に合わなかったらごめんなさい…

Unityプロジェクトでも Claude Code に自走させるワークフロー

最近は Claude Code 中心に使っていて、Unityプロジェクト(といってもゲームでなくUPMパッケージ開発が多い)でもそこそこ安定するワークフローが固まってきました。 一例として紹介します。

目的

紹介するワークフローの目的は次の2点です。

  • できるだけ自律的に、自走させたい
  • レビュー負荷を減らしたい

前提として、すべてこのワークフローを使うのではなく、例えばコードを書きながら探索的に設計したいケースでは*1 従来通りGitHub Copilotのサジェストを使ってTDDで書いています。

また、いわゆる "Vibe Coding" ではなく、実装コードもテストコードもレビューし、保守性を高める修正を入れています。

ワークフロー

次の手順で開発を進めます。

  1. Plan modeで、実装する機能の要件、仕様、一部の実装方法などを伝え、それを一旦ドキュメントに書き出し
  2. ドキュメントをもとにテストケースを作成、ドキュメントに追記
  3. ドキュメントに従って実装。テストファースト(TDDではない)で、テストをパス、Roslynアナライザなどの診断にも対応させて完了

ドキュメントを経由する理由は、チェックポイントとして使うことと、コンテキストウィンドウの圧縮です。 各工程でドキュメント出力したら /clear コマンドなどでコンテキストをクリアしています*2

Claude Codeに限らず、ルールの強制力は弱いのであまり信用していなくて、プロンプトによる指示を重視していました。 Claude Codeにはカスタムコマンド機能があるので、各工程に対応したコマンドファイルを用意して使用しています。

/create-doc コマンド

工程1で要件などを伝え終えた後で(Claude Codeが実装に進もうとするのを止めて)実行します。

これまでの会話内容を仕様書としてファイルに書き出してください。

以下の手順に従ってください。

1. 会話中に、要件(なぜ)、仕様(何を)、設計(どのように)が明確になっているか分析してください。明確でない場合は、このコマンドの実行を中止してください。
2. ファイルに書き出してください。
  - 形式:Markdown
  - 言語:日本語
  - パス:./Documentation/yyyy-mm-dd-subject.md
    - yyyy-mm-dd は現在日付です。
  - セクション:
    - 要件、問題、または動機(適切なものを選択してください)
    - 仕様
    - 設計
3. Git にコミットしてください。

ドキュメント化に必要な情報を伝えきれていれば、ファイルが作られます。 軸になるのは「仕様」の部分ですが、LLMへの指示が目的ですので、ある程度の実装方式まで盛り込むほうが後工程が安定します。

この時点でレビューし、不足情報があれば付け足すなど修正します。 修正は Claude Code に頼んでも、ファイルを直接書き換えても構いません。

/create-test-cases コマンド

/clear 後もしくは新しい会話セッションで入力します。引数に、create-doc で生成したドキュメントファイルパスを渡します。

$ARGUMENTS ファイルの仕様が満たされていることを確認するために必要なテストケースを考え、ファイルに追加してください。

以下の手順に従ってください。

1. 仕様を読み、分析します。明確でない場合は、このコマンドの実行を中止してください。
2. テスト対象の public クラスとメソッドを、統合度の低い順に選択します。
3. カバレッジを考慮したテストケースを導出するために使用するテスト技法(同値分割法、境界値分析、状態遷移テストなど)を選択します。
4. テスト技法を使用して、自然言語でテストケースを作成します。以下の点に注意してください。
  - 連続した ID は作成しないでください。
  - 検証内容を記述します。検証できない場合はテストケースを削除します。
5. テスト対象のすべてのクラスがカバーされるまで、手順 2 から 4 を繰り返します。
6. $ARGUMENTS ファイルにテストケースを追加します。
7. Git にコミットします。

これで、ドキュメントにテストケースが追記されますので、レビューします。 検証しにくく、かつ価値の低いテストケースを挙げられることもあるので、この時点で刈り取っておきます*3

テストケースを見て仕様の不備に気づくことも多いので、その場合は仕様部分も同時に修正したり、問題が大きい場合は revert して前工程に戻ってもいいでしょう。

なお、手順2で結合度の低い順と指示しているのは、逆だと無駄にテストダブルを使ってテストを書こうとするためです。

/implement-code コマンド

/clear 後もしくは新しい会話セッションで入力します。引数に、create-doc で生成したドキュメントファイルパスを渡します。

$ARGUMENTS ファイルの仕様を満たすコードを実装してください。

以下の手順に従ってください。

1. 仕様にテストケースが含まれていない場合は、このコマンドの実行を中止してください。
2. 製品コードに対して、コンパイル可能な型とパブリックメソッドシグネチャのみを作成します。動作しなくても問題ありません。
3. ドキュメントに記載されているテストケースに基づいてテストコードを実装します。
4. 追加したテストを実行し、失敗することを確認します。
5. Git にコミットします。
6. 製品コードを実装します。
7. `mcp__jetbrains__open_file_in_editor` および `get_current_file_errors` ツールを使用して、重大度レベルが `error` の診断を解決します。
8. テストを実行し、すべて合格します。
9. Git にコミットします。
10. KISS および SOLID 原則を念頭に置いてリファクタリングし、合格するようにテストを再実行します。
11. 重大度レベルが `suggestion` 以上の診断を解決し、テストを再実行して合格を確認します。
12. `mcp__jetbrains__reformat_file` ツールを使用して、変更したファイルを再フォーマットします。
13. Git にコミットします。

注:
- テストの実行方法については、@.claude/commands/run-tests.md ファイルを参照してください。

ドキュメントの指示通りにテストファーストで実装が進められ、最終的にテストがパスし、Roslynアナライザも含めコンパイラの警告も解消したコードができあがります。

手順10でリファクタリングを指示していますがあまり期待はしておらず、たとえば CognitiveComplexity プラグインでコグニティブ複雑度の高いコードに警告が出るようにしておき、それを手順11で検出・修正させているのが重要です。

最低限、テストコードが適切な検証(アサーション)をしているかをレビューし、問題があれば修正します。 問題が大きい場合は revert して前工程に戻ってもいいでしょう。 動作に問題がなくなったら(今後の変更可能性の高い箇所なら特に)Tidy/リファクタリングまで行ないます。

なお、実はTDD版コマンドも作ってあるのですが使っていません。設計がほぼ固まっている、コンテキストウィンドウを圧迫するなどの理由でLLM向きではないと思っています。

/run-tests コマンド

これはワークフロー外ですが、コードを修正した後などにUnityエディター上でテストを実行するコマンドを定義しています。 必要に応じて個別に使います。

`UnityNaturalMCP` 経由で Unity エディターでテストを実行してください。

## 適切なツールの選択

変更したクラスの名前空間またはアセンブリ名に「Editor」が含まれている場合は、`mcp__UnityNaturalMCP__RunEditModeTests` ツールを使用してください。
変更したクラスの名前空間に「Editor」が含まれていない場合は、`mcp__UnityNaturalMCP__RunPlayModeTests` ツールを使用してください。

## フィルターの指定

実行されるテストの数を最小限に抑えるため、フィルターは以下の順序で決定されます。

1. **testNames**: 特定のテストのみが失敗している場合、または影響を受けるテストが限られた数のみである場合に指定します。
2. **groupNames**: 変更したクラスに対応するテストクラスを指定します。名前空間は変更したクラスと同じで、クラス名に「Test」が付加されます。
3. **assemblyNames**: 変更したクラスを含むアセンブリに対応するテストアセンブリの名前を指定します。アセンブリ名に「.Tests」を追加して指定してください。

## トラブルシューティング

ツールが接続エラーで失敗する場合は、以下の原因が考えられます。

- コンパイルなどによるドメインの再読み込みにより、接続が切断されている可能性があります。しばらく待ってから再試行してください。
- コンパイルエラーがある場合、Play Mode テストは実行できません。`mcp__jetbrains__open_file_in_editor` ツールと `get_current_file_errors` ツールを使用して、コンパイルエラーがないか確認してください。
- 実行時間が長いため、テストがタイムアウトしている可能性があります。フィルター設定を確認して実行するテストを絞り込むか、ユーザーにタイムアウト設定を長くするよう依頼してください。

環境

前後しますが、上記ワークフローを成立させている環境は次のとおりです。

Unityエディターでテストを実行するために UnityNaturalMCP パッケージを使用しています。Unityエディター向けMCPサーバはいくつか公開されていますが、テストを実行できるものは限られます。 前述の /run-tests コマンドではフィルターの指定などが UnityNaturalMCP に最適化されています。

また、Roslynアナライザなどの診断結果を取得するために、Riderに Claude Code プラグインもしくは JetBrainsの MCP Server プラグインをインストールします。

Claude Code の設定

紹介したコマンドファイルを含む Claude Code 設定ファイルを次のリポジトリに公開しています。

github.com

このまま使うのであれば、プロジェクトルートにサブモジュールとして追加します。

git submodule add git@github.com:nowsprinting/claude-code-settings-for-unity.git .claude

.mcp.json

MCP設定ファイルはプロジェクトルートに置く必要があるので、コピーするかシンボリックリンクを張ります。

ln -s .claude/.mcp.json .mcp.json

CLAUDE.md

最後に CLAUDE.md ファイルを作ります。リポジトリには含まれていません。

プロジェクト固有の情報はこのファイルに書き、コーディングガイドラインなど共通的なものは rules/ 下にあるファイルを読み込ませています。 Claude Codeがメモリとして読んでいるファイルは /status コマンドで確認できます。

# Development Guidelines

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

プロジェクト固有の情報

## 行動規範

Claude Code の行動規範、開発の進め方に関するグランドルールは、次のファイルを参照してください。

- @.claude/rules/00-code-of-conduct.md

## コーディングおよびテストのガイドライン

次のファイルを参照してください。

- @.claude/rules/01-coding.md
- @.claude/rules/02-testing.md

## Unityプロジェクトの構造およびガイドライン

次のファイルを参照してください。

- @.claude/rules/10-unity-project.md
- @.claude/rules/11-unity-documentation.md
- @.claude/rules/12-unity-yaml.md

~/.claude/CLAUDE.md(任意)

ユーザースコープのCLAUDE.mdでは、チャットの応答について指示しています。言葉の乱れはコンテキストウィンドウの乱れです。

# CLAUDE.md

## チャットの応答

チャットの応答は日本語で、ギャル口調で返してね☆

~/.claude/settings.json(任意)

ユーザースコープのsettings.jsonには、次の2項目だけ書いています。

  • permissions.deny: プロジェクトにsettings.jsonを入れずにclaude実行しちゃった対策でdenyだけ書いています。permissionsは丸ごとプロジェクトスコープで上書きされるようです(allowはリストアイテム単位でlocalがマージされるようですが)
  • hooks.Notification, Stop: 通知音を鳴らす設定を入れています。ただしhooksはファイルに書くだけでは有効化せず、/hooks コマンドで登録する必要があります

現状の課題

紹介したワークフローは完全とは言えず、いくつかの課題を残しています。解決策などあればぜひ教えて下さい。

  • テスト実行前のコンパイル確認がスルーされることがある(なぜかテストが実行できない… となる。そもそもUintyのエディタ拡張でMCPサーバを立てることの限界もある)
  • implement-code.md の手順11で重大度レベル suggestion 以上を指示していますが、現状では Claude Code プラグインide__getDiagnostics でも、JetBrains MCP Server プラグインget_current_file_errors でも、warning までしか拾うことはできません
  • テストの下手さ、保守性の低さは解決しきれておらず、まだ手作業でのTidy/リファクタリングが必要

参考

permissions.denyの設定はこちらの記事を参考にしています。

izanami.dev

宣伝

LLM はテストを書くのが下手くそなので、本書を読んでテストコードを書くスキルを身に着けておくことが役に立つはずです。 ikagoya.booth.pm

CEDEC 2025 で登壇します。開発者テストの話ですが、Agentic Coding にからめた話もする予定です。 cedec.cesa.or.jp

*1:使ったことのないライブラリに強く依存する、外向けのインタフェースを決めきれない等

*2:筆者はセッション自体を切ってしまうことが多いです。RiderのTerminalもクリアしたいため

*3:検証できない==アサーションの無いテストでコードカバレッジだけ上げるのは避けるべきです

Anjinに関する記事・リソースまとめ

株式会社ディー・エヌ・エー(以下DeNA)が公開しているオープンソースのUnity向けオートパイロットフレームワーク Anjin(あんじん)に関する記事・リソースのまとめです。随時更新予定。

github.com

DeNA公式

swet.dena.com

swet.dena.com

メディア

gamemakers.jp

カンファレンス

www.docswell.com

cedec.cesa.or.jp

cedec.cesa.or.jp

cedec.cesa.or.jp

cedec.cesa.or.jp

同人誌

booth.pm

ブログ記事

myudon.hatenablog.com

qiita.com

www.nowsprinting.com

technote.qualiarts.jp

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

依存ライブラリに関する記事

www.nowsprinting.com

www.nowsprinting.com