Writing Automated Unit Tests and Integration Tests
When you use DI to write low-coupling code correctly, it's very easy to isolate code in a given area. The basic intention is to run tests without starting your project; these test frameworks that can be derived from users or use NUnit automated testing; automated testing is especially helpful for continuous and server interaction. This allows you to run tests automatically whenever new submissions are made to version control.
There are three basic test help classes included in Zenject, which make it easier to write automated test games; the first is Unit Tests, the second is interactive testing, and the third is scenario testing; all methods run through Unity's built-in Test Runner (and a command line interface that allows you to keep kimonos running). The main difference is that the smaller scope of Unit Tests means the test subset classes. In applications, interactive testing can be more exaggerated and triggered from many different systems. Scenario testing is generally used to trigger entry into the scene and verify part of the scenario status function test.
Code example:
Unit Tests
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; } }
Test the class by following steps:
- Click Window - > General - > Test Runner to open Unity's Test Runner
- Under the EditMode tab, click Create EditMode Test Assembly Folder; this creates a folder containing the necessary asmdef files.
Need to access the NUnit namespace - Select the newly created asmdef file and add a Zenject-Test Framework reference
- Right-click the Project tab and select Create - > Zenject - > Unit Test; name it TestLogger.cs; create an empty template test class
- Copy the following code:
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)); } }
Run the test, open Unity's Test Runner, select Window - > Test Runner; then make sure the EditMode tab is selected, and then click Run All or right-click the test you want to specify.
As you can see from the above, this method is very basic and only interacts through the ZenjectUnitTestFixture class; all ZenjectUnitTestFixture runs to ensure that the new container is recreated before each test method call; here is the whole code:
public abstract class ZenjectUnitTestFixture { DiContainer _container; protected DiContainer Container { get { return _container; } } [SetUp] public virtual void Setup() { _container = new DiContainer(); } }
So typically, you can run the installer through the [SetUp] method and call Resolve <> directly to redirect the class you want to test.
You can also avoid all calls to Container.Resolve by injecting unit test itself and modifying the code after installation:
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)); } }
Intergration Tests
Interactive testing, on the other hand, executes in a similar environment to the scene in your project; unlike unit testing, interactive testing includes a SceneContext and a ProjectContext, any bindings to IInitializable, ITickable and IDisposable will be executed as if running your game; Running mode supported by Unity;
Use a simple example to test:
public class SpaceShip : MonoBehaviour { [InjectOptional] public Vector3 Velocity { get; set; } public void Update() { transform.position += Velocity * Time.deltaTime; } }
Test the class by:
- Click Window - > General - > Test Runner to open Unity's Test Runner
- Under the PlayMode tab, click Create PlayMode Test Assembly Folder; this creates a folder containing the necessary asmdef files.
Need to access the NUnit namespace - Select the newly created asmdef file and add a Zenject-Test Framework reference
- Right-click the Project tab and select Create - > Zenject - > Integration Test; name it SpaceShipTests.cs; create an empty template test class
- Template code:
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; } }
Let's write code for the 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); } }
All we're doing here is making sure that space ship moves to vectors in the same direction; if we have a lot of tests running on SpaceShip, modify the code:
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); } }
After the PostInstall() call, our interaction test has been injected, so we can define the [Inject] variable above if we don't want to call Container.Resolve for each test;
Note that we can wait for our collaboration to test behaviour passing time; if you are running test runner using Unity stably;
Each Zenject interaction test is interrupted by three phases:
- Before PreInstall - Establish initialization scenarios for your tests; this may include loading prefabricates from resource directories, creating new game objects, and so on.
- After PreInstall - Install bindings to containers to the tests you need
- After PostInstall - This point, all non-lazy objects we bind to the container have been initialized, all objects have been injected into the scene, all IInitializable.Initialize methods have been invoked; so we can start adding assertions to test state, manipulate object state at runtime, and so on. You'll need to wait for the MonobeBehaviour method you want to start working properly once PostInstall is done.
Scene Tests
Scenario testing actually accompanies testing by running scenarios, then accessing containers that rely on scenarios to pass through scenarios, and then making changes to or verifying specific states; using the SpaceFighter game as an example, we may want to ensure that our enemy's simple AI specifies running:
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); } }
Note that you may add your own scenario tests, select Create - > Zenject - > Scene Test by right-clicking the menu in the project label, and note that they will request an asmdef file to create a similar way to interact with the interactive tests described above.
Each scenario test should inherit from SceneTest Fixture and then test each other at some point.
yield return LoadScene(NameOfScene) should be called
Sometimes we need to configure some settings to inject into our scenario before calling LoadScene; we can do this by adding bindings to StaticContext;
StaticContext is the parent of ProjectContext and inherits from all dependencies; in this case, we want to configure the EnemySpawner class to produce only one enemy, because that's the simple test we need;
Note that if there is any error log output, our test will fail, or any exception will be thrown during the execution of the test method;
Scenario testing is very helpful in combination with continuous interactive server testing; even as a simple test, the following tests are very effective.
Make sure that each scenario starts correctly:
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); } }
Note that the name of the scenario must have been added to the compilation settings beforehand.
If you want to test multiple scenarios loading at once, you can do this 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); } }
In this case, your SceneTestFixture derived class will be injected into the recently loaded SceneContext container, and the SceneContainer property will also be set; if you want to access other scenario containers, you can also use the SceneContainers property.
Note that if you perform a fairly long test, you may need to increase the timeout value by default of 30s timeout
public class LongTestExample : SceneTestFixture { [UnityTest] [Timeout(60000)] public IEnumerator ExecuteSoakTest() { ... } }
User Driven Test Beds
The fourth common test method is user-driven test bed. This only involves creating a new scenario using SceneContext and so on, just like a production scenario, except that only a portion of the bindings that are usually included in the production scenario are installed and may simulate parts that you don't need to test. Then, by iterating over the system that you are using the test bed, you can make faster progress without having to start a normal production scenario.
If the functionality you want to test is too complex for unit or integration testing, you may also need to do so.
The only drawback of this approach is that it's not automated and requires people to run it - so you can't run these tests as part of a continuous integration server
Summarize test cases
Unit Test Case Writing Process
- Create asmdef files and corresponding folders through creation initialization in EditMode of Test Runner
- Create Unit Test Template Classes through Zenject in the Project
- [TestFixture] class declares [Setup] setup environment [Test] test methods (use cases)
- Unit testing through Test Runner, Run All or Run Selected
General test scope for unit testing
A single function point test can be executed when there are abnormal data in the project, such as configuration table test, which can be tested without starting the game.
Interactive Test Case Writing Process
- Create asmdef files and corresponding folders through creation initialization in PlayMode of Test Runner
- Create Integration Test Template Classes through Zenject in the Project
- PreInstall() Prepares to enter the test environment PostInstall() Sets up the environment and completes [UnityTest] test methods (use cases)
- Interactive testing through Test Runner, Run All or Run Selected
General test scope for interactive testing
When the game logic needs to be tested
Scenario Test Case Writing Process
- Create asmdef files and corresponding folders through creation initialization in PlayMode of Test Runner
- Create Scene Test Template Classes through Zenject in the Project
- [UnityTest] test methods (use cases)
- Interactive testing through Test Runner, Run All or Run Selected
Scenario testing general test scope
When a lot of logical decisions are needed
summary
Unit Test Action Points, Interaction Test Action Lines, Scenario Test Action Surfaces
Only unit testing is universal, interactive testing and scenario testing require good game building decoupling
Community building is in progress
Zenject Github address
Zenject Chinese Community in Construction
Discussion Groups: 518658723
Welcome friends who like to use DI to join the group, there are voluntary registration activities!
My code foundation is shallow, God saw when passing, I hope to comment!
Aha, a friend with ability can scan the following two-dimensional code__