Skip to content

Commit

Permalink
Add temporary build scenes when running play mode tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nowsprinting committed Oct 16, 2023
1 parent 9ca1b21 commit b3fca4a
Show file tree
Hide file tree
Showing 17 changed files with 2,695 additions and 9 deletions.
140 changes: 140 additions & 0 deletions Editor/TemporaryBuildScenesUsingInTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) 2023 Koji Hasegawa.
// This software is released under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using TestHelper.Attributes;
using TestHelper.Editor;
using UnityEditor;
using UnityEditor.TestTools;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;

[assembly: TestPlayerBuildModifier(typeof(TemporaryBuildScenesUsingInTest.RunOnStandalonePlayer))]

namespace TestHelper.Editor
{
/// <summary>
/// Temporarily build scenes specified by <c>ScenesUsingInTestAttribute</c> when running play mode tests.
/// </summary>
public static class TemporaryBuildScenesUsingInTest
{
private static IEnumerable<ScenesUsingInTestAttribute> FindScenesUsingInTestAttributesOnAssemblies()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var attribute in assemblies
.Select(assembly => assembly.GetCustomAttributes(typeof(ScenesUsingInTestAttribute), false))
.SelectMany(attributes => attributes))
{
yield return attribute as ScenesUsingInTestAttribute;
}
}

private static IEnumerable<ScenesUsingInTestAttribute> FindScenesUsingInTestAttributesOnTypes()
{
var symbols = TypeCache.GetTypesWithAttribute<ScenesUsingInTestAttribute>();
foreach (var attribute in symbols
.Select(symbol => symbol.GetCustomAttributes(typeof(ScenesUsingInTestAttribute), false))
.SelectMany(attributes => attributes))
{
yield return attribute as ScenesUsingInTestAttribute;
}
}

private static IEnumerable<ScenesUsingInTestAttribute> FindScenesUsingInTestAttributesOnMethods()
{
var symbols = TypeCache.GetMethodsWithAttribute<ScenesUsingInTestAttribute>();
foreach (var attribute in symbols
.Select(symbol => symbol.GetCustomAttributes(typeof(ScenesUsingInTestAttribute), false))
.SelectMany(attributes => attributes))
{
yield return attribute as ScenesUsingInTestAttribute;
}
}

internal static IEnumerable<string> GetScenesUsingInTest()
{
var attributes = FindScenesUsingInTestAttributesOnAssemblies()
.Concat(FindScenesUsingInTestAttributesOnTypes())
.Concat(FindScenesUsingInTestAttributesOnMethods());
foreach (var attribute in attributes)
{
if (attribute.ScenePath.ToLower().EndsWith(".unity"))
{
yield return attribute.ScenePath;
}
else
{
foreach (var guid in AssetDatabase.FindAssets("t:SceneAsset", new[] { attribute.ScenePath }))
{
yield return AssetDatabase.GUIDToAssetPath(guid);
}
}
}
}

/// <summary>
/// Add temporary scenes to build when running play mode tests in editor.
/// </summary>
public class RunInEditor : ICallbacks
{
[InitializeOnLoadMethod]
private static void SetupRunningInEditor()
{
var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.RegisterCallbacks(new RunInEditor());
}

/// <inheritdoc />
public void RunStarted(ITestAdaptor testsToRun)
{
var scenesInBuild = EditorBuildSettings.scenes.ToList();
foreach (var scenePath in GetScenesUsingInTest())
{
if (scenesInBuild.All(scene => scene.path != scenePath))
{
scenesInBuild.Add(new EditorBuildSettingsScene(scenePath, true));
}
}

EditorBuildSettings.scenes = scenesInBuild.ToArray();
}

/// <inheritdoc />
public void RunFinished(ITestResultAdaptor result) { }

/// <inheritdoc />
public void TestStarted(ITestAdaptor test) { }

/// <inheritdoc />
public void TestFinished(ITestResultAdaptor result) { }
}

/// <summary>
/// Add temporary scenes to build when running play mode tests on standalone player.
/// </summary>
/// <remarks>
/// Required Unity Test Framework package v1.1.13 or higher is to use this script.
/// For details, see the <see href="https://forum.unity.com/threads/testplayerbuildmodifier-not-working.844447/">report in forum</see>.
/// </remarks>
public class RunOnStandalonePlayer : ITestPlayerBuildModifier
{
/// <inheritdoc />
public BuildPlayerOptions ModifyOptions(BuildPlayerOptions playerOptions)
{
var scenesInBuild = new List<string>(playerOptions.scenes);
foreach (var scenePath in GetScenesUsingInTest())
{
if (!scenesInBuild.Contains(scenePath))
{
scenesInBuild.Add(scenePath);
}
}

playerOptions.scenes = scenesInBuild.ToArray();
return playerOptions;
}
}
}
}
11 changes: 11 additions & 0 deletions Editor/TemporaryBuildScenesUsingInTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Runtime/Attributes/ScenesUsingInTestAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace TestHelper.Attributes
{
/// <summary>
/// Temporarily build scene files not added to "Scenes in Build" when running a test.
/// Temporarily build scene files not added to "Scenes in Build" when running a play mode tests.
/// It has the following effects:
/// - Can specify only scene name to `SceneManager.LoadScene()` method
/// - Can load in `SceneManager.LoadScene()` method when running a test on a standalone player.
Expand All @@ -25,10 +25,10 @@ public class ScenesUsingInTestAttribute : NUnitAttribute
internal string ScenePath { get; private set; }

/// <summary>
/// Specify scene file path to temporarily build when running a test.
/// Specify scene file or directory path to temporarily build when running a test.
/// </summary>
/// <param name="scenePath">Scene path not in "Scenes in Build".
/// The scene file path starts with `Assets/` or `Packages/`, and ends with `.unity`.
/// The scene file path starts with `Assets/` or `Packages/`.
/// Use `name` instead of `displayName` in package paths.
/// </param>
public ScenesUsingInTestAttribute(string scenePath)
Expand Down
56 changes: 56 additions & 0 deletions Tests/Editor/TemporaryBuildScenesUsingInTestTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2023 Koji Hasegawa.
// This software is released under the MIT License.

using System.Linq;
using NUnit.Framework;

namespace TestHelper.Editor
{
[TestFixture]
public class TemporaryBuildScenesUsingInTestTest
{
[Test]
public void GetScenesUsingInTest_AttachedToAssembly_ReturnScenesSpecifiedByAttribute()
{
var actual = TemporaryBuildScenesUsingInTest.GetScenesUsingInTest();
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild_Assembly.unity"));
}

[Test]
public void GetScenesUsingInTest_AttachedToClass_ReturnScenesSpecifiedByAttribute()
{
var actual = TemporaryBuildScenesUsingInTest.GetScenesUsingInTest();
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild_Class.unity"));
}

[Test]
public void GetScenesUsingInTest_AttachedToMethod_ReturnScenesSpecifiedByAttribute()
{
var actual = TemporaryBuildScenesUsingInTest.GetScenesUsingInTest();
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild.unity"));
}

[Test]
public void GetScenesUsingInTest_AttachedToMethodMultiple_ReturnScenesSpecifiedByAttribute()
{
var actual = TemporaryBuildScenesUsingInTest.GetScenesUsingInTest().ToArray();
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild2.unity"));
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild3.unity"));
}

[Test]
public void GetScenesUsingInTest_SpecifyDirectory_ReturnScenesSpecifiedByAttribute()
{
var actual = TemporaryBuildScenesUsingInTest.GetScenesUsingInTest().ToArray();
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/Sub/NotInScenesInBuild4.unity"));
Assert.That(actual,
Does.Contain("Packages/com.nowsprinting.test-helper/Tests/Scenes/Sub/NotInScenesInBuild5.unity"));
}
}
}
3 changes: 3 additions & 0 deletions Tests/Editor/TemporaryBuildScenesUsingInTestTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 39 additions & 6 deletions Tests/Runtime/Attributes/ScenesUsingInTestAttributeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,70 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework;
using TestHelper.Attributes;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;

[assembly: ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild_Assembly.unity")]

namespace TestHelper.Attributes
{
[TestFixture]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild_Class.unity")]
[SuppressMessage("ReSharper", "Unity.LoadSceneUnexistingScene")]
public class ScenesUsingInTestAttributeTest
{
[UnityTest]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild.unity")]
public IEnumerator Attach_CanLoadSceneNotIncludedBuild()
public IEnumerator AttachedToAssembly_CanLoadSceneNotIncludedBuild()
{
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild");
var cube = GameObject.Find("CubeInNotInScenesInBuild");
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild_Assembly");
var cube = GameObject.Find("CubeInNotInScenesInBuild_Assembly");
Assert.That(cube, Is.Not.Null);
}

[UnityTest]
public IEnumerator AttachedToClass_CanLoadSceneNotIncludedBuild()
{
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild_Class");
var cube = GameObject.Find("CubeInNotInScenesInBuild_Class");
Assert.That(cube, Is.Not.Null);
}

[UnityTest]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild.unity")]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild2.unity")]
public IEnumerator AttachMultiple_CanLoadScenesNotIncludedBuild()
public IEnumerator AttachedToMethod_CanLoadSceneNotIncludedBuild()
{
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild");
var cube = GameObject.Find("CubeInNotInScenesInBuild");
Assert.That(cube, Is.Not.Null);
}

[UnityTest]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild2.unity")]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/NotInScenesInBuild3.unity")]
public IEnumerator AttachedToMethodMultiple_CanLoadScenesNotIncludedBuild()
{
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild2");
var cube2 = GameObject.Find("CubeInNotInScenesInBuild2");
Assert.That(cube2, Is.Not.Null);

yield return SceneManager.LoadSceneAsync("NotInScenesInBuild3");
var cube3 = GameObject.Find("CubeInNotInScenesInBuild3");
Assert.That(cube3, Is.Not.Null);
}

[UnityTest]
[ScenesUsingInTest("Packages/com.nowsprinting.test-helper/Tests/Scenes/Sub")]
public IEnumerator SpecifyDirectory_CanLoadScenesNotIncludedBuild()
{
yield return SceneManager.LoadSceneAsync("NotInScenesInBuild4");
var cube4 = GameObject.Find("CubeInNotInScenesInBuild4");
Assert.That(cube4, Is.Not.Null);

yield return SceneManager.LoadSceneAsync("NotInScenesInBuild5");
var cube5 = GameObject.Find("CubeInNotInScenesInBuild5");
Assert.That(cube5, Is.Not.Null);
}
}
}
Loading

0 comments on commit b3fca4a

Please sign in to comment.