Skip to content

Instantly share code, notes, and snippets.

@naninunenoy
Last active October 2, 2019 10:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naninunenoy/6f68709ec20fadbd865e42e780fe8d16 to your computer and use it in GitHub Desktop.
Save naninunenoy/6f68709ec20fadbd865e42e780fe8d16 to your computer and use it in GitHub Desktop.
Zenject/WritingAutomatedTests.md(Google翻訳)

original

Zenject/WritingAutomatedTests.md at master · modesttree/Zenject · GitHub

自動化された単体テストと統合テストの作成

Writing Automated Unit Tests and Integration Tests

依存性注入を使用して適切に疎結合されたコードを記述する場合、プロジェクト全体を起動する必要なく、テストを実行する目的でコードベースの特定の領域を分離する方がはるかに簡単です。これは、ユーザー主導のテストベッドまたはNUnitを使用した完全自動テストの形式をとることができます。自動テストは、継続的インテグレーションサーバーで使用する場合に特に役立ちます。これにより、新しいコミットがソース管理にプッシュされるたびにテストを自動的に実行できます。

When writing properly loosely coupled code using dependency injection, it is much easier to isolate specific areas of your code base for the purposes of running tests on them without needing to fire up your entire project. This can take the form of user-driven test-beds or fully automated tests using NUnit. Automated tests are especially useful when used with a continuous integration server. This allows you to automatically run the tests whenever new commits are pushed to source control.

Zenjectには3つの非常に基本的なヘルパークラスが含まれており、ゲームの自動テストを簡単に作成できます。 1つは単体テスト用、もう1つは統合テスト用、3つ目はシーンテスト用です。すべてのアプローチは、Unityに組み込まれているテストランナー(継続的な統合サーバーに接続できるコマンドラインインターフェイスも備えています)を介して実行されます。主な違いは、単体テストのスコープがはるかに小さく、アプリケーションのクラスの小さなサブセットをテストすることを意図しているのに対し、統合テストはより拡張性が高く、多くの異なるシステムの起動を伴うことができることです。また、シーンテストは、シーン全体を起動し、テストの一部としてシーンの状態を調べるために使用されます。

There are three very basic helper classes included with Zenject that can make it easier to write automated tests for your game. One is for Unit Tests, the other is for Integration Tests, and the third is for Scene Tests. All approaches are run via Unity's built in Test Runner (which also has a command line interface that you can hook up to a continuous integration server). The main differences are that Unit Tests are much smaller in scope and meant for testing a small subset of the classes in your application, whereas Integration Tests can be more expansive and can involve firing up many different systems. And Scene Tests are used to fire up entire scenes and then probe the state of the scene as part of the test.

これはいくつかの例で最もよく示されます。

This is best shown with some examples.

単体テスト

Unit Tests

例として、Unityプロジェクトに次のクラスを追加しましょう。

As an example, let's add the following class to our Unity project:

using System;

public class Logger
{
    public Logger()
    {
        Log = "";
    }

    public string Log
    {
        get;
        private set;
    }

    public void Write(string value)
    {
        if (value == null)
        {
            throw new ArgumentException();
        }

        Log += value;
    }
}

クラスをテストするには、次を実行します。

  • [ウィンドウ]-> [一般]-> [テストランナー]をクリックして、Unityのテストランナーを開きます。
  • [EditMode]タブの下で、[Create EditMode Test Assembly Folder]をクリックします。これにより、Nunit名前空間にアクセスするために必要なasmdefファイルを含むフォルダーが作成されます。
  • 新しく作成されたasmdefファイルを選択し、Zenject-TestFrameworkへの参照を追加します
  • [プロジェクト]タブでフォルダー内を右クリックし、[作成-> Zenject->ユニットテスト]を選択します。 TestLogger.csという名前を付けます。これにより、テストで入力できる基本的なテンプレートが作成されます
  • 次をコピーして貼り付けます:

To test the class do the following:

  • Open up Unity's Test Runner by clicking Window -> General -> Test Runner
  • Underneath the EditMode tab click "Create EditMode Test Assembly Folder". This will create a folder that contains the necessary asmdef file that is needed to get access to the Nunit namespace.
  • Select the newly created asmdef file and add a reference to Zenject-TestFramework
  • Right click inside the folder in the Project tab and select Create -> Zenject -> Unit Test. Name it TestLogger.cs. This will create a basic template that we can fill in with our tests
  • Copy and paste the following:
using System;
using Zenject;
using NUnit.Framework;

[TestFixture]
public class TestLogger : ZenjectUnitTestFixture
{
    [SetUp]
    public void CommonInstall()
    {
        Container.Bind<Logger>().AsSingle();
    }

    [Test]
    public void TestInitialValues()
    {
        var logger = Container.Resolve<Logger>();

        Assert.That(logger.Log == "");
    }

    [Test]
    public void TestFirstEntry()
    {
        var logger = Container.Resolve<Logger>();

        logger.Write("foo");
        Assert.That(logger.Log == "foo");
    }

    [Test]
    public void TestAppend()
    {
        var logger = Container.Resolve<Logger>();

        logger.Write("foo");
        logger.Write("bar");

        Assert.That(logger.Log == "foobar");
    }

    [Test]
    public void TestNullValue()
    {
        var logger = Container.Resolve<Logger>();

        Assert.Throws(() => logger.Write(null));
    }
}

実行するには、ウィンドウ->テストランナーを選択して、Unityのテストランナーを開きます。次に、[EditMode]タブが選択されていることを確認してから、[すべて実行]をクリックするか、実行する特定のテストを右クリックします。

To run it, open up Unity's test runner by selecting Window -> Test Runner. Then make sure the EditMode tab is selected, then click Run All or right click on the specific test you want to run.

上記でわかるように、このアプローチは非常に基本的であり、 ZenjectUnitTestFixtureクラスから継承するだけです。 ZenjectUnitTestFixtureは、各テストメソッドが呼び出される前に新しいコンテナが再作成されることを確認します。それでおしまい。これはそのためのコード全体です:

As you can see above, this approach is very basic and just involves inheriting from the ZenjectUnitTestFixture class. All ZenjectUnitTestFixture does is ensure that a new Container is re-created before each test method is called. That's it. This is the entire code for it:

public abstract class ZenjectUnitTestFixture
{
    DiContainer _container;

    protected DiContainer Container
    {
        get
        {
            return _container;
        }
    }

    [SetUp]
    public virtual void Setup()
    {
        _container = new DiContainer();
    }
}

そのため、通常は[SetUp]メソッド内からインストーラーを実行し、直接Resolve<>を呼び出して、テストするクラスのインスタンスを取得します。

So typically you run installers from within [SetUp] methods and then directly call Resolve<> to retrieve instances of the classes you want to test.

ユニットテストを次のように変更することで、インストールの完了後にユニットテスト自体に注入することで、 Container.Resolveのすべての呼び出しを回避することもできます。

You could also avoid all the calls to Container.Resolve by injecting into the unit test itself after finishing the install, by changing your unit test to this:

using System;
using Zenject;
using NUnit.Framework;

[TestFixture]
public class TestLogger : ZenjectUnitTestFixture
{
    [SetUp]
    public void CommonInstall()
    {
        Container.Bind<Logger>().AsSingle();
        Container.Inject(this);
    }

    [Inject]
    Logger _logger;

    [Test]
    public void TestInitialValues()
    {
        Assert.That(_logger.Log == "");
    }

    [Test]
    public void TestFirstEntry()
    {
        _logger.Write("foo");
        Assert.That(_logger.Log == "foo");
    }

    [Test]
    public void TestAppend()
    {
        _logger.Write("foo");
        _logger.Write("bar");

        Assert.That(_logger.Log == "foobar");
    }

    [Test]
    public void TestNullValue()
    {
        Assert.Throws(() => _logger.Write(null));
    }
}

統合テスト

Integration Tests

一方、統合テストは、プロジェクトのシーンと同様の環境で実行されます。単体テストとは異なり、統合テストにはSceneContextProjectContextが含まれ、IInitializable、ITickable、およびIDisposableへのバインディングは、ゲームを通常実行するときと同じように実行されます。 Unityのプレイモードテストのサポートを使用してこれを実現します。

Integration tests, on the other hand, are executed in a similar environment to the scenes in your project. Unlike unit tests, integration tests involve a SceneContext and ProjectContext, and any bindings to IInitializable, ITickable, and IDisposable will be executed just like when running your game normally. It achieves this by using Unity's support for 'playmode tests'.

非常に簡単な例として、テストしたい次のクラスがあるとしましょう:

As a very simple example, let's say we have the following class we want to test:

public class SpaceShip : MonoBehaviour
{
    [InjectOptional]
    public Vector3 Velocity
    {
        get; set;
    }

    public void Update()
    {
        transform.position += Velocity * Time.deltaTime;
    }
}

クラスをテストするには、次を実行します。

  • [ウィンドウ]-> [一般]-> [テストランナー]をクリックして、Unityのテストランナーを開きます。
  • [PlayMode]タブの下で、[Create PlayMode Test Assembly Folder]をクリックします。これにより、Nunit名前空間にアクセスするために必要なasmdefファイルを含むフォルダーが作成されます。
  • 新しく作成されたasmdefファイルを選択し、Zenject-TestFrameworkへの参照を追加します
  • [プロジェクト]タブでフォルダー内を右クリックし、[作成-> Zenject->統合テスト]を選択します。 SpaceShipTests.csという名前を付けます。これにより、テストで入力できる基本的なテンプレートが作成されます
  • これにより、テストの作成を開始するために必要なすべてのものを含む次のテンプレートコードが作成されます。

To test the class do the following:

  • Open up Unity's Test Runner by clicking Window -> General -> Test Runner
  • Underneath the PlayMode tab click "Create PlayMode Test Assembly Folder". This will create a folder that contains the necessary asmdef file that is needed to get access to the Nunit namespace.
  • Select the newly created asmdef file and add a reference to Zenject-TestFramework
  • Right click inside the folder in the Project tab and select Create -> Zenject -> Integration Test. Name it SpaceShipTests.cs. This will create a basic template that we can fill in with our tests
  • This will create the following template code with everything you need to start writing your test:
public class SpaceShipTests : ZenjectIntegrationTestFixture
{
    [UnityTest]
    public IEnumerator RunTest1()
    {
        // Setup initial state by creating game objects from scratch, loading prefabs/scenes, etc

        PreInstall();

        // Call Container.Bind methods

        PostInstall();

        // Add test assertions for expected state
        // Using Container.Resolve or [Inject] fields
        yield break;
    }
}

SpaceShipクラスのテストコードを入力してみましょう。

Let's fill in some test code for our SpaceShip class:

public class SpaceShipTests : ZenjectIntegrationTestFixture
{
    [UnityTest]
    public IEnumerator TestVelocity()
    {
        PreInstall();

        Container.Bind<SpaceShip>().FromNewComponentOnNewGameObject()
            .AsSingle().WithArguments(new Vector3(1, 0, 0));

        PostInstall();

        var spaceShip = Container.Resolve<SpaceShip>();

        Assert.IsEqual(spaceShip.transform.position, Vector3.zero);

        yield return null;

        // Should move in the direction of the velocity
        Assert.That(spaceShip.transform.position.x > 0);
    }
}

ここで行っているのは、宇宙船がその速度と同じ方向に移動するようにすることです。 SpaceShipで実行するテストが多数ある場合は、これを次のように変更することもできます。

All we're doing here is ensuring that the space ship moves in the same direction as its velocity. If we had many tests to run on SpaceShip we could also change it to this:

public class SpaceShipTests : ZenjectIntegrationTestFixture
{
    void CommonInstall()
    {
        PreInstall();

        Container.Bind<SpaceShip>().FromNewComponentOnNewGameObject()
            .AsSingle().WithArguments(new Vector3(1, 0, 0));

        PostInstall();
    }

    [Inject]
    SpaceShip _spaceship;

    [UnityTest]
    public IEnumerator TestInitialState()
    {
        CommonInstall();

        Assert.IsEqual(_spaceship.transform.position, Vector3.zero);
        Assert.IsEqual(_spaceship.Velocity, new Vector3(1, 0, 0));
        yield break;
    }

    [UnityTest]
    public IEnumerator TestVelocity()
    {
        CommonInstall();

        // Wait one frame to allow update logic for SpaceShip to run
        yield return null;

        // Should move in the direction of the velocity
        Assert.That(_spaceship.transform.position.x > 0);
    }
}

PostInstall()が呼び出された後、統合テストが挿入されるため、すべてのテストで Container.Resolveを呼び出したくない場合は、上記のように[Inject]フィールドを定義できます。

After PostInstall() is called, our integration test is injected, so we can define [Inject] fields on it like above if we don't want to call Container.Resolve for every test.

コルーチンを生成して、時間をかけて動作をテストできることに注意してください。 Unityのテストランナーの動作方法(特にプレイモードテストの動作方法)に慣れていない場合は、ユニティドキュメントを参照してください。

Note that we can yield our coroutine to test behaviour across time. If you are unfamiliar with how Unity's test runner works (and in particular how 'playmode test' work) please see the unity documentation.

すべてのzenject統合テストは、3つのフェーズに分けられます。

  • PreInstallの前に-テストの初期シーンを設定します。これには、Resourcesディレクトリからプレハブをロードしたり、新しいGameObjectをゼロから作成したりすることが含まれます。
  • プレインストール後-テストに必要なすべてのバインディングをコンテナにインストールします
  • PostInstall後-この時点で、コンテナにバインドしたすべての非遅延オブジェクトがインスタンス化され、シーン内のすべてのオブジェクトが注入され、すべてのIInitializable.Initializeメソッドが呼び出されました。したがって、Assertの追加を開始して、状態のテスト、オブジェクトの実行時状態の操作などを行うことができます。MonoBehaviour開始メソッドも実行する場合は、PostInstallの直後に1回譲る必要があることに注意してください。

Every zenject integration test is broken up into three phases:

  • Before PreInstall - Set up the initial scene how you want for your test. This could involve loading prefabs from the Resources directory, creating new GameObject's from scratch, etc.
  • After PreInstall - Install all the bindings to the Container that you need for your test
  • After PostInstall - At this point, all the non-lazy objects that we've bound to the container have been instantiated, all objects in the scene have been injected, and all IInitializable.Initialize methods have been called. So we can now start adding Assert's to test the state, manipulate the runtime state of the objects, etc. Note that you will have to yield once immediately after PostInstall if you want the MonoBehaviour start methods to run as well.

シーンテスト

Scene Tests

シーンテストは、実際にテストと一緒にシーンを実行し、シーンDiContainerを介してシーン内の依存関係にアクセスし、それを変更するか、単に予想される状態を確認することで機能します。付属のSpaceFighterゲームを例に取るために、敵船のシンプルなAIが期待どおりに実行されるようにする必要があります。

Scene tests work by actually running the scene alongside the test, then accessing dependencies in the scene through the scene DiContainer, then making changes to it or simply verifying expected state. To take the included SpaceFighter game as an example, we might want to ensure that our simple AI for the enemy ships runs as expected:

public class SpaceFighterTests : SceneTestFixture
{
    [UnityTest]
    public IEnumerator TestEnemyStateChanges()
    {
        // Override settings to only spawn one enemy to test
        StaticContext.Container.BindInstance(
            new EnemySpawner.Settings()
            {
                SpeedMin = 50,
                SpeedMax = 50,
                AccuracyMin = 1,
                AccuracyMax = 1,
                NumEnemiesIncreaseRate = 0,
                NumEnemiesStartAmount = 1,
            });

        yield return LoadScene("SpaceFighter");

        var enemy = SceneContainer.Resolve<EnemyRegistry>().Enemies.Single();

        // Should always start by chasing the player
        Assert.IsEqual(enemy.State, EnemyStates.Follow);

        // Wait a frame for AI logic to run
        yield return null;

        // Our player mock is always at position zero, so if we move the enemy there then the enemy
        // should immediately go into attack mode
        enemy.Position = Vector3.zero;

        // Wait a frame for AI logic to run
        yield return null;

        Assert.IsEqual(enemy.State, EnemyStates.Attack);

        enemy.Position = new Vector3(100, 100, 0);

        // Wait a frame for AI logic to run
        yield return null;

        // The enemy is very far away now, so it should return to searching for the player
        Assert.IsEqual(enemy.State, EnemyStates.Follow);
    }
}

[作成]-> [Zenject]-> [シーンテスト]を選択して、[プロジェクト]タブの右クリックメニューから独自のシーンテストを追加できることに注意してください。上記の統合テストと同様の方法で設定されたasmdefファイルが必要になることに注意してください。

Note that you can add your own scene tests through the right click menu in the Projects tab by choosing Create -> Zenject -> Scene Test. Note that they will require an asmdef file set up in a similar way to integration tests as described above.

すべてのシーンテストはSceneTestFixtureから継承し、各テストメソッドのある時点で、 yield return LoadScene(NameOfScene)を呼び出す必要があります。

Every scene test should inherit from SceneTestFixture, and then at some point in each test method it should call yield return LoadScene(NameOfScene)

LoadSceneを呼び出す前に、シーンに挿入される設定を構成すると便利な場合があります。 StaticContextにバインディングを追加することでこれを行うことができます。 StaticContextはProjectContextの親コンテキストであるため、すべての依存関係によって継承されます。この場合、単純なテストに必要なのは1つだけなので、EnemySpawnerクラスを構成して、1人の敵のみをスポーンします。

Before calling LoadScene it is sometimes useful to configure some settings that will get injected into our scene. We can do this by adding bindings to the StaticContext. StaticContext is the parent context of ProjectContext, and so will be inherited by all dependencies. In this case, we want to configure the EnemySpawner class to only spawn one enemy because that's all we need for our simple test.

テストメソッドの実行中にエラーがコンソールに記録された場合、または例外がスローされた場合、テストは失敗することに注意してください。

Note that the test will fail if any errors are logged to the console during the execution of the test method, or if any exceptions are thrown.

シーンテストは、継続的統合テストサーバーと組み合わせると特に便利です。次のような簡単なテストでさえ、各シーンがエラーなしで開始することを保証するために非常に貴重です。

Scene tests can be particularly useful when combined with a continuous integration test server. Even tests as simple as the following can be invaluable to ensure that each scene starts without errors:

public class TestSceneStartup : SceneTestFixture
{
    [UnityTest]
    public IEnumerator TestSpaceFighter()
    {
        yield return LoadScene("SpaceFighter");

        // Wait a few seconds to ensure the scene starts correctly
        yield return new WaitForSeconds(2.0f);
    }

    [UnityTest]
    public IEnumerator TestAsteroids()
    {
        yield return LoadScene("Asteroids");

        // Wait a few seconds to ensure the scene starts correctly
        yield return new WaitForSeconds(2.0f);
    }
}

LoadSceneメソッドに渡すシーン名をビルド設定に追加して適切にロードする必要があることに注意してください

Note that the scene name that you pass to the LoadScene method must be added to the build settings for it to be loaded properly

一度にロードされる複数のシーンをテストする場合は、次のようにLoadSceneの代わりにLoadScenesを使用して、それも実行できます。

If you want to test multiple scenes being loaded at once, you can do that too, by using LoadScenes instead of LoadScene, like this:

public class TestSceneStartup : SceneTestFixture
{
    [UnityTest]
    public IEnumerator TestSpaceFighter()
    {
        yield return LoadScenes("SpaceFighterMenu", "SpaceFighterEnvironment");

        // Wait a few seconds to ensure the scene starts correctly
        yield return new WaitForSeconds(2.0f);
    }
}

この場合、SceneContainerプロパティにも設定される、最後にロードされたSceneContextコンテナをSceneTestFixture派生クラスに注入します。他のシーンコンテナにアクセスする場合は、SceneContainersプロパティを使用してアクセスすることもできます。

In this case, it will inject your SceneTestFixture derived class with the last loaded SceneContext container which will also be set to the SceneContainer property. If you want to access the other scene containers you can do that too using the SceneContainers property.

特に長いテストを実行している場合、デフォルトで30秒に設定されているタイムアウト値を増やす必要があることに注意してください。例えば:

Note that if you are executing a particularly long test, you might have to increase the timeout value which defaults to 30 seconds. For example:

public class LongTestExample : SceneTestFixture
{
    [UnityTest]
    [Timeout(60000)]
    public IEnumerator ExecuteSoakTest()
    {
        ...
    }
}

ユーザー駆動テストベッド

User Driven Test Beds

言及する価値のあるテストへの4番目の一般的なアプローチは、ユーザードリブンテストベッドです。プロダクションシーンの場合と同様に、SceneContextなどを使用して新しいシーンを作成するだけです。ただし、プロダクションシーンに通常含めるバインディングのサブセットのみをインストールし、場合によっては不要な特定の部分をモックアウトします。テストする。次に、このテストベッドを使用して作業しているシステムを反復処理することにより、通常の運用シーンを起動する必要がなく、進行を速くすることができます。

A fourth common approach to testing worth mentioning is User Driven Test Beds. This just involves creating a new scene with a SceneContext etc. just as you do for production scenes, except installing only a subset of the bindings that you would normally include in the production scenes, and possibly mocking out certain parts that you don't need to test. Then, by iterating on the system you are working on using this test bed, it can be much faster to make progress rather than needing to fire up your normal production scene.

これは、テストする機能が単体テストまたは統合テストにとって複雑すぎる場合にも必要になることがあります。

This might also be necessary if the functionality you want to test is too complex for a unit test or an integration test.

このアプローチの唯一の欠点は、自動化されておらず、実行に人間が必要なことです。したがって、これらのテストを継続的統合サーバーの一部として実行することはできません。

The only drawback with this approach is that it isn't automated and requires a human to run - so you can't have these tests run as part of a continuous integration server

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment