diff --git a/org.mixedrealitytoolkit.core/CHANGELOG.md b/org.mixedrealitytoolkit.core/CHANGELOG.md
index df03116c9..4c6da0596 100644
--- a/org.mixedrealitytoolkit.core/CHANGELOG.md
+++ b/org.mixedrealitytoolkit.core/CHANGELOG.md
@@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
* Updated code style in `HandsSubsystemDescriptor`, `MRTKSubsystemDescriptor`, `DictationSubsystemDescriptor`, and `XRSubsystemHelpers`. [PR #1109](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1109)
+* `AssemblyExtensions.GetLoadableTypes` now throws `ArgumentNullException` when called on a null assembly. [PR #1122](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1122)
+
+### Added
+
+* Added edit mode tests for `AssemblyExtensions`, `SystemType`, and `SerializableDictionary`. [PR #1122](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1122)
### Fixed
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs
new file mode 100644
index 000000000..0ceffcba0
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace MixedReality.Toolkit.Core.Tests.EditMode
+{
+ ///
+ /// Unit tests for AssemblyExtensions.
+ /// These run outside of PlayMode and do not require Unity engine initialization.
+ ///
+ public class AssemblyExtensionsTests
+ {
+ [Test]
+ public void GetLoadableTypes_ReturnsTypes_ForValidAssembly()
+ {
+ Assembly currentAssembly = Assembly.GetExecutingAssembly();
+
+ IEnumerable types = currentAssembly.GetLoadableTypes();
+
+ Assert.IsNotNull(types, "GetLoadableTypes should never return null.");
+ Assert.Greater(types.Count(), 0, "GetLoadableTypes should return the types within the executing assembly.");
+ Assert.IsTrue(types.Contains(typeof(AssemblyExtensionsTests)), "GetLoadableTypes failed to return known loadable types.");
+ }
+
+ [Test]
+ public void GetLoadableTypes_HandlesNullAssembly_Safely()
+ {
+ Assembly nullAssembly = null;
+ Assert.Throws(() => nullAssembly.GetLoadableTypes());
+ }
+ }
+}
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta
new file mode 100644
index 000000000..46f2addd4
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 32e95d882d3d7c44ca4f53530fed61b0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs
new file mode 100644
index 000000000..7f99497c5
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+using NUnit.Framework;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace MixedReality.Toolkit.Core.Tests.EditMode
+{
+ ///
+ /// Unit tests for SerializableDictionary.
+ /// These run outside of PlayMode and do not require Unity engine initialization.
+ ///
+ public class SerializableDictionaryTests
+ {
+ [Test]
+ public void Serialization_RestoresDictionary_FromInternalEntries()
+ {
+ var dict = new SerializableDictionary();
+ dict.Add("Key1", 100);
+ dict.Add("Key2", 200);
+
+ ISerializationCallbackReceiver receiver = dict;
+
+ // Simulate Unity preparing to serialize the object (populates the internal 'entries' list)
+ receiver.OnBeforeSerialize();
+
+ // Clear the base dictionary to simulate starting fresh after deserialization
+ ((Dictionary)dict).Clear();
+ Assert.AreEqual(0, dict.Count, "Base dictionary should be empty before deserialization.");
+
+ // Simulate Unity finishing deserialization (repopulates the dictionary from 'entries')
+ receiver.OnAfterDeserialize();
+
+ Assert.AreEqual(2, dict.Count, "Dictionary should have restored 2 items.");
+ Assert.AreEqual(100, dict["Key1"]);
+ Assert.AreEqual(200, dict["Key2"]);
+ }
+
+ [Test]
+ public void EditorOverride_Clear_RemovesAllSerializedEntries()
+ {
+ var dict = new SerializableDictionary();
+ dict.Add("Key1", 100);
+
+ ISerializationCallbackReceiver receiver = dict;
+ receiver.OnBeforeSerialize(); // Populate internal list
+
+ // Call the custom overridden Clear()
+ dict.Clear();
+
+ // Attempt to deserialize (which would normally restore Key1 if the internal list wasn't cleared)
+ receiver.OnAfterDeserialize();
+
+ Assert.AreEqual(0, dict.Count, "Dictionary should remain empty because the internal serialized entries were cleared.");
+ }
+
+ [Test]
+ public void EditorOverride_Remove_RemovesSpecificSerializedEntry()
+ {
+ var dict = new SerializableDictionary();
+ dict.Add("A", 1);
+ dict.Add("B", 2);
+ dict.Add("C", 3);
+
+ ISerializationCallbackReceiver receiver = dict;
+ receiver.OnBeforeSerialize();
+
+ // Use the overridden remove method which should also remove from the internal list
+ bool removedB = dict.Remove("B");
+ bool removedC = dict.Remove("C", out int valC);
+
+ // Clear the dictionary and restore from the serialized list to verify they are gone
+ ((Dictionary)dict).Clear();
+ receiver.OnAfterDeserialize();
+
+ Assert.IsTrue(removedB, "Remove(key) should return true for existing key.");
+ Assert.IsTrue(removedC, "Remove(key, out val) should return true for existing key.");
+ Assert.AreEqual(3, valC, "Remove(key, out val) should output the correct value.");
+
+ Assert.AreEqual(1, dict.Count, "Only 1 item should remain after deserialization.");
+ Assert.IsTrue(dict.ContainsKey("A"), "Key 'A' should have been preserved.");
+ Assert.IsFalse(dict.ContainsKey("B"), "Key 'B' should have been permanently removed.");
+ Assert.IsFalse(dict.ContainsKey("C"), "Key 'C' should have been permanently removed.");
+ }
+ }
+}
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta
new file mode 100644
index 000000000..77e68063f
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2801ac5575b6e5044ac92e55fe647979
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs
new file mode 100644
index 000000000..a1d62be59
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Mixed Reality Toolkit Contributors
+// Licensed under the BSD 3-Clause
+
+using NUnit.Framework;
+using System;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace MixedReality.Toolkit.Core.Tests.EditMode
+{
+ ///
+ /// Unit tests for SystemType.
+ /// These run outside of PlayMode and do not require Unity engine initialization.
+ ///
+ public class SystemTypeTests
+ {
+ [Test]
+ public void GetReference_FromType_ReturnsValidString()
+ {
+ string reference = SystemType.GetReference(typeof(Vector3));
+
+ Assert.IsFalse(string.IsNullOrEmpty(reference));
+ Assert.IsTrue(reference.Contains("UnityEngine.Vector3"));
+ Assert.IsTrue(reference.Contains("UnityEngine.CoreModule"));
+ }
+
+ [Test]
+ public void GetReference_NullOrEmpty_ReturnsEmptyString()
+ {
+ Assert.AreEqual(string.Empty, SystemType.GetReference((Type)null));
+ Assert.AreEqual(string.Empty, SystemType.GetReference((string)null));
+ Assert.AreEqual(string.Empty, SystemType.GetReference(string.Empty));
+ }
+
+ [Test]
+ public void Constructors_SetPropertiesCorrectly()
+ {
+ Type targetType = typeof(int);
+
+ // Initialize from Type
+ SystemType fromType = new SystemType(targetType);
+ Assert.AreEqual(targetType, fromType.Type);
+ Assert.AreEqual(SystemType.GetReference(targetType), (string)fromType);
+
+ // Initialize from AssemblyQualifiedName
+ SystemType fromString = new SystemType(targetType.AssemblyQualifiedName);
+ Assert.AreEqual(targetType, fromString.Type);
+ }
+
+ [Test]
+ public void Constructor_AbstractType_SetsTypeToNull()
+ {
+ // SystemType is intentionally designed to nullify abstract types when initialized via string
+ SystemType fromString = new SystemType(typeof(Array).AssemblyQualifiedName);
+ Assert.IsNull(fromString.Type, "SystemType should not allow abstract types when initialized from an assembly string.");
+ }
+
+ [Test]
+ public void InvalidTypeAssignment_LogsError_ButSetsType()
+ {
+ SystemType sysType = new SystemType(typeof(int));
+
+ // Enums violate the ValidConstraint. SystemType logs an error but still completes the assignment.
+ LogAssert.Expect(LogType.Error, $"'{typeof(DayOfWeek).FullName}' is not a valid class or struct type.");
+
+ sysType.Type = typeof(DayOfWeek);
+
+ Assert.AreEqual(typeof(DayOfWeek), sysType.Type);
+ }
+
+ [Test]
+ public void ImplicitConversions_WorkCorrectly()
+ {
+ Type originalType = typeof(string);
+
+ // Type -> SystemType
+ SystemType sysType = originalType;
+ Assert.IsNotNull(sysType);
+
+ // SystemType -> Type
+ Type convertedType = sysType;
+ Assert.AreEqual(originalType, convertedType);
+
+ // SystemType -> string
+ string reference = sysType;
+ Assert.AreEqual(SystemType.GetReference(originalType), reference);
+ }
+
+ [Test]
+ public void Equality_MatchesSameTypes()
+ {
+ SystemType type1 = new SystemType(typeof(float));
+ SystemType type2 = new SystemType(typeof(float));
+ SystemType type3 = new SystemType(typeof(double));
+
+ Assert.IsTrue(type1.Equals(type2));
+ Assert.IsFalse(type1.Equals(type3));
+ Assert.IsFalse(type1.Equals(null));
+
+ // HashCodes should match for identical references
+ Assert.AreEqual(type1.GetHashCode(), type2.GetHashCode());
+ }
+
+ [Test]
+ public void Serialization_RestoresType()
+ {
+ var sysType = new SystemType(typeof(int));
+ ISerializationCallbackReceiver receiver = sysType;
+
+ // SystemType uses OnAfterDeserialize to re-establish the `type` mapping from the string `reference`
+ receiver.OnAfterDeserialize();
+
+ Assert.AreEqual(typeof(int), sysType.Type);
+ }
+ }
+}
diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta
new file mode 100644
index 000000000..f6e786970
--- /dev/null
+++ b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bff9d511c00e10941baf652aeb49d861
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs b/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs
index 8eba33c1e..59e4586da 100644
--- a/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs
+++ b/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs
@@ -14,10 +14,15 @@ namespace MixedReality.Toolkit
public static class AssemblyExtensions
{
///
- /// Assembly.GetTypes() can throw in some cases. This extension will catch that exception and return only the types which were successfully loaded from the assembly.
+ /// Assembly.GetTypes() can throw in some cases. This extension will catch that exception and return only the types which were successfully loaded from the assembly.
///
public static IEnumerable GetLoadableTypes(this Assembly @this)
{
+ if (@this == null)
+ {
+ throw new ArgumentNullException(nameof(@this), "Assembly cannot be null.");
+ }
+
try
{
return @this.GetTypes();