Writing high-quality unit tests for unity3d using NUnit

Time:2022-4-13

0x00 unit test pro & con

Recently, I tried to introduce TDD (Test Driven Development) development mode into the game project I participated in, so unit testing has become very necessary. This blog will talk about the feelings and thoughts of this period of time. Due to the differences between game development and traditional software development, writing unit tests in the process of developing games, especially using unity3d, often faces two main problems:

  1. Game development will involve a lot of I / O operation processing, as well as visual and UI processing, and this part is difficult to deal with in unit testing.

  2. When it comes to developing games using unity3d, we naturally hope to integrate the testing framework into the editor of unity3d, which is easier to operate.

However, there are many benefits of unit testing.

  1. TDD, test driven development. Writing unit tests will enable us to observe and think from the caller. In particular, writing tests first forces us to design programs that are easy to call and testable, that is, it forces us to decouple the software. You can reduce the granularity of tasks. Of course, whether TDD is suitable for game development is debated, but the necessity of unit testing is beyond doubt.

  2. Unit testing is an invaluable document that is the best document to show how a method or class is used. This document is compiled and runnable, and it remains up-to-date and always in sync with the code.

  3. More suitable for dealing with frequent changes in requirements. One thing that practitioners in the game development industry can’t deny is that demand change is an inevitable or even essential thing in game development, and another advantage of unit testing is that once a bug occurs due to demand change, it can quickly find and solve the problem.

0x01 test tools commonly used in unity3d

For problem 1, it is difficult to implement unit test for I / O processing and UI visual operation, so the main object of our unit test is the logical operation and data access.

For question 2, unity5 3. X has integrated the test module in the editor. The test module relies onNUnitNUnit is a unit testing framework written specifically for. Net. In fact, JUnit (Java) and CPPUnit (c + +) are all members of xUnit. It was originally from JUnit 4.3d).
The reason why testing is implemented in unity editor rather than in IDE is that some unity APIs need to be run in unity environment and cannot be implemented directly in external IDE, such as instantiating GameObject.

And besides unity5 In addition to the unit test module of 3. X, unity officially launched a test plug-in unity test tool (based onNSubstitute), in addition to unit testing, it also includes:

  1. unit testing

  2. integration testing

  3. Assertion component

It should be noted that unity test tool is based on nsubstitute.

0x02 initial unit test

Since the topic of this article is unit testing, we must first define unit testing:

A unit test is a piece of automated code that calls the unit of work being tested and then tests some assumptions about the single final result of the unit. The unit test is written using the unit test framework, and the unit test is required to be reliable, readable and maintainable. As long as the product code does not change, the result of unit test is stable.

Now that we have the definition of unit test, let’s try to write unit test in unity project.

A small example of unit testing

When writing unit test cases, the unit test module of unity editor is mainly used, so the unit test is based on NUnit framework.
With NUnit, we can:

  1. Write structured tests.

  2. Automatically execute selected or all unit tests.

  3. View the results of the test run.

Therefore, this requires that when writing the unit test of unity3d project, we should introduceNUnit.FrameworkNamespace, and the [testfixture] attribute should be added to the unit test class, the [test] attribute should be added to the unit test method, and the file of the test case should be placed in the editor folder.
Here is an example:

using UnityEngine;
using System.Collections;
using NUnit.Framework;

[TestFixture]
public class HpCompTests
{
  //Test whether the damage value after being attacked is equal to the expected value
  [Test]
  public void TakeDamage_BeAttacked_HpEqual()
{
  //Arrange
      HpComp health = new HpComp();
      health.currentHp = 100;
     //Act
      health.TakeDamage(50);
     //Assert
      Assert.AreEqual(50f, health.currentHp);
    }
}

This example is to test whether the hero’s health is equal to the expected after being hurt.

The test framework will create the test case class and call takedamage_ BeAttacked_ Hpequal method to interact with it, and finally use NUnit’s assert class to assert whether it passes the test.

0x03 structure of unit test

Through the above small example, we can find that unit testing is actually structured. Let’s analyze it in detail:

Use the features provided by NUnit to identify the test code

NUnit uses c#’s feature mechanism to identify and load tests. These features are like bookmarks to help the test framework identify which parts of the test to call.
If we want to use the features of NUnit, we need to introduce them in the test code firstNUnit.FrameworkNamespace.

The NUnit runner needs at least two features to know what to run.

  1. [testfixture]: identifies a class that automates NUnit testing.

  2. [test]: it can be added to a method to identify that the method is an automated test that needs to be called.

Of course, there are other features for us to use to facilitate our better control of test code. For example, the [category] feature can classify tests, and the [ignore] feature can ignore tests.

Common NUnit attributes are shown in the following table:

[SetUp] 
[TearDown]  
[TestFixture] 
[Test] 
[TestCase] 
[Category] 
[Ignore] 

Test naming and layout standards

  • Naming of test class:Create a class named [classname] tests corresponding to a class in the tested project.

  • Naming of work unit:For each work unit (test), the method name of the test method consists of three parts and is named according to the following rules: [name of the method to be tested]_ [test assumptions]_ [expectation of test method].

Specifically:

  1. Name of the method being tested

  2. Test the assumptions, such as “login failed”, “invalid user”, “correct password”.

  3. Expectation of test method: our expectation of the behavior of the tested method under the conditions specified in the test scenario.

Among them, there are three possible results expected from the test method:

  1. Returns a value (numeric, Boolean, and so on).

  2. The state of a system being tested.

  3. Call a third-party system.

It can be seen that the format of our test code is different from the standard code. The test name can be very long, but readability is one of the most important aspects when writing test code, and the underline in the test name can prevent us from missing all important information. We can even read the test method name as a sentence, which will make the test goal of this test method The scenario and expectations are very clear and no additional comments are required.

Behavior of test unit – 3A principle

With the NUnit attribute, you can identify the test code that can be run automatically and some naming rules of the test code. Let’s take a look at how to test your own code.
A unit test usually contains three behaviors, which can be summarized into 3A principles, namely:

  1. Arrange: prepare objects, create objects and make necessary settings

  2. Act: operands

  3. Assert: assert that something is expected

The following is the previous simple code, including the above NUnit attributes, naming conventions and behaviors under the 3A principle. The assert part uses the assert class provided by the NUnit framework. The tested class is hpcomp and the tested method is takedamage.

using NUnit.Framework;

[TestFixture]
public class HpCompTests
{
  //Test whether the damage value after being attacked is equal to the expected value
  [Test]
  public void TakeDamage_BeAttacked_HpEqual()
{
  //Arrange
      HpComp health = new HpComp();
      health.currentHp = 100;
     //Act
      health.TakeDamage(50);
     //Assert
      Assert.AreEqual(50f, health.currentHp);
    }
}

Assertion of unit test — assert class

The NUnit framework provides an assert class to handle the related functions of assertions. The asset class is used to declare that a specific assumption should be true. Therefore, if the parameters passed to the asset class are different from the values we assert (expect), the NUnit framework will think that the test fails.

The assert class will provide some static methods for us to use.

For example:

Assert. Areequal (expected value, actual value);
Assert.AreEqual(1,2 - 1);

For the static method of assert class, you can see it directly in the code.

0x04 reliability of unit test

Our goal is to write reliable, maintainable and readable tests.

Therefore, in addition to following the unit test structure specification to write unit tests, we also need to pay attention to reliability, maintainability and readability. Therefore, we also need to pay attention to some principles.

Not easy to delete and modify tests

Once the tests are written and passed, they should not be easily modified or deleted. Because these tests are the umbrella of the corresponding system code, when modifying the system code, these tests will tell us whether the modified code will destroy the existing functions.

Try to avoid logic in the test

With the increase of logic in the test, the probability of defects in the test code will also increase. And because we often believe that the test is reliable, once the test has defects, we often don’t first consider the problem of the test, which may waste time to modify the system code. In unit testing, it is best to keep the logic simple, so try to avoid using the following logic control code.

  1. switch、if

  2. foreach、for、while

A unit test should be a series of method calls and assertions, but should not contain control flow statements.

Test only one focus

Verifying multiple concerns in a unit test can complicate the test code, but it is of no value. Instead, we should verify redundant concerns in separate, independent units so that we can find what really leads to failure.

0x05 maintainability of unit test

Remove duplicate codes

Like the repeated code in the system, repeated code in unit test also means that more test code should be modified when some aspect of the test object changes.

If the tests look the same, but the parameters are different, then we can use parametric testing[TestCase]Properties pass different data into the test method as parameters.

Implement test isolation

The so-called test isolation refers to the isolation of one test from other tests, even without knowing the existence of other tests, and only runs in its own small world.

The purpose of test isolation is to prevent the interaction between tests. The common interaction between tests can be summarized as follows:

  1. Mandatory test order: the test should be executed in a certain order, and the latter test needs the previous test results. This situation may lead to problems because NUnit cannot guarantee that the test will be executed in a certain order, so the test passed today may not be easy to use tomorrow

  2. Hidden test calls: test calls other tests

  3. The shared state is broken: the test needs to share the state, but the state is not reset after a test is completed, which affects the subsequent tests

0x06 readability of unit test

As mentioned in the overview, unit testing is an invaluable document that is the best document to show how methods or classes are used. Therefore, the importance of readability can be seen. Imagine that even after a few months, other programmers can understand the composition and use of a system through unit testing, and can quickly understand what they want to do and where to cut in.

Unit test naming

There have been requirements and introductions in the structure of unit testing. Refer to that section.

Variable naming in unit test

By naming variables reasonably, the readability can be improved, so that the reading test personnel can understand the content you want to verify as soon as possible.

Let’s look at the example above

[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
//Arrange
    HpComp health = new HpComp();
    health.currentHp = 100;
   //Act
    health.TakeDamage(50);
   //Assert
    Assert.AreEqual(50f, health.currentHp);
}

The assertion in this code uses a magic number 50, but this number does not use a descriptive name, so we can’t know what this number expects as soon as possible. Therefore, as far as possible, we should not directly use numbers to compare with the results, but use a meaningful named variable to compare with the results.

[Test]
public void TakeDamage_BeAttacked_HpEqual()
{
    HpComp health = new HpComp();
    health.currentHp = 100;

    health.TakeDamage(50);

    float leftHp = 50f;

    Assert.AreEqual(leftHp, health.currentHp);
}

0x07 write unit test in untiy editor

When writing unit test cases in the unity editor, the unit test module of the unity editor is mainly used, so the unit test is based on the NUnit framework.

This requires the introduction of NUnit. Com when writing unit tests Framework namespace, and the [testfixture] attribute should be added to the unit test class, the [test] attribute should be added to the unit test method, and the test case file should be placed in the editor folder.

The writing structure of test cases should follow the principle of 3a, that is, array, act and asset. That is, first set the test environment, such as instantiating the test class and assigning values to the fields of the test class. Then write the behavior of the test, and finally judge whether it passes the test.

Here is an example:

using UnityEngine;
using System.Collections;
using NUnit.Framework;

[TestFixture]
public class HealthComponentTests
{
  //Test whether the value of blood is greater than 0 after injury
  [Test]
  public void TakeDamage_BeAttacked_BiggerZero()
    {
      //Arrange 
      UnMonoHealthClass health = new UnMonoHealthClass();
      health.healthAmount = 50f;

      //Act
      health.TakeDamage(60f);

      //Assert
        Assert.GreaterOrEqual(health.healthAmount, 0);
    }
}

This example is to test whether the blood volume of the hero will cross the boundary and have a negative value after being hurt.

The test framework will create the test case class and call takedamage_ BeAttacked_ Biggerzero method to interact with it, and finally use NUnit’s assert class to assert whether it passes the test.

Start unit tests using editor tests runner

After writing the unit test cases, we can Unit testing started in the editor of 3. X. As shown in the figure:

Writing high-quality unit tests for unity3d using NUnit

Writing high-quality unit tests for unity3d using NUnit

Here, we can run both individual test cases and all test cases. What passes is the green mark and what fails is the red mark.
The top line is the part we can operate:

  • Run all: test all cases

  • Run selected: test the selected case

  • Rerun failed: retest the test case that failed last time

  • Search box: you can search for use cases

  • Category filter: use cases can be filtered according to category. Category needs to be identified by categoryattribute in test code.

  • Test result filter: use cases can be filtered by pass, fail, and ignore

Writing high-quality unit tests for unity3d using NUnit

Here we can also set up to run unit tests automatically before compilation.

Run unit tests using the command line

In addition to using unit tests in editor, we naturally prefer to incorporate unit tests into the automatic integration pipeline. Therefore, it is necessary to call tests from outside U3D. Fortunately, U3D also provides the way of external call, so it is feasible to add unit test to our automatic integration pipeline.
Unity3D 5.3. The command line options available in version x are as follows:

Runeditortests must have the option to run editor test
Editortestsresultfile is used to save test results
Editortestsfilter runs the specified use case according to the use case name
Editor tests categories runs the specified use cases according to the type of use cases
Editortestsverboselog prints a more detailed log
Projectpath project directory

So you can start the test on the command line as follows:

Unity -runEditorTests -projectPath /Users/fanyou/UnitTest -editorTestsResultFile  /Users/fanyou/UnitTest/test.xml -batchmode -quit

0x08 postscript

The above are some thoughts on introducing unit testing into U3D. Of course, whether game development is suitable for TDD, in other words, whether to write unit testing first and then realize functions is worth discussing, but unit testing itself is very necessary to be used in engineering. It will be very helpful in code structure design and future refactoring.

relevant

Practice of TDD in the development of unity3d game project

Recommended Today

JS generate guid method

JS generate guid method https://blog.csdn.net/Alive_tree/article/details/87942348 Globally unique identification(GUID) is an algorithm generatedBinaryCount Reg128 bitsNumber ofidentifier , GUID is mainly used in networks or systems with multiple nodes and computers. Ideally, any computational geometry computer cluster will not generate two identical guids, and the total number of guids is2^128In theory, it is difficult to make two […]