Skip to content

Commit

Permalink
Fix using UnityEditor assembly in Play Mode tests
Browse files Browse the repository at this point in the history
Add temporary build scenes when running tests
  • Loading branch information
nowsprinting committed Oct 16, 2023
1 parent 9ca1b21 commit 1b09545
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 1b09545

Please sign in to comment.