# 4. Play Mode Tests and Scene Fixtures
Part of [[Unity Test Framework]]. Previous: [[3. UnityTest Async Tests and Domain Reloads]]. Next: [[5. Worked Example - Inventory and Stats Tests]].
> [!note] Verification status
> `IPrebuildSetup` / `IPostBuildCleanup`, `IPrebuildSetupWithTestData` / `IPostbuildCleanupWithTestData` (Test Framework 1.6.0), and `ITestPlayerBuildModifier` are part of the documented Test Framework API. Scene-loading examples use `SceneManager.LoadSceneAsync` with `LoadSceneMode.Additive`, which is also covered in [[SceneManagement in Unity 6]]. **Code not compiled in Unity.** Menu wording for "Run Tests in Player" is **not verified** against a live 6000.5 Test Runner click-through.
## 4.1 A Play Mode assembly
Create a second test assembly (`Assets/Tests/PlayMode/Studio.PlayMode.Tests.asmdef`):
1. **Assets > Create > Testing > Test Assembly Folder**.
2. In the Inspector, enable **Any Platform** (or Windows / Android / whatever you will run in a Player). Leave **Editor** checked too so you can iterate in the Editor.
3. Reference your runtime asmdef(s).
4. Keep `UnityEditor.TestRunner` only if you also have Edit Mode tests in the same assembly. A clean split — Edit Mode assembly (Editor only) and Play Mode assembly (Any Platform) — is easier to teach.
Play Mode tests appear under the **PlayMode** tab. Running them in the Editor enters Play Mode, runs the selected tests, then restores the open scene setup (Test Framework 1.4.0).
## 4.2 Don't pollute the current scene
A Play Mode test that calls `new GameObject()` in whatever scene you happened to have open will leak objects into your working scene if teardown fails, and it will depend on leftover lights, event systems or audio listeners. Use a **fixture scene**:
- A nearly empty scene named `Assets/Tests/PlayMode/Fixtures/PlayModeFixture.unity` with a camera, a light, and nothing else.
- Load it additively (or Single, if you want isolation) at the start of the test or in `[UnitySetUp]`.
- Unload it in `[UnityTearDown]`.
```csharp
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
public class FixtureSceneTests
{
const string FixtureScene = "PlayModeFixture";
[UnitySetUp]
public IEnumerator LoadFixture()
{
yield return SceneManager.LoadSceneAsync(FixtureScene, LoadSceneMode.Additive);
Scene loaded = SceneManager.GetSceneByName(FixtureScene);
SceneManager.SetActiveScene(loaded);
}
[UnityTearDown]
public IEnumerator UnloadFixture()
{
yield return SceneManager.UnloadSceneAsync(FixtureScene);
}
[UnityTest]
public IEnumerator Fixture_HasACamera()
{
yield return null;
Assert.IsNotNull(Camera.main);
}
}
```
Add `PlayModeFixture` to **File > Build Profiles > Scene List** if you run these tests in a Player (or add it from `IPrebuildSetup`). Scenes that are only for tests should not ship in the production build — the prebuild hook below is the clean way to include them for test players only.
If you use Addressables to load scenes in production, Play Mode tests can use the same API; that belongs with the Addressables tutorial. For this series, `SceneManager` is enough. See [[SceneManagement in Unity 6]] for additive-load details.
## 4.3 `IPrebuildSetup` and cleanup
When tests run in a **Player**, Unity builds a player that contains the tests. `IPrebuildSetup.Setup()` runs in the Editor before that build, which is your chance to:
- Add fixture scenes to the build list
- Enable a development-build or scripting-define you need
- Generate a tiny ScriptableObject asset the tests will load
```csharp
using UnityEditor;
using UnityEngine.TestTools;
public class PlayModePrebuild : IPrebuildSetup, IPostBuildCleanup
{
public void Setup()
{
// Example: make sure the fixture scene is in the test player.
// EditorBuildSettings.scenes can be mutated here; restore it in Cleanup.
}
public void Cleanup()
{
// Restore anything Setup changed so the next regular build is clean.
}
}
```
Attach the setup to a test or fixture with `[PrebuildSetup(typeof(PlayModePrebuild))]`, or implement the interfaces on the test class itself.
Errors logged with `Debug.LogError` during setup fail the run (unless expected with `LogAssert`). From 1.6.0, `IPrebuildSetupWithTestData` / `IPostbuildCleanupWithTestData` give you a `TestData` object if you need to pass information from the prebuild step.
`ITestPlayerBuildModifier.ModifyOptions` lets you tweak `BuildPlayerOptions` (development build, autoconnect profiler, target path). The running-tests manual has a "Modifying a Player build for tests" topic for the current signature — **check it before you copy an old blog post**, because the options type has evolved with Build Profiles.
## 4.4 Running in a Player
From the Test Runner you can run selected or all Play Mode tests in a Player, or export a player that already contains the tests. From the command line you pass a `BuildTarget` as `-testPlatform` (part 6) instead of `EditMode` / `PlayMode`.
Device notes worth teaching:
- **Android / Quest:** a first Player run is a lot of USB or Wi-Fi ceremony. Use a development build. The Test Framework changelog (1.4.4) specifically mentions Quest headsets timing out before the run started; if a Quest Player run sits idle, look at the player connection heartbeat, not only your test body. VR project setup is in [[3.Quest 2 Project and Build Setup]] and [[4.Quest 2 Testing and Deployment]].
- **IL2CPP and stripping:** `[Values]` can be stripped; `PreservedValuesAttribute` exists for that. Keep test-only types from being stripped with link.xml or by actually referencing them.
- Scene restore: standalone Player runs restore scene setup after a completed run (1.4.0). Cancelling a Play Mode run in the Editor also restores scene setup (1.4.5).
## 4.5 Physics and input in Play Mode
If you assert on a `Rigidbody` contact, wait enough `FixedUpdate` steps:
```csharp
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class GravitySmokeTests
{
[UnityTest]
public IEnumerator DynamicBody_Falls()
{
var go = new GameObject("box", typeof(Rigidbody), typeof(BoxCollider));
go.transform.position = Vector3.up * 2f;
var body = go.GetComponent<Rigidbody>();
body.useGravity = true;
float startY = body.position.y;
for (int i = 0; i < 8; i++)
yield return new WaitForFixedUpdate();
Assert.Less(body.position.y, startY);
Object.Destroy(go);
}
}
```
Do not drive the Input System from a "simulated key" unless you have set up the Input System test utilities; that is a follow-up, not a first Play Mode test. Prefer to call the same method your Input action would call ([[Input System]]).
## Official docs
- Running tests: https://docs.unity.com/en-us/engine/6000.5/manual/scripting/test-framework-introduction/running-tests
- Scene management: see [[SceneManagement in Unity 6]]
- Test Framework 1.7 changelog (Quest timeout, scene restore, IPrebuildSetupWithTestData): https://docs.unity3d.com/Packages/
[email protected]/changelog/CHANGELOG.html