Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Content/GameSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ID": "6888874e4229953ae4692194ef19ddf2",
"TypeName": "FlaxEditor.Content.Settings.GameSettings",
"EngineBuild": 6187,
"Data": {
"ProductName": "Simple Unit Testing",
"FirstScene": "00000000000000000000000000000000"
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Unit Testing Plugin for the FlaxEngine

22 changes: 17 additions & 5 deletions Source/Assert.cs → Source/Editor/Assert.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
using System;

namespace FlaxEngine.UnitTesting
namespace FlaxCommunity.UnitTesting.Editor
{
/// <summary>
/// Special type of exception that is used to terminate the test case early <seealso cref="Assert.Pass"/>
/// </summary>
/// <seealso cref="System.Exception" />
public class SuccessException : Exception { }
public class SuccessException : Exception
{
public SuccessException()
{
}

public SuccessException(string message) : base(message)
{
}

public SuccessException(string message, Exception innerException) : base(message, innerException)
{
}
}

public static class Assert
{
public static void Pass() => throw new SuccessException();
public static void Fail() => throw new Exception();

// TODO: use Equals instead of ==
public static void AreEqual(object a, object b) { if (a != b) throw new Exception(); }
public static void AreNotEqual(object a, object b) { if (a == b) throw new Exception(); }
public static void AreEqual(object a, object b) { if (!Equals(a, b)) throw new Exception(); }
public static void AreNotEqual(object a, object b) { if (Equals(a, b)) throw new Exception(); }

public static void True(bool a) { if (!a) throw new Exception(); }
public static void False(bool a) { if (a) throw new Exception(); }
Expand Down
20 changes: 16 additions & 4 deletions Source/ExampleClass.cs → Source/Editor/ExampleClass.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#if !FLAX_PLUGIN
using FlaxEngine.UnitTesting;
using FlaxCommunity.UnitTesting.Editor;

namespace UnitTests
namespace UnitTests.Editor
{
[TestFixture]
internal class SimpleTests
Expand All @@ -15,17 +15,24 @@ public void SuccessTest()
[Test]
public void ErrorTest()
{
Assert.True(null != null);
Assert.True(1 != 1);
}
}

[TestFixture]
internal class SetupTests
{
private object Tested = null;
private object Database = null;

[OneTimeSetUp]
public void Init()
{
Database = new object { };
}

[SetUp]
public void Setup()
public void BeforeEach()
{
Tested = "Test";
}
Expand All @@ -37,8 +44,13 @@ public void SetupTest()
}

[TearDown]
public void AfterEach()
{
}

public void Dispose()
{
Database = null;
}
}

Expand Down
43 changes: 37 additions & 6 deletions Source/TestAttributes.cs → Source/Editor/TestAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;

namespace FlaxEngine.UnitTesting
namespace FlaxCommunity.UnitTesting.Editor
{
/// <summary>
/// A test case
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TestCase : Attribute
public sealed class TestCase : Attribute
{
public readonly object[] Attributes;
public object ExpectedResult { get; set; }
Expand Down Expand Up @@ -43,24 +46,52 @@ public TestCase(object T1, object T2, object T3, object T4, object T5, object T6
}
}

/// <summary>
/// A single test
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class Test : Attribute
public sealed class Test : Attribute
{

}

/// <summary>
/// Executed before every single test
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class SetUp : Attribute
public sealed class SetUp : Attribute
{
}

/// <summary>
/// Executed after every single test
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class TearDown : Attribute
public sealed class TearDown : Attribute
{
}

/// <summary>
/// Executed before all tests
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class OneTimeSetUp : Attribute
{
}

/// <summary>
/// Executed after all tests
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class OneTimeTearDown : Attribute
{
}

/// <summary>
/// Specifies a class as a unit test class
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class TestFixture : Attribute
public sealed class TestFixture : Attribute
{
}
}
111 changes: 68 additions & 43 deletions Source/Editor/TestRunner.cs
Original file line number Diff line number Diff line change
@@ -1,120 +1,145 @@
using FlaxEditor;
using FlaxEditor.GUI;
using FlaxEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FlaxCommunity.UnitTesting;

namespace FlaxEngine.UnitTesting.Editor
namespace FlaxCommunity.UnitTesting.Editor
{
public class TestRunner : EditorPlugin
{
private static List<Type> suites = new List<Type>();
private MainMenuButton mmBtn;
private static readonly List<Type> _suites = new List<Type>();
private MainMenuButton _mmBtn;

public override PluginDescription Description => new PluginDescription
{
Author = "Lukáš Jech",
AuthorUrl ="https://lukas.jech.me",
AuthorUrl = "https://lukas.jech.me",
Category = "Unit Testing",
Description = "Simple unit testing framework",
IsAlpha = false,
IsBeta = false,
Name = "Simple Unit Testing",
SupportedPlatforms = new PlatformType[] {PlatformType.Windows},
Version = new Version(1,0),
RepositoryUrl = "https://github.com/klukule/flax-ut"
SupportedPlatforms = new PlatformType[] { PlatformType.Windows },
Version = new Version(1, 1),
RepositoryUrl = "https://github.com/FlaxCommunityProjects/FlaxUnitTesting"
};

public override void InitializeEditor()
{
base.InitializeEditor();

mmBtn = Editor.UI.MainMenu.AddButton("Unit Tests");
mmBtn.ContextMenu.AddButton("Run unit tests").Clicked += RunTests;
_mmBtn = Editor.UI.MainMenu.AddButton("Unit Tests");
_mmBtn.ContextMenu.AddButton("Run unit tests").Clicked += RunTests;
FlaxEditor.Scripting.ScriptsBuilder.ScriptsReloadBegin += ScriptsBuilder_ScriptsReloadBegin;
}

private void ScriptsBuilder_ScriptsReloadBegin()
{
// Clear type information as per warning https://docs.flaxengine.com/manual/scripting/plugins/index.html
_suites.Clear();
}

public override void Deinitialize()
{
base.Deinitialize();
if (mmBtn != null)
if (_mmBtn != null)
{
mmBtn.Dispose();
mmBtn = null;
_mmBtn.Dispose();
_mmBtn = null;
}
}

private static void GatherTests()
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
suites.Clear();
_suites.Clear();
foreach (var assembly in assemblies)
foreach (var type in assembly.GetTypes())
if (type.GetCustomAttributes<TestFixture>().Count() > 0)
suites.Add(type);
_suites.Add(type);
}

public static void RunTests()
{
GatherTests();

foreach (var suite in suites)
foreach (var suite in _suites)
{
var tests = suite.GetMethods().Where(m => m.GetCustomAttributes<TestCase>().Count() > 0 || m.GetCustomAttributes<Test>().Count() > 0).ToArray();
var setup = suite.GetMethods().Where(m => m.GetCustomAttributes<SetUp>().Count() > 0).FirstOrDefault();
var disposer = suite.GetMethods().Where(m => m.GetCustomAttributes<TearDown>().Count() > 0).FirstOrDefault();
var suiteMethods = suite.GetMethods();

var tests = suiteMethods.Where(m => m.GetCustomAttributes<Test>().Count() > 0 || m.GetCustomAttributes<TestCase>().Count() > 0).ToArray();
var setup = suiteMethods.Where(m => m.GetCustomAttributes<OneTimeSetUp>().Count() > 0).FirstOrDefault();
var disposer = suiteMethods.Where(m => m.GetCustomAttributes<OneTimeTearDown>().Count() > 0).FirstOrDefault();
var beforeEach = suiteMethods.Where(m => m.GetCustomAttributes<SetUp>().Count() > 0).FirstOrDefault();
var afterEach = suiteMethods.Where(m => m.GetCustomAttributes<TearDown>().Count() > 0).FirstOrDefault();

var instance = Activator.CreateInstance(suite);

setup?.Invoke(instance, null);

foreach (var testMethod in tests)
{
// Mitigates the AttributeNullException
foreach (var test in testMethod.GetCustomAttributes<Test>())
if (testMethod.GetCustomAttributes<Test>().Count() > 0)
{
bool failed = false;
beforeEach?.Invoke(instance, null);
try
{
testMethod?.Invoke(instance, null);
}
catch (Exception e)
catch (TargetInvocationException e)
{
if(e.GetType() != typeof(SuccessException))
if (!(e.InnerException is SuccessException))
failed = true;
}
catch (Exception e)
{
failed = true;
}
finally
{
afterEach?.Invoke(instance, null);
Debug.Log($"Test '{suite.Name} {testMethod.Name}' finished with " + (failed ? "Error" : "Success"));
}
}

var testCases = testMethod.GetCustomAttributes<TestCase>();
int successCount = 0;
foreach (var testCase in testCases)
else
{
bool failed = false;
try
var testCases = testMethod.GetCustomAttributes<TestCase>();
int successCount = 0;
foreach (var testCase in testCases)
{
var result = testMethod?.Invoke(instance, testCase.Attributes);
if (testCase.ExpectedResult != null)
failed = !testCase.ExpectedResult.Equals(result);
}
catch (Exception e)
{
if(e.GetType() != typeof(SuccessException))
bool failed = false;
beforeEach?.Invoke(instance, null);
try
{
var result = testMethod?.Invoke(instance, testCase.Attributes);
if (testCase.ExpectedResult != null)
failed = !testCase.ExpectedResult.Equals(result);
}
catch (TargetInvocationException e)
{
if (!(e.InnerException is SuccessException))
failed = true;
}
catch (Exception e)
{
failed = true;
}
finally
{
afterEach?.Invoke(instance, null);

if (!failed)
successCount++;
}
}
finally
{
if (!failed)
successCount++;
}
}

if(testCases.Count() > 0)
Debug.Log($"Test '{suite.Name} {testMethod.Name}' finished with {successCount}/{testCases.Count()} successfull test cases.");
}
}

disposer?.Invoke(instance, null);
Expand Down
Loading