{"id":1232,"date":"2026-10-24T19:28:41","date_gmt":"2026-10-24T19:28:41","guid":{"rendered":"https:\/\/docs.sisus.co\/init-args\/?p=1232"},"modified":"2026-10-24T19:28:41","modified_gmt":"2026-10-24T19:28:41","slug":"unit-testing","status":"publish","type":"post","link":"https:\/\/docs.sisus.co\/init-args\/advanced\/unit-testing\/","title":{"rendered":"Unit-testing"},"content":{"rendered":"<h1>Testable By Default<\/h1>\n<p>Because clients receive all their dependencies from the outside, instead of fetching them from singletons or using methods like <code>GetComponent<\/code> and <code>FindAnyObjectByType<\/code>, it is easy to swap out any of them with test doubles during unit tests.<\/p>\n<p>There&#8217;s no need to create test scenes or prefabs and configure them using the Inspector. You can create clients directly in code, pass them fake objects, and then verify that they behave as expected.<\/p>\n<p>This works in both Edit Mode and Play Mode tests.<\/p>\n<h1>Setting Up A Test Assembly<\/h1>\n<p>Unity&#8217;s <a href=\"https:\/\/docs.unity3d.com\/Packages\/com.unity.test-framework@latest\">Test Framework<\/a> requires that unit tests are placed inside their own <a href=\"https:\/\/docs.unity3d.com\/Manual\/ScriptCompilationAssemblyDefinitionFiles.html\">assembly definition<\/a>. You can create one using the Test Runner window (<strong>Window &gt; General &gt; Test Runner<\/strong>).<\/p>\n<p>Your test assembly should have references to the following assemblies:<\/p>\n<ul>\n<li><strong>InitArgs<\/strong> &#8211; needed to pass arguments to clients using <code>AddComponent<\/code>, <code>Instantiate<\/code>, <code>new GameObject&lt;T...&gt;<\/code> and the <a href=\"https:\/\/docs.sisus.co\/init-args\/reference\/service-api\/\">Service API<\/a>.<\/li>\n<li><strong>InitArgs.Editor<\/strong> &#8211; needed to use the <code>Testable<\/code> helper class in Edit Mode tests.<\/li>\n<li>The assemblies that contain the code you want to test.<\/li>\n<\/ul>\n<p><!-- SCREENSHOT: Inspector of a test assembly definition asset, showing InitArgs and InitArgs.Editor in the Assembly Definition References list. --><\/p>\n<p>See <a href=\"https:\/\/docs.sisus.co\/init-args\/installation\/assembly-definitions\/\">Referencing Init(args) Assemblies<\/a> for more information.<\/p>\n<h1>Passing Test Doubles With AddComponent<\/h1>\n<p>Let&#8217;s say you have a component that triggers an event when it is collected:<\/p>\n<pre>using Sisus.Init;\r\nusing UnityEngine;\r\n\r\npublic class Collectable : MonoBehaviour&lt;IEventTrigger&gt;\r\n{\r\n    IEventTrigger onCollected;\r\n\r\n    protected override void Init(IEventTrigger onCollected) =&gt; this.onCollected = onCollected;\r\n\r\n    public void Collect()\r\n    {\r\n        onCollected.Trigger();\r\n        gameObject.SetActive(false);\r\n    }\r\n}<\/pre>\n<p>To test it, first create a simple fake implementation of <code>IEventTrigger<\/code> that keeps track of whether it has been triggered:<\/p>\n<pre>public class FakeEvent : IEventTrigger\r\n{\r\n    public bool HasBeenTriggered { get; private set; }\r\n    public void Trigger() =&gt; HasBeenTriggered = true;\r\n}<\/pre>\n<p>Then pass it to the client using <code>AddComponent<\/code>:<\/p>\n<pre>using NUnit.Framework;\r\nusing Sisus.Init;\r\nusing UnityEngine;\r\n\r\npublic class TestCollectable\r\n{\r\n    [Test]\r\n    public void Collect_Triggers_Event()\r\n    {\r\n        var gameObject = new GameObject();\r\n        var onCollected = new FakeEvent();\r\n        var collectable = gameObject.AddComponent&lt;Collectable, IEventTrigger&gt;(onCollected);\r\n\r\n        collectable.Collect();\r\n\r\n        Assert.IsTrue(onCollected.HasBeenTriggered);\r\n\r\n        Object.DestroyImmediate(gameObject);\r\n    }\r\n}<\/pre>\n<p>The <code>Init<\/code> method of the client gets executed before <code>AddComponent<\/code> returns, so the client is immediately ready to be tested. This is the case even if the GameObject is inactive, and even in Edit Mode tests.<\/p>\n<p>Prefabs can be initialized in the same way using <code>Instantiate<\/code>:<\/p>\n<pre>var collectable = collectablePrefab.Instantiate(onCollected);<\/pre>\n<h1>Creating Clients With new GameObject&lt;T&#8230;&gt;<\/h1>\n<p>You can also use <a href=\"https:\/\/docs.sisus.co\/init-args\/initializers\/gameobject-t\/\">new GameObject&lt;T&#8230;&gt;<\/a> to create a new GameObject and initialize a client in a single line:<\/p>\n<pre>var collectable = new GameObject&lt;Collectable&gt;().Init(onCollected);<\/pre>\n<p>This also makes it easy to create a GameObject with multiple components. For example, here a <code>Killable<\/code> component is added together with a <code>BoxCollider<\/code>, and initialized using <code>Init1<\/code> (the <strong>1<\/strong> indicating that the arguments are for the first component):<\/p>\n<pre>var killable = new GameObject&lt;Killable, BoxCollider&gt;().Init1((IEventTrigger)onKilled);<\/pre>\n<p>If a client depends on another component on the same GameObject, you can use the <code>Second.Component<\/code> token to pass it the second added component:<\/p>\n<pre>var player = new GameObject&lt;Player, Camera&gt;().Init1(inputManager, Second.Component);<\/pre>\n<h1>Executing Event Functions In Edit Mode<\/h1>\n<p>In Edit Mode tests Unity doesn&#8217;t execute event functions like <code>Awake<\/code>, <code>Start<\/code> and <code>Update<\/code> automatically. You can use the <code>Testable<\/code> class from the <code>Sisus.Init.Testing<\/code> namespace to execute them manually.<\/p>\n<p>Simply wrap the GameObject containing your client inside a <code>Testable<\/code>, and then call the event functions you want to execute on it:<\/p>\n<pre>using NUnit.Framework;\r\nusing Sisus.Init;\r\nusing Sisus.Init.Testing;\r\nusing UnityEngine;\r\n\r\npublic class TestMovable\r\n{\r\n    [Test]\r\n    public void Can_Move_Right()\r\n    {\r\n        var settings = new FakeMoveSettings { MoveSpeed = 1f };\r\n        var level = new FakeLevel { Bounds = new RectInt(0, 0, 3, 1) };\r\n        var timeProvider = new TestTimeProvider { DeltaTime = 1f };\r\n        var movable = new GameObject&lt;Movable&gt;().Init(settings, level, timeProvider);\r\n        using var testable = new Testable(movable.gameObject);\r\n\r\n        movable.MoveDirection = Vector2.right;\r\n        testable.Update();\r\n\r\n        Assert.AreEqual(new Vector2(1f, 0f), movable.Position);\r\n    }\r\n}<\/pre>\n<p>Things to know about <code>Testable<\/code>:<\/p>\n<ul>\n<li>When a <code>Testable<\/code> is created, it executes <code>Awake<\/code>, <code>OnEnable<\/code> and <code>Start<\/code> on all components on the GameObject. You can turn this off by passing <code>false<\/code> as the second constructor argument.<\/li>\n<li><code>Update<\/code> and <code>FixedUpdate<\/code> optionally accept the number of times to execute them, e.g. <code>testable.Update(3)<\/code>.<\/li>\n<li>Physics messages can be simulated as well, e.g. <code>testable.OnTriggerEnter(collider)<\/code>.<\/li>\n<li>Events can be raised using <code>RaiseEvent(nameof(MyClient.EventName))<\/code>.<\/li>\n<li>Calling <code>Destroy<\/code> executes <code>OnDisable<\/code> and <code>OnDestroy<\/code> and then destroys the GameObject. A <code>using<\/code> statement can be used to do this automatically at the end of the test.<\/li>\n<\/ul>\n<p>Here&#8217;s an example of a test that simulates two objects colliding:<\/p>\n<pre>[Test]\r\npublic void OnTriggerEnter_With_Deadly_Object_Sets_GameObject_Inactive()\r\n{\r\n    var killable = new GameObject&lt;Killable, BoxCollider&gt;().Init1((IEventTrigger)new FakeEvent());\r\n    using var testable = new Testable(killable.gameObject);\r\n    (FakeDeadly deadly, BoxCollider collider) deadly = new GameObject&lt;FakeDeadly, BoxCollider&gt;();\r\n\r\n    testable.OnTriggerEnter(deadly.collider);\r\n\r\n    Assert.IsFalse(killable.gameObject.activeSelf);\r\n\r\n    Object.DestroyImmediate(deadly.collider.gameObject);\r\n}<\/pre>\n<h1>Testing Plain Old C# Objects<\/h1>\n<p>If a plain old C# object is attached to a GameObject using a <a href=\"https:\/\/docs.sisus.co\/init-args\/wrappers\/wrapper\/\">Wrapper<\/a>, you can test it without creating any GameObjects at all. Just pass all its dependencies to it through its constructor:<\/p>\n<pre>var spawner = new Spawner(settings, new FakeTrackable(), new FakeLevel(), timeProvider, gameObjectBuilder);<\/pre>\n<h1>Replacing Global Services<\/h1>\n<p>Sometimes it is not practical to pass every dependency to clients individually. For example:<\/p>\n<ul>\n<li>A Play Mode test loads a scene or instantiates a prefab containing many clients that receive their services automatically.<\/li>\n<li>A client acquires a dependency using a loose <code>Any&lt;TValue&gt;<\/code> field or inside a method body using the Service API instead of receiving it as an Init argument.<\/li>\n<\/ul>\n<p>In such cases you can replace global services with test doubles for the duration of the test using the <a href=\"https:\/\/docs.sisus.co\/init-args\/reference\/service-api\/\">Service API<\/a>:<\/p>\n<pre>[SetUp]\r\npublic void SetUp() =&gt; Service.Set&lt;ILogger&gt;(testLogger);\r\n\r\n[TearDown]\r\npublic void TearDown() =&gt; Service.Unset&lt;ILogger&gt;(testLogger);<\/pre>\n<p>Note that <code>Service.Set<\/code> replaces the existing global service of the same type, and <code>Service.Unset<\/code> does not bring it back. If you want to restore the original service after the test, you can store a reference to it and set it back afterwards:<\/p>\n<pre>public readonly struct ScopedService&lt;T&gt; : IDisposable\r\n{\r\n    readonly T previous;\r\n\r\n    public ScopedService(T service)\r\n    {\r\n        previous = Service&lt;T&gt;.Instance;\r\n        Service.Set(service);\r\n    }\r\n\r\n    public void Dispose()\r\n    {\r\n        Service.Unset&lt;T&gt;();\r\n        if(previous is not null)\r\n        {\r\n            Service.Set(previous);\r\n        }\r\n    }\r\n}<\/pre>\n<p>One pattern that can sometimes be useful is to create a base class that replaces commonly used services with test doubles before each test, and restores the originals after it:<\/p>\n<pre>public abstract class UnitTests\r\n{\r\n    ScopedService&lt;ILogger&gt; logger;\r\n    ScopedService&lt;ITimeProvider&gt; timeProvider;\r\n\r\n    protected TestLogger Logger { get; } = new();\r\n    protected TestTimeProvider TimeProvider { get; } = new();\r\n\r\n    [SetUp]\r\n    public void Setup()\r\n    {\r\n        logger = new(Logger);\r\n        timeProvider = new(TimeProvider);\r\n        OnSetup();\r\n    }\r\n\r\n    [TearDown]\r\n    public void TearDown()\r\n    {\r\n        OnTearDown();\r\n        logger.Dispose();\r\n        timeProvider.Dispose();\r\n    }\r\n\r\n    protected virtual void OnSetup() { }\r\n    protected virtual void OnTearDown() { }\r\n}<\/pre>\n<h1>Tips<\/h1>\n<ul>\n<li><strong>Depend on interfaces.<\/strong> Clients that depend on interfaces like <code>ILogger<\/code>, <code>ITimeProvider<\/code> or <code>IInputManager<\/code> instead of concrete types can be easier to test, because a simple fake class can be passed to them instead of the real implementation.<\/li>\n<li><strong>Wrap Unity&#8217;s static APIs.<\/strong> Static members like <code>Time.deltaTime<\/code> and <code>Input.GetAxis<\/code> can&#8217;t be replaced in tests. If you access them through a service instead (e.g. <code>ITimeProvider<\/code>), you can fully control them during tests.<\/li>\n<li><strong>Test-only Init arguments.<\/strong> If a client&#8217;s dependencies are only ever passed to it in unit tests, you can add <a href=\"https:\/\/docs.sisus.co\/init-args\/reference\/init-attribute\/\">[Init(Enabled = false)]<\/a> to it. This hides the Init section in the Inspector and disables warnings about missing Init arguments in your scenes.<\/li>\n<li><strong>Ambiguous Is.<\/strong> Both <code>NUnit.Framework<\/code> and <code>Sisus.Init<\/code> contain a type named <code>Is<\/code>. If you get an ambiguity error when using <code>Assert.That(x, Is.EqualTo(y))<\/code>, you can add <code>using Is = NUnit.Framework.Is;<\/code> to the top of the file.<\/li>\n<\/ul>\n<h1>More Examples<\/h1>\n<p>The <a href=\"https:\/\/docs.sisus.co\/init-args\/installation\/demo-game\/\">Demo Game<\/a> sample contains a full suite of Edit Mode unit tests that make use of all the techniques described on this page. After importing the sample, you can find them in the <strong>Samples\/Init(args) &#8211; A next-gen DI framework\/&lt;VersionNumber&gt;\/Demo Game\/Tests<\/strong> folder.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Testable By Default Because clients receive all their dependencies from the outside, instead of fetching them from singletons or using methods like GetComponent and FindAnyObjectByType, it is easy to swap out any of them with test doubles during unit tests. There&#8217;s no need to create test scenes or prefabs and configure them using the Inspector. ..<\/p>\n<div class=\"clear-fix\"><\/div>\n<p><a href=\"https:\/\/docs.sisus.co\/init-args\/advanced\/unit-testing\/\" title=\"read more\">Read more<\/a><\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/posts\/1232"}],"collection":[{"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/comments?post=1232"}],"version-history":[{"count":1,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/posts\/1232\/revisions"}],"predecessor-version":[{"id":1233,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/posts\/1232\/revisions\/1233"}],"wp:attachment":[{"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/media?parent=1232"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/categories?post=1232"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/docs.sisus.co\/init-args\/wp-json\/wp\/v2\/tags?post=1232"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}