diff --git a/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs b/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs index 4b30857a..6b201f0c 100644 --- a/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs +++ b/Assets/UIComponents.Benchmarks/BenchmarkUtils.cs @@ -1,10 +1,11 @@ -using Unity.PerformanceTesting; +using UIComponents.DependencyInjection; +using Unity.PerformanceTesting; namespace UIComponents.Benchmarks { public static class BenchmarkUtils { - public const string Version = "0.18.0.0"; + public const string Version = "0.19.0.0"; private static SampleGroup[] GetProfilerMarkers() { @@ -23,7 +24,7 @@ private static SampleGroup[] GetProfilerMarkers() .SetUp(() => { UIComponent.ClearCache(); - DependencyInjector.Container.Clear(); + DiContext.Current.Container.Clear(); }) .SampleGroup(new SampleGroup("Cold Cache Time")) .ProfilerMarkers(GetProfilerMarkers()) diff --git a/Assets/UIComponents.Tests/AssetPathAttributeTests.cs b/Assets/UIComponents.Tests/AssetPathAttributeTests.cs index 6efe4e4a..ecb3347f 100644 --- a/Assets/UIComponents.Tests/AssetPathAttributeTests.cs +++ b/Assets/UIComponents.Tests/AssetPathAttributeTests.cs @@ -1,6 +1,7 @@ using System.Linq; using NSubstitute; using NUnit.Framework; +using UIComponents.Testing; using UIComponents.Tests.Utilities; namespace UIComponents.Tests @@ -15,27 +16,29 @@ private class UIComponentWithAssetPaths : UIComponent {} [AssetPath("Path3")] [AssetPath("Path4")] private class UIComponentSubclassWithAssetPaths : UIComponentWithAssetPaths {} + + private TestBed _testBed; + private IAssetResolver _mockResolver; - private IAssetResolver _resolver; - - [OneTimeSetUp] - public void OneTimeSetUp() + [SetUp] + public void SetUp() { - _resolver = MockUtilities.CreateMockResolver(); - DependencyInjector.SetDependency(_resolver); + _mockResolver = MockUtilities.CreateMockResolver(); + _testBed = TestBed.Create() + .WithSingleton(_mockResolver) + .Build(); } [TearDown] public void TearDown() { - _resolver.ClearReceivedCalls(); - DependencyInjector.ResetProvidedInstance(); + _mockResolver.ClearReceivedCalls(); } [Test] public void Specified_Asset_Paths_Are_Stored() { - var component = new UIComponentWithAssetPaths(); + var component = _testBed.CreateComponent(); var assetPaths = component.GetAssetPaths().ToList(); @@ -48,7 +51,7 @@ public void Specified_Asset_Paths_Are_Stored() [Test] public void Asset_Path_Are_Inherited_From_Parent_Classes() { - var component = new UIComponentSubclassWithAssetPaths(); + var component = _testBed.CreateComponent(); var assetPaths = component.GetAssetPaths().ToList(); diff --git a/Assets/UIComponents.Tests/DependencyInjectorStaticTests.cs b/Assets/UIComponents.Tests/DependencyInjectorStaticTests.cs deleted file mode 100644 index 6fb4a074..00000000 --- a/Assets/UIComponents.Tests/DependencyInjectorStaticTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using NUnit.Framework; - -namespace UIComponents.Tests -{ - [TestFixture] - public class DependencyInjectorStaticTests - { - private interface IDependency {} - - private class DependencyProvider : IDependency {} - - [Dependency(typeof(IDependency), provide: typeof(DependencyProvider))] - private class UIComponentWithDependency : UIComponent - { - public IDependency GetDependency() => Provide(); - } - - [TestFixture] - public class ClearDependency - { - [Test] - public void Removes_The_Dependency_Instance() - { - var component = new UIComponentWithDependency(); - - DependencyInjector.ClearDependency(); - - Assert.Throws(() => component.GetDependency()); - } - - [TearDown] - public void TearDown() - { - DependencyInjector.SetDependency( - new DependencyProvider() - ); - } - } - - [TestFixture] - public class RemoveInjector - { - [Test] - public void Removes_The_Injector_From_The_Dependency_Injector() - { - var componentType = typeof(UIComponentWithDependency); - var injector = new DependencyInjector(); - DependencyInjector.Container.InjectorDictionary[componentType] = injector; - DependencyInjector.RemoveInjector(componentType); - Assert.That(DependencyInjector.Container.InjectorDictionary.ContainsKey(componentType), Is.False); - } - } - - [TestFixture] - public class ResetProvidedInstance - { - [Test] - public void Resets_The_Dependency_Instance() - { - var component = new UIComponentWithDependency(); - - DependencyInjector.ClearDependency(); - DependencyInjector.ResetProvidedInstance(); - - Assert.That(component.GetDependency(), Is.InstanceOf()); - } - } - } -} \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DependencyInjectorTests.cs b/Assets/UIComponents.Tests/DependencyInjectorTests.cs index 1cc448ba..add1b667 100644 --- a/Assets/UIComponents.Tests/DependencyInjectorTests.cs +++ b/Assets/UIComponents.Tests/DependencyInjectorTests.cs @@ -1,5 +1,6 @@ using System; using NUnit.Framework; +using UIComponents.DependencyInjection; namespace UIComponents.Tests { @@ -20,7 +21,7 @@ public void Can_Be_Created_Using_DependencyAttributes() new DependencyAttribute(typeof(IDependency), typeof(DependencyOne)) }; - var injector = new DependencyInjector(dependencyAttributes); + var injector = new DependencyInjector(dependencyAttributes, DiContext.Current.Container); Assert.That(injector.Provide(), Is.InstanceOf()); } @@ -31,7 +32,7 @@ public class Provide [Test] public void Returns_Desired_Dependency() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); injector.SetDependency(new DependencyOne()); Assert.That(injector.Provide(), Is.InstanceOf()); } @@ -39,7 +40,7 @@ public void Returns_Desired_Dependency() [Test] public void Throws_If_No_Provider_Exists() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); var exception = Assert.Throws( () => injector.Provide() @@ -55,7 +56,7 @@ public class TryProvide [Test] public void Returns_If_Dependency_Could_Be_Provided() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); Assert.That(injector.TryProvide(out _), Is.False); @@ -70,7 +71,7 @@ public void Returns_If_Dependency_Could_Be_Provided() [Test] public void Yields_Null_If_Dependency_Can_Not_Be_Provided() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); injector.TryProvide(out var instance); @@ -84,7 +85,7 @@ public class SetDependency [Test] public void Switches_The_Dependency() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); injector.SetDependency(new DependencyOne()); injector.SetDependency(new DependencyTwo()); @@ -95,7 +96,7 @@ public void Switches_The_Dependency() [Test] public void Throws_Exception_If_Null_Is_Given_As_Parameter() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); Assert.Throws( () => injector.SetDependency(null) @@ -109,7 +110,7 @@ public class ClearDependency [Test] public void Removes_Dependency_Instance() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); injector.SetDependency(new DependencyOne()); @@ -121,7 +122,7 @@ public void Removes_Dependency_Instance() [Test] public void Does_Not_Throw_If_Dependency_Does_Not_Exist() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); Assert.DoesNotThrow(() => injector.ClearDependency()); } @@ -133,13 +134,13 @@ public class ResetProvidedInstance [SetUp] public void SetUp() { - DependencyInjector.Container.Clear(); + DiContext.Current.Container.Clear(); } [Test] public void Throws_If_No_Default_Dependency_Exists() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); Assert.Throws( () => injector.ResetProvidedInstance() @@ -157,7 +158,7 @@ public void Throws_If_No_Default_Dependency_Exists() [Test] public void Restores_Singleton_Instance() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); var singletonInstance = new DependencyOne(); @@ -171,7 +172,7 @@ public void Restores_Singleton_Instance() [Test] public void Creates_New_Transient_Instance() { - var injector = new DependencyInjector(); + var injector = new DependencyInjector(DiContext.Current.Container); var transientInstance = new DependencyOne(); injector.SetDependency(transientInstance, Scope.Transient); diff --git a/Assets/UIComponents.Tests/DependencyScopeTests.cs b/Assets/UIComponents.Tests/DependencyScopeTests.cs deleted file mode 100644 index 6d4ed257..00000000 --- a/Assets/UIComponents.Tests/DependencyScopeTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using NUnit.Framework; -using UIComponents.Utilities; - -namespace UIComponents.Tests -{ - [TestFixture] - public class DependencyScopeTests - { - public interface IDependency {} - - public class DependencyClass : IDependency {} - - public class NewDependencyClass : IDependency {} - - public class UIComponentWithNoDependencyAttribute : UIComponent - { - public IDependency GetDependency() => Provide(); - } - - [Dependency(typeof(IDependency), provide: typeof(DependencyClass))] - public class UIComponentWithDependency : UIComponentWithNoDependencyAttribute {} - - [Test] - public void Should_Set_Dependency_And_Restore_Previous() - { - var component = new UIComponentWithDependency(); - - using (new DependencyScope(new NewDependencyClass())) - { - Assert.That(component.GetDependency(), Is.InstanceOf()); - } - - Assert.That(component.GetDependency(), Is.InstanceOf()); - } - - [Test] - public void Should_Set_Dependency_And_Clear_It_If_No_Previous_Exists() - { - var component = new UIComponentWithNoDependencyAttribute(); - - using (new DependencyScope(new NewDependencyClass())) - { - Assert.That(component.GetDependency(), Is.InstanceOf()); - } - - Assert.Throws(() => component.GetDependency()); - } - } -} \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DependencyScopeTests.cs.meta b/Assets/UIComponents.Tests/DependencyScopeTests.cs.meta deleted file mode 100644 index ee0a94e3..00000000 --- a/Assets/UIComponents.Tests/DependencyScopeTests.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 254e309568934e58b521710a4a902cb8 -timeCreated: 1652280678 \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DependencyTests.cs b/Assets/UIComponents.Tests/DependencyTests.cs index 406f3388..2a7ed6a3 100644 --- a/Assets/UIComponents.Tests/DependencyTests.cs +++ b/Assets/UIComponents.Tests/DependencyTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using UIComponents.DependencyInjection; namespace UIComponents.Tests { diff --git a/Assets/UIComponents.Tests/DiContainerTests.cs b/Assets/UIComponents.Tests/DiContainerTests.cs new file mode 100644 index 00000000..ded40abe --- /dev/null +++ b/Assets/UIComponents.Tests/DiContainerTests.cs @@ -0,0 +1,72 @@ +using System; +using NUnit.Framework; +using UIComponents.DependencyInjection; + +namespace UIComponents.Tests +{ + [TestFixture] + public class DiContainerTests + { + private static readonly Type StringType = typeof(string); + + [Test] + public void Singleton_Instance_Can_Be_Set() + { + var container = new DiContainer(); + + container.RegisterSingletonInstance(StringType, "Hello world"); + + var couldFetchValue = container.TryGetSingletonInstance(StringType, out var text); + + Assert.That(couldFetchValue, Is.True); + Assert.That(text, Is.EqualTo("Hello world")); + } + + [Test] + public void Singleton_Instance_Existence_Can_Be_Queried() + { + var container = new DiContainer(); + + Assert.That(container.ContainsSingletonInstanceOfType(StringType), Is.False); + container.RegisterSingletonInstance(StringType, "Hello world"); + Assert.That(container.ContainsSingletonInstanceOfType(StringType), Is.True); + } + + [Test] + public void Null_Can_Not_Be_Set_As_Singleton_Instance() + { + var container = new DiContainer(); + + Assert.That(() => container.RegisterSingletonInstance(StringType, null), Throws.ArgumentNullException); + } + + [Test] + public void Singleton_Override_Can_Be_Set() + { + var container = new DiContainer(); + + Assert.That(container.TryGetSingletonOverride(StringType, out _), Is.False); + container.SetSingletonOverride("Hello world"); + Assert.That(container.TryGetSingletonOverride(StringType, out var text), Is.True); + Assert.That(text, Is.EqualTo("Hello world")); + } + + [Test] + public void Singleton_Override_Can_Not_Be_Null() + { + var container = new DiContainer(); + + Assert.That(() => container.SetSingletonOverride((string) null), Throws.ArgumentNullException); + } + + [Test] + public void Singleton_Override_Can_Be_Removed() + { + var container = new DiContainer(); + + container.SetSingletonOverride("Hello world"); + container.RemoveSingletonOverride(); + Assert.That(container.TryGetSingletonOverride(StringType, out _), Is.False); + } + } +} \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DiContainerTests.cs.meta b/Assets/UIComponents.Tests/DiContainerTests.cs.meta new file mode 100644 index 00000000..4f10fb23 --- /dev/null +++ b/Assets/UIComponents.Tests/DiContainerTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 807642a88cf14b5684c8e75c142cdeb3 +timeCreated: 1661005972 \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DiContextTests.cs b/Assets/UIComponents.Tests/DiContextTests.cs new file mode 100644 index 00000000..2dfa580d --- /dev/null +++ b/Assets/UIComponents.Tests/DiContextTests.cs @@ -0,0 +1,163 @@ +using System; +using NUnit.Framework; +using UIComponents.DependencyInjection; + +namespace UIComponents.Tests +{ + [TestFixture] + public class DiContextTests + { + private interface IDependency {} + + private class DependencyProvider : IDependency {} + + [Dependency(typeof(IDependency), provide: typeof(DependencyProvider))] + private class UIComponentWithDependency : UIComponent + { + public IDependency GetDependency() => Provide(); + } + + [TestFixture] + public class SetCurrent + { + [Test] + public void Sets_Current_Context() + { + var previousContext = DiContext.Current; + var context = new DiContext(); + DiContext.SetCurrent(context); + Assert.That(DiContext.Current, Is.SameAs(context)); + Assert.That(DiContext.Previous, Is.SameAs(previousContext)); + DiContext.SetCurrent(previousContext); + } + + [Test] + public void Does_Not_Set_Previous_If_Given_Context_Is_Same_As_Current() + { + var previousContext = DiContext.Current; + var context = new DiContext(); + DiContext.SetCurrent(context); + Assert.That(DiContext.Previous, Is.SameAs(previousContext)); + DiContext.SetCurrent(context); + Assert.That(DiContext.Previous, Is.SameAs(previousContext)); + DiContext.SetCurrent(previousContext); + } + + [Test] + public void Throws_ArgumentNullException_If_Given_Context_Is_Null() + { + Assert.Throws(() => DiContext.SetCurrent(null)); + } + } + + [TestFixture] + public class SwitchContainer + { + [Test] + public void Should_Switch_Container() + { + var context = new DiContext(); + DiContext.SetCurrent(context); + var dependency = new DependencyProvider(); + + context.SetDependency(dependency); + + var component = new UIComponentWithDependency(); + Assert.That(component.GetDependency(), Is.EqualTo(dependency)); + + var otherContainer = new DiContainer(); + var dependencyTwo = new DependencyProvider(); + + otherContainer + .GetInjector(typeof(UIComponentWithDependency)) + .SetDependency(dependencyTwo); + + context.SwitchContainer(otherContainer); + + component = new UIComponentWithDependency(); + Assert.That(component.GetDependency(), Is.Not.EqualTo(dependency)); + Assert.That(component.GetDependency(), Is.EqualTo(dependencyTwo)); + DiContext.SetCurrent(DiContext.Previous); + } + + [Test] + public void Throws_ArgumentNullException_If_Given_Container_Is_Null() + { + var context = new DiContext(); + Assert.Throws(() => context.SwitchContainer(null)); + } + } + + [TestFixture] + public class ClearDependency + { + [Test] + public void Removes_The_Dependency_Instance() + { + var diContext = new DiContext(); + DiContext.SetCurrent(diContext); + var component = new UIComponentWithDependency(); + + diContext.ClearDependency(); + + Assert.Throws(() => component.GetDependency()); + DiContext.SetCurrent(DiContext.Previous); + } + } + + [TestFixture] + public class SetDependency + { + [Test] + public void Sets_The_Dependency_Instance() + { + var diContext = new DiContext(); + DiContext.SetCurrent(diContext); + var component = new UIComponentWithDependency(); + var dependency = new DependencyProvider(); + + diContext.SetDependency(dependency); + + Assert.That(component.GetDependency(), Is.SameAs(dependency)); + + DiContext.SetCurrent(DiContext.Previous); + } + } + + [TestFixture] + public class RemoveInjector + { + [Test] + public void Removes_The_Injector() + { + var diContext = new DiContext(); + var componentType = typeof(UIComponentWithDependency); + var injector = new DependencyInjector(diContext.Container); + diContext.Container.InjectorDictionary[componentType] = injector; + diContext.RemoveInjector(componentType); + Assert.That(diContext.Container.InjectorDictionary.ContainsKey(componentType), Is.False); + } + } + + [TestFixture] + public class ResetProvidedInstance + { + [Test] + public void Resets_The_Dependency_Instance() + { + var diContext = new DiContext(); + + DiContext.SetCurrent(diContext); + + var component = new UIComponentWithDependency(); + + diContext.ClearDependency(); + diContext.ResetProvidedInstance(); + + Assert.That(component.GetDependency(), Is.InstanceOf()); + + DiContext.SetCurrent(DiContext.Previous); + } + } + } +} \ No newline at end of file diff --git a/Assets/UIComponents.Tests/DependencyInjectorStaticTests.cs.meta b/Assets/UIComponents.Tests/DiContextTests.cs.meta similarity index 100% rename from Assets/UIComponents.Tests/DependencyInjectorStaticTests.cs.meta rename to Assets/UIComponents.Tests/DiContextTests.cs.meta diff --git a/Assets/UIComponents.Tests/LayoutAttributeTests.cs b/Assets/UIComponents.Tests/LayoutAttributeTests.cs index 515c5bfc..b116696a 100644 --- a/Assets/UIComponents.Tests/LayoutAttributeTests.cs +++ b/Assets/UIComponents.Tests/LayoutAttributeTests.cs @@ -1,6 +1,7 @@ using NSubstitute; using NSubstitute.ReturnsExtensions; using NUnit.Framework; +using UIComponents.Testing; using UIComponents.Tests.Utilities; using UnityEngine.UIElements; @@ -20,71 +21,53 @@ private class InheritedComponentWithAttribute : UIComponentWithLayout {} [Layout("Assets/MissingAsset.uxml")] private class UIComponentWithNullLayout : UIComponent {} - private IAssetResolver _assetResolver; + private TestBed _testBed; + private IAssetResolver _mockResolver; - [OneTimeSetUp] - public void OneTimeSetUp() + [SetUp] + public void SetUp() { - _assetResolver = MockUtilities.CreateMockResolver(); - - _assetResolver.LoadAsset("Assets/MissingAsset.uxml") + _mockResolver = MockUtilities.CreateMockResolver(); + _mockResolver.LoadAsset("Assets/MissingAsset.uxml") .ReturnsNull(); - - DependencyInjector.SetDependency( - _assetResolver - ); - DependencyInjector.SetDependency( - _assetResolver - ); - DependencyInjector.SetDependency( - _assetResolver - ); - DependencyInjector.SetDependency( - _assetResolver - ); - } - [OneTimeTearDown] - public void OneTimeTearDown() - { - DependencyInjector.ResetProvidedInstance(); - DependencyInjector.ResetProvidedInstance(); - DependencyInjector.ResetProvidedInstance(); - DependencyInjector.ResetProvidedInstance(); + _testBed = TestBed.Create() + .WithSingleton(_mockResolver) + .Build(); } - + [TearDown] public void TearDown() { - _assetResolver.ClearReceivedCalls(); + _mockResolver.ClearReceivedCalls(); } [Test] public void Given_Layout_Is_Loaded() { - var component = new UIComponentWithLayout(); - _assetResolver.Received().LoadAsset("Assets/MyAsset.uxml"); + var component = _testBed.CreateComponent(); + _mockResolver.Received().LoadAsset("Assets/MyAsset.uxml"); } [Test] public void Superclass_Layout_Is_Loaded_If_It_Is_Not_Overridden() { - var component = new InheritedComponentWithoutAttribute(); - _assetResolver.Received().LoadAsset("Assets/MyAsset.uxml"); + var component = _testBed.CreateComponent(); + _mockResolver.Received().LoadAsset("Assets/MyAsset.uxml"); } [Test] public void Superclass_Layout_Is_Not_Loaded_If_Overridden() { - var component = new InheritedComponentWithAttribute(); - _assetResolver.Received().LoadAsset("Assets/MyOtherAsset.uxml"); - _assetResolver.DidNotReceive().LoadAsset("Assets/MyAsset.uxml"); + var component = _testBed.CreateComponent(); + _mockResolver.Received().LoadAsset("Assets/MyOtherAsset.uxml"); + _mockResolver.DidNotReceive().LoadAsset("Assets/MyAsset.uxml"); } [Test] public void Null_Layout_Is_Handled() { - Assert.DoesNotThrow(() => new UIComponentWithNullLayout()); + Assert.DoesNotThrow(() => _testBed.CreateComponent()); } } } \ No newline at end of file diff --git a/Assets/UIComponents.Tests/PathAttributeTests.cs b/Assets/UIComponents.Tests/PathAttributeTests.cs index 173a4fc3..e3173352 100644 --- a/Assets/UIComponents.Tests/PathAttributeTests.cs +++ b/Assets/UIComponents.Tests/PathAttributeTests.cs @@ -1,5 +1,6 @@ using NSubstitute; using NUnit.Framework; +using UIComponents.Testing; using UIComponents.Tests.Utilities; namespace UIComponents.Tests @@ -26,28 +27,23 @@ private class UIComponentWithValidAssetPath : UIComponent {} [AssetPath("Assets/Invalid/Path")] private class UIComponentWithInvalidAssetPath : UIComponent {} - private IAssetResolver _assetResolver; + private TestBed _testBed; + private IAssetResolver _mockResolver; - [OneTimeSetUp] - public void OneTimeSetUp() + [SetUp] + public void SetUp() { - _assetResolver = MockUtilities.CreateMockResolver(); - DependencyInjector.SetDependency(_assetResolver); - DependencyInjector.SetDependency(_assetResolver); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - DependencyInjector.ResetProvidedInstance(); - DependencyInjector.ResetProvidedInstance(); + _mockResolver = MockUtilities.CreateMockResolver(); + _testBed = TestBed.Create() + .WithSingleton(_mockResolver) + .Build(); } [Test] public void Should_Return_Empty_String_If_No_Path_Is_Configured() { var pathAttribute = new BasicPathAttribute(null); - var component = new UIComponentWithNoAssetPaths(); + var component = _testBed.CreateComponent(); var path = pathAttribute.GetAssetPathForComponent(component); @@ -59,7 +55,7 @@ public void Should_Ignore_AssetPath_In_Case_Of_Complete_Path() { var assetsPathAttribute = new BasicPathAttribute("Assets/Asset"); var packagesPathAttribute = new BasicPathAttribute("Packages/Asset"); - var component = new UIComponentWithValidAssetPath(); + var component = _testBed.CreateComponent(); var assetPath = assetsPathAttribute.GetAssetPathForComponent(component); var packagePath = packagesPathAttribute.GetAssetPathForComponent(component); @@ -73,11 +69,11 @@ public void Should_Use_Valid_AssetPath_In_Case_Of_Incomplete_Path() { var pathAttribute = new BasicPathAttribute("MyAsset"); - _assetResolver.AssetExists("Assets/Valid/Path/MyAsset").Returns(true); - _assetResolver.AssetExists("Assets/Invalid/Path/MyAsset").Returns(false); + _mockResolver.AssetExists("Assets/Valid/Path/MyAsset").Returns(true); + _mockResolver.AssetExists("Assets/Invalid/Path/MyAsset").Returns(false); - var componentWithValidPath = new UIComponentWithValidAssetPath(); - var componentWithInvalidPath = new UIComponentWithInvalidAssetPath(); + var componentWithValidPath = _testBed.CreateComponent(); + var componentWithInvalidPath = _testBed.CreateComponent(); var filledPath = pathAttribute.GetAssetPathForComponent(componentWithValidPath); var ignoredPath = pathAttribute.GetAssetPathForComponent(componentWithInvalidPath); diff --git a/Assets/UIComponents.Tests/ProvideAttributeTests.cs b/Assets/UIComponents.Tests/ProvideAttributeTests.cs index 0b488f48..945023be 100644 --- a/Assets/UIComponents.Tests/ProvideAttributeTests.cs +++ b/Assets/UIComponents.Tests/ProvideAttributeTests.cs @@ -1,7 +1,7 @@ using NSubstitute; using NUnit.Framework; using UIComponents.Experimental; -using UIComponents.Utilities; +using UIComponents.Testing; namespace UIComponents.Tests { @@ -56,12 +56,12 @@ private class ComponentWithInvalidDependency : UIComponent public void Does_Not_Throw_When_Provider_Is_Missing() { var logger = Substitute.For(); - - ComponentWithInvalidDependency component; - - using (new DependencyScope(logger)) - component = new ComponentWithInvalidDependency(); + var testBed = TestBed.Create() + .WithSingleton(logger) + .Build(); + var component = testBed.CreateComponent(); + Assert.That(component.StringProperty, Is.Null); logger.Received().LogError("Could not provide IStringProperty to StringProperty", component); } diff --git a/Assets/UIComponents.Tests/QueryAttributeTests.cs b/Assets/UIComponents.Tests/QueryAttributeTests.cs index 20e63c34..50b299b8 100644 --- a/Assets/UIComponents.Tests/QueryAttributeTests.cs +++ b/Assets/UIComponents.Tests/QueryAttributeTests.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using NSubstitute; using NUnit.Framework; -using UIComponents.Utilities; using UnityEngine.UIElements; namespace UIComponents.Tests diff --git a/Assets/UIComponents.Tests/StylesheetAttributeTests.cs b/Assets/UIComponents.Tests/StylesheetAttributeTests.cs index d331961e..7323ca77 100644 --- a/Assets/UIComponents.Tests/StylesheetAttributeTests.cs +++ b/Assets/UIComponents.Tests/StylesheetAttributeTests.cs @@ -1,8 +1,8 @@ using NSubstitute; using NSubstitute.ReturnsExtensions; using NUnit.Framework; +using UIComponents.Testing; using UIComponents.Tests.Utilities; -using UIComponents.Utilities; using UnityEngine; using UnityEngine.UIElements; @@ -17,70 +17,57 @@ private class UIComponentWithTwoStylesheets : UIComponent {} [Stylesheet("Assets/StylesheetThree.uss")] private class InheritedComponent : UIComponentWithTwoStylesheets {} - - private IAssetResolver _resolver; - [OneTimeSetUp] - public void OneTimeSetUp() + private TestBed _testBed; + private IAssetResolver _mockResolver; + private IUIComponentLogger _mockLogger; + + [SetUp] + public void SetUp() { - _resolver = MockUtilities.CreateMockResolver(); - _resolver.LoadAsset("Assets/StylesheetOne.uss") + _mockLogger = Substitute.For(); + _mockResolver = MockUtilities.CreateMockResolver(); + _mockResolver.LoadAsset("Assets/StylesheetOne.uss") .Returns(ScriptableObject.CreateInstance()); - _resolver.LoadAsset("Assets/StylesheetTwo.uss") + _mockResolver.LoadAsset("Assets/StylesheetTwo.uss") .Returns(ScriptableObject.CreateInstance()); - _resolver.LoadAsset("Assets/StylesheetThree.uss") + _mockResolver.LoadAsset("Assets/StylesheetThree.uss") .Returns(ScriptableObject.CreateInstance()); - DependencyInjector.SetDependency(_resolver); - DependencyInjector.SetDependency(_resolver); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - DependencyInjector.ResetProvidedInstance(); - DependencyInjector.ResetProvidedInstance(); - } - - [TearDown] - public void TearDown() - { - _resolver.ClearReceivedCalls(); + _testBed = TestBed.Create() + .WithSingleton(_mockLogger) + .WithSingleton(_mockResolver) + .Build(); } [Test] public void Given_Stylesheets_Are_Loaded() { - var component = new UIComponentWithTwoStylesheets(); - _resolver.Received().LoadAsset("Assets/StylesheetOne.uss"); - _resolver.Received().LoadAsset("Assets/StylesheetTwo.uss"); + var component = _testBed.CreateComponent(); + _mockResolver.Received().LoadAsset("Assets/StylesheetOne.uss"); + _mockResolver.Received().LoadAsset("Assets/StylesheetTwo.uss"); Assert.That(component.styleSheets.count, Is.EqualTo(2)); } [Test] public void Inherited_Stylesheets_Are_Loaded() { - var component = new InheritedComponent(); - _resolver.Received().LoadAsset("Assets/StylesheetOne.uss"); - _resolver.Received().LoadAsset("Assets/StylesheetTwo.uss"); - _resolver.Received().LoadAsset("Assets/StylesheetThree.uss"); + var component = _testBed.CreateComponent(); + _mockResolver.Received().LoadAsset("Assets/StylesheetOne.uss"); + _mockResolver.Received().LoadAsset("Assets/StylesheetTwo.uss"); + _mockResolver.Received().LoadAsset("Assets/StylesheetThree.uss"); Assert.That(component.styleSheets.count, Is.EqualTo(3)); } [Test] public void Invalid_Stylesheets_Output_Error_Message() { - _resolver.LoadAsset("Assets/StylesheetOne.uss") + _mockResolver.LoadAsset("Assets/StylesheetOne.uss") .ReturnsNull(); - var mockLogger = Substitute.For(); - - UIComponentWithTwoStylesheets component; - - using (new DependencyScope(mockLogger)) - component = new UIComponentWithTwoStylesheets(); + var component = _testBed.CreateComponent(); - _resolver.Received().LoadAsset("Assets/StylesheetOne.uss"); - mockLogger.Received().LogError("Could not find stylesheet Assets/StylesheetOne.uss", component); + _mockResolver.Received().LoadAsset("Assets/StylesheetOne.uss"); + _mockLogger.Received().LogError("Could not find stylesheet Assets/StylesheetOne.uss", component); Assert.That(component.styleSheets.count, Is.EqualTo(1)); } diff --git a/Assets/UIComponents.Tests/TestBedTests.cs b/Assets/UIComponents.Tests/TestBedTests.cs new file mode 100644 index 00000000..5def165b --- /dev/null +++ b/Assets/UIComponents.Tests/TestBedTests.cs @@ -0,0 +1,83 @@ +using NUnit.Framework; +using UIComponents.Testing; + +namespace UIComponents.Tests +{ + [TestFixture] + public class TestBedTests + { + private TestBed _testBed; + + private Dependency _dependencyInstance; + private TransientDependency _transientDependencyInstance; + + private interface IDependency {} + + private class Dependency : IDependency {} + + private interface ITransientDependency {} + + private class TransientDependency : ITransientDependency {} + + [Dependency(typeof(IDependency), provide: typeof(Dependency))] + [Dependency(typeof(ITransientDependency), provide: typeof(TransientDependency), Scope.Transient)] + private class Component : UIComponent + { + public readonly bool Value; + + public Component(bool value) + { + Value = value; + } + + public Component() : this(false) {} + + public IDependency GetDependency() => Provide(); + + public ITransientDependency GetTransientDependency() => Provide(); + } + + [SetUp] + public void SetUp() + { + _dependencyInstance = new Dependency(); + _transientDependencyInstance = new TransientDependency(); + _testBed = TestBed.Create() + .WithSingleton(_dependencyInstance) + .WithTransient(_transientDependencyInstance) + .Build(); + } + + [Test] + public void Allows_Creating_Component_With_Singleton_Dependencies() + { + var component = _testBed.CreateComponent(); + Assert.That(component.GetDependency(), Is.SameAs(_dependencyInstance)); + } + + [Test] + public void Allows_Creating_Component_With_Transient_Dependencies() + { + var component = _testBed.CreateComponent(); + Assert.That(component.GetTransientDependency(), Is.SameAs(_transientDependencyInstance)); + + var newComponent = _testBed.CreateComponent(); + Assert.That(newComponent.GetTransientDependency(), Is.SameAs(_transientDependencyInstance)); + } + + [Test] + public void Allows_Creating_Component_With_Predicate() + { + var component = _testBed.CreateComponent(() => new Component(true)); + Assert.That(component.Value, Is.True); + Assert.That(component.GetDependency(), Is.SameAs(_dependencyInstance)); + } + + [Test] + public void Allows_Fetching_Dependencies() + { + var dependency = _testBed.Provide(); + Assert.That(dependency, Is.SameAs(_dependencyInstance)); + } + } +} diff --git a/Assets/UIComponents.Tests/TestBedTests.cs.meta b/Assets/UIComponents.Tests/TestBedTests.cs.meta new file mode 100644 index 00000000..6d319fc4 --- /dev/null +++ b/Assets/UIComponents.Tests/TestBedTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a973a4da1584a8a837794a69608b4bd +timeCreated: 1659288545 \ No newline at end of file diff --git a/Assets/UIComponents.Tests/UIComponentCacheTests.cs b/Assets/UIComponents.Tests/UIComponentCacheTests.cs index e5855760..78a331a8 100644 --- a/Assets/UIComponents.Tests/UIComponentCacheTests.cs +++ b/Assets/UIComponents.Tests/UIComponentCacheTests.cs @@ -1,6 +1,6 @@ using NUnit.Framework; +using UIComponents.Testing; using UIComponents.Tests.Utilities; -using UIComponents.Utilities; namespace UIComponents.Tests { @@ -11,31 +11,35 @@ public class UIComponentCacheTests [Stylesheet("TestStylesheet")] private class TestComponent : UIComponent {} + private TestBed _testBed; + + [SetUp] + public void SetUp() + { + var resolver = MockUtilities.CreateMockResolver(); + + _testBed = TestBed.Create() + .WithSingleton(resolver) + .Build(); + } + [Test] public void CacheIsCreatedForComponent() { - var resolver = MockUtilities.CreateMockResolver(); - using (new DependencyScope(resolver)) - { - Assert.That(UIComponent.TryGetCache(out _), Is.False); - var component = new TestComponent(); - Assert.That(UIComponent.TryGetCache(out var cache), Is.True); - Assert.That(cache.LayoutAttribute, Is.Not.Null); - Assert.That(cache.StylesheetAttributes.Count, Is.EqualTo(1)); - } + Assert.That(UIComponent.TryGetCache(out _), Is.False); + var component = _testBed.CreateComponent(); + Assert.That(UIComponent.TryGetCache(out var cache), Is.True); + Assert.That(cache.LayoutAttribute, Is.Not.Null); + Assert.That(cache.StylesheetAttributes.Count, Is.EqualTo(1)); } [Test] public void CacheCanBeCleared() { - var resolver = MockUtilities.CreateMockResolver(); - using (new DependencyScope(resolver)) - { - var component = new TestComponent(); - Assert.That(UIComponent.TryGetCache(out _), Is.True); - UIComponent.ClearCache(); - Assert.That(UIComponent.TryGetCache(out _), Is.False); - } + var component = _testBed.CreateComponent(); + Assert.That(UIComponent.TryGetCache(out _), Is.True); + UIComponent.ClearCache(); + Assert.That(UIComponent.TryGetCache(out _), Is.False); } } } \ No newline at end of file diff --git a/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs b/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs index f0e8e65d..f777f9a4 100644 --- a/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs +++ b/Assets/UIComponents.Tests/UIComponentNoAttributesTests.cs @@ -1,6 +1,7 @@ using System.Linq; using NSubstitute; using NUnit.Framework; +using UIComponents.Testing; using UnityEngine.UIElements; namespace UIComponents.Tests @@ -9,48 +10,45 @@ namespace UIComponents.Tests public class UIComponentNoAttributesTests { private class UIComponentNoAttributes : UIComponent {} - - private IAssetResolver _resolver; - [OneTimeSetUp] - public void OneTimeSetUp() - { - _resolver = Substitute.For(); - DependencyInjector.SetDependency(_resolver); - } - - [OneTimeTearDown] - public void OneTimeTearDown() + private TestBed _testBed; + private IAssetResolver _mockResolver; + + [SetUp] + public void SetUp() { - DependencyInjector.ResetProvidedInstance(); + _mockResolver = Substitute.For(); + _testBed = TestBed.Create() + .WithSingleton(_mockResolver) + .Build(); } - + [TearDown] public void TearDown() { - _resolver.ClearReceivedCalls(); + _mockResolver.ClearReceivedCalls(); } [Test] public void No_Layout_Is_Loaded() { - var component = new UIComponentNoAttributes(); + var component = _testBed.CreateComponent(); Assert.That(component.childCount, Is.Zero); - _resolver.DidNotReceive().LoadAsset(Arg.Any()); + _mockResolver.DidNotReceive().LoadAsset(Arg.Any()); } [Test] public void No_Styles_Are_Loaded() { - var component = new UIComponentNoAttributes(); + var component = _testBed.CreateComponent(); Assert.That(component.styleSheets.count, Is.Zero); - _resolver.DidNotReceive().LoadAsset(Arg.Any()); + _mockResolver.DidNotReceive().LoadAsset(Arg.Any()); } [Test] public void No_Asset_Paths_Exist() { - var component = new UIComponentNoAttributes(); + var component = _testBed.CreateComponent(); Assert.That(component.GetAssetPaths().Count(), Is.EqualTo(0)); } } diff --git a/Assets/UIComponents/Core/DependencyInjection/Dependency.cs b/Assets/UIComponents/Core/DependencyInjection/Dependency.cs index 19aa116e..70b15ee5 100644 --- a/Assets/UIComponents/Core/DependencyInjection/Dependency.cs +++ b/Assets/UIComponents/Core/DependencyInjection/Dependency.cs @@ -1,7 +1,7 @@ using System; using JetBrains.Annotations; -namespace UIComponents +namespace UIComponents.DependencyInjection { /// /// An internal class for provided dependencies. diff --git a/Assets/UIComponents/Core/DependencyInjection/DependencyInjector.cs b/Assets/UIComponents/Core/DependencyInjection/DependencyInjector.cs index 4979122e..3f7bfb9f 100644 --- a/Assets/UIComponents/Core/DependencyInjection/DependencyInjector.cs +++ b/Assets/UIComponents/Core/DependencyInjection/DependencyInjector.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using JetBrains.Annotations; -namespace UIComponents +namespace UIComponents.DependencyInjection { /// /// The class responsible for providing UIComponents with their @@ -12,10 +12,7 @@ namespace UIComponents /// public class DependencyInjector { - /// - /// A container used with the static DependencyInjector methods. - /// - internal static readonly DiContainer Container = new DiContainer(); + private readonly DiContainer _container; /// /// Contains the dependencies the injector provides to its consumer. @@ -24,114 +21,77 @@ private readonly Dictionary _dependencyDictionary = new Dictionary(); /// - /// Switches the dependency of a consumer. - /// - /// - /// Can be used in unit tests to switch to - /// a mocked dependency. - /// - /// - /// The new instance used for the dependency - /// - /// Consumer type - /// Dependency type - public static void SetDependency(TDependency provider) - where TConsumer : class - where TDependency : class - { - var injector = GetInjector(typeof(TConsumer)); - - injector.SetDependency(provider); - } - - /// - /// Clears the dependency of a consumer. - /// - /// - /// Can be used in unit tests to clear - /// a dependency between tests. - /// - /// Consumer type - /// Dependency type - public static void ClearDependency() - where TConsumer : class - where TDependency : class - { - var injector = GetInjector(typeof(TConsumer)); - - injector.ClearDependency(); - } - - /// - /// Resets the provided instance of a dependency. - /// If a singleton instance exists, it is restored. - /// Otherwise, a new instance of the dependency is created. - /// - /// - /// Can be used in unit tests to clear - /// restore dependency between or after tests. - /// - /// Consumer type - /// Dependency type - /// - /// Thrown if no configured dependency exists - /// - public static void ResetProvidedInstance() - where TConsumer : class - where TDependency : class - { - var injector = GetInjector(typeof(TConsumer)); - - injector.ResetProvidedInstance(); - } - - /// - /// Returns the injector of the given consumer type. - /// - /// Consumer type - /// Injector of the consumer type - public static DependencyInjector GetInjector(Type consumerType) - { - return Container.GetInjector(consumerType); - } - - /// - /// Removes the injector of the given consumer type. - /// Used primarily for testing. + /// Constructs a new injector with no dependencies configured. /// - /// Consumer type - public static void RemoveInjector(Type consumerType) + /// Dependency injection container + public DependencyInjector(DiContainer container) { - Container.RemoveInjector(consumerType); + _container = container; } - /// - /// Constructs a new injector with no dependencies configured. - /// - public DependencyInjector() {} - /// /// Constructs a new injector with dependencies configured /// according to the given DependencyAttributes. /// /// Dependency attributes - public DependencyInjector(IEnumerable dependencyAttributes) + /// Dependency injection container + public DependencyInjector(IEnumerable dependencyAttributes, DiContainer container) : this(container) { foreach (var dependencyAttribute in dependencyAttributes) { var dependencyType = dependencyAttribute.DependencyType; - + if (_dependencyDictionary.ContainsKey(dependencyType)) continue; var providerType = dependencyAttribute.ProvideType; var scope = dependencyAttribute.Scope; - var dependency = Container.CreateDependency(dependencyType, providerType, scope); + var dependency = CreateDependency(dependencyType, providerType, scope); _dependencyDictionary.Add(dependencyType, dependency); } } + + private Dependency CreateSingletonDependency(Type dependencyType, Type providerType) + { + if (_container.TryGetSingletonOverride(dependencyType, out var overrideInstance)) + return new Dependency(dependencyType, overrideInstance, Scope.Singleton); + + if (_container.TryGetSingletonInstance(providerType, out var instance)) + return new Dependency(dependencyType, instance, Scope.Singleton); + + var dependency = new Dependency(dependencyType, providerType, Scope.Singleton); + + _container.RegisterSingletonInstance(providerType, dependency.Instance); + + return dependency; + } + + /// + /// Creates a new Dependency object with the given dependency and provider types. + /// The dependency instance is created using the provider type. + /// + private Dependency CreateDependency(Type dependencyType, Type providerType, Scope scope) + { + if (scope == Scope.Singleton) + return CreateSingletonDependency(dependencyType, providerType); + + return new Dependency(dependencyType, providerType, Scope.Transient); + } + + /// + /// Creates a new Dependency object with the dependency type and provided instance. + /// + private Dependency CreateDependency(Type dependencyType, object instance, Scope scope) + { + var instanceType = instance.GetType(); + + if (scope == Scope.Singleton && !_container.ContainsSingletonInstanceOfType(instanceType)) + _container.RegisterSingletonInstance(instanceType, instance); + + return new Dependency(dependencyType, instance, scope); + } /// /// Sets the instance used for a dependency. @@ -156,7 +116,7 @@ public void SetDependency([NotNull] T instance, Scope scope = Scope.Singleton return; } - var dependency = Container.CreateDependency(dependencyType, instance, scope); + var dependency = CreateDependency(dependencyType, instance, scope); _dependencyDictionary.Add(dependencyType, dependency); } @@ -195,7 +155,7 @@ public void ResetProvidedInstance() where T : class object instance = null; if (dependency.Scope == Scope.Singleton) - Container.TryGetSingletonInstance(initialProviderType, out instance); + _container.TryGetSingletonInstance(initialProviderType, out instance); if (instance == null) instance = Activator.CreateInstance(initialProviderType); @@ -269,4 +229,4 @@ public bool TryProvide(out T instance) where T : class return true; } } -} \ No newline at end of file +} diff --git a/Assets/UIComponents/Core/DependencyInjection/DiContainer.cs b/Assets/UIComponents/Core/DependencyInjection/DiContainer.cs index 020aa59b..d0233f27 100644 --- a/Assets/UIComponents/Core/DependencyInjection/DiContainer.cs +++ b/Assets/UIComponents/Core/DependencyInjection/DiContainer.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; -namespace UIComponents +namespace UIComponents.DependencyInjection { /// - /// An internal class for storing injectors and singleton instances. + /// A class for storing injectors and singleton instances. /// - internal class DiContainer + public class DiContainer { /// /// Contains all injectors created for consumers. @@ -16,24 +17,31 @@ internal class DiContainer /// /// Contains the instantiated singleton dependencies which are consumed /// by interested parties. + /// The key is the type of the instantiated value. /// - internal readonly Dictionary SingletonInstanceDictionary; - + private readonly Dictionary _singletonInstanceDictionary; + + /// + /// Contains singletons which are overridden for testing purposes. + /// The key is the type of dependency and the value is the singleton instance. + /// + private readonly Dictionary _singletonOverrideDictionary; + public DiContainer() { - InjectorDictionary = - new Dictionary(); + InjectorDictionary = new Dictionary(); - SingletonInstanceDictionary - = new Dictionary(); + _singletonInstanceDictionary = new Dictionary(); + + _singletonOverrideDictionary = new Dictionary(); } private DependencyInjector CreateInjector(Type consumerType) { var injectAttributes = (DependencyAttribute[]) consumerType.GetCustomAttributes(typeof(DependencyAttribute), true); - - var injector = new DependencyInjector(injectAttributes); + + var injector = new DependencyInjector(injectAttributes, this); InjectorDictionary.Add(consumerType, injector); @@ -69,7 +77,7 @@ public void RemoveInjector(Type consumerType) public void Clear() { InjectorDictionary.Clear(); - SingletonInstanceDictionary.Clear(); + _singletonInstanceDictionary.Clear(); } /// @@ -80,56 +88,59 @@ public void Clear() /// Whether the instance could be fetched public bool TryGetSingletonInstance(Type type, out object instance) { - return SingletonInstanceDictionary.TryGetValue(type, out instance); + return _singletonInstanceDictionary.TryGetValue(type, out instance); } - - private Dependency CreateSingletonDependency(Type dependencyType, Type providerType) + + /// Type of the singleton instance + /// Singleton instance to add + /// Thrown if instance is null + public void RegisterSingletonInstance(Type instanceType, [NotNull] object instance) { - if (SingletonInstanceDictionary.TryGetValue(providerType, out var instance)) - return new Dependency(dependencyType, instance, Scope.Singleton); - - var dependency = new Dependency(dependencyType, providerType, Scope.Singleton); - - SingletonInstanceDictionary.Add(providerType, dependency.Instance); + if (instance == null) + throw new ArgumentNullException(nameof(instance)); - return dependency; + _singletonInstanceDictionary[instanceType] = instance; } - private Dependency CreateTransientDependency(Type dependencyType, Type providerType) + /// Singleton type + /// Whether an instance is contained in the container + public bool ContainsSingletonInstanceOfType(Type type) { - return new Dependency(dependencyType, providerType, Scope.Transient); + return _singletonInstanceDictionary.ContainsKey(type); } /// - /// Creates a new Dependency object with the given dependency and provider types. + /// Overrides a singleton dependency. /// - /// Dependency type - /// Provider type - /// Dependency scope - /// Dependency object - public Dependency CreateDependency(Type dependencyType, Type providerType, Scope scope) + /// New singleton value + /// Dependency type + /// Thrown if value is null + public void SetSingletonOverride([NotNull] TDependency value) where TDependency : class { - if (scope == Scope.Singleton) - return CreateSingletonDependency(dependencyType, providerType); + if (value == null) + throw new ArgumentNullException(nameof(value)); - return CreateTransientDependency(dependencyType, providerType); + _singletonOverrideDictionary[typeof(TDependency)] = value; } - + /// - /// Creates a new Dependency object with the a dependency type and provided instance. + /// Removes any set singleton override. + /// + /// Dependency type + public void RemoveSingletonOverride() where TDependency : class + { + _singletonOverrideDictionary.Remove(typeof(TDependency)); + } + + /// + /// Tries to get a set singleton override. /// /// Dependency type - /// Provided instance - /// Dependency scope - /// Dependency object - public Dependency CreateDependency(Type dependencyType, object instance, Scope scope) + /// Singleton value + /// Whether the singleton override could be fetched + public bool TryGetSingletonOverride(Type dependencyType, out object value) { - var instanceType = instance.GetType(); - - if (scope == Scope.Singleton && !SingletonInstanceDictionary.ContainsKey(instanceType)) - SingletonInstanceDictionary.Add(instanceType, instance); - - return new Dependency(dependencyType, instance, scope); + return _singletonOverrideDictionary.TryGetValue(dependencyType, out value); } } } \ No newline at end of file diff --git a/Assets/UIComponents/Core/DependencyInjection/DiContext.cs b/Assets/UIComponents/Core/DependencyInjection/DiContext.cs new file mode 100644 index 00000000..fc709a94 --- /dev/null +++ b/Assets/UIComponents/Core/DependencyInjection/DiContext.cs @@ -0,0 +1,141 @@ +using System; +using JetBrains.Annotations; + +namespace UIComponents.DependencyInjection +{ + /// + /// Dependency injection context for consumers. + /// + public class DiContext + { + /// + /// The current dependency injection context. + /// + [NotNull] + public static DiContext Current { get; private set; } = new DiContext(); + + /// + /// The previous dependency injection context. Null if there is none. + /// + [CanBeNull] + public static DiContext Previous { get; private set; } = null; + + /// + /// Changes the current dependency injection context. + /// + /// New context + /// Thrown if context is null + public static void SetCurrent([NotNull] DiContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (context != Current) + Previous = Current; + + Current = context; + } + + public DiContainer Container { get; private set; } + + public DiContext() + { + Container = new DiContainer(); + } + + /// + /// Change the DiContainer used in this context. + /// + /// New DiContainer + /// Thrown if container is null + public void SwitchContainer([NotNull] DiContainer container) + { + if (container == null) + throw new ArgumentNullException(nameof(container)); + + Container = container; + } + + /// + /// Switches the dependency of a consumer. + /// + /// + /// Can be used in unit tests to switch to + /// a mocked dependency. + /// + /// + /// The new instance used for the dependency + /// + /// Consumer type + /// Dependency type + public void SetDependency(TDependency provider) + where TConsumer : class + where TDependency : class + { + var injector = GetInjector(typeof(TConsumer)); + + injector.SetDependency(provider); + } + + /// + /// Clears the dependency of a consumer. + /// + /// + /// Can be used in unit tests to clear + /// a dependency between tests. + /// + /// Consumer type + /// Dependency type + public void ClearDependency() + where TConsumer : class + where TDependency : class + { + var injector = GetInjector(typeof(TConsumer)); + + injector.ClearDependency(); + } + + /// + /// Resets the provided instance of a dependency. + /// If a singleton instance exists, it is restored. + /// Otherwise, a new instance of the dependency is created. + /// + /// + /// Can be used in unit tests to clear + /// restore dependency between or after tests. + /// + /// Consumer type + /// Dependency type + /// + /// Thrown if no configured dependency exists + /// + public void ResetProvidedInstance() + where TConsumer : class + where TDependency : class + { + var injector = GetInjector(typeof(TConsumer)); + + injector.ResetProvidedInstance(); + } + + /// + /// Returns the injector of the given consumer type. + /// + /// Consumer type + /// Injector of the consumer type + public DependencyInjector GetInjector(Type consumerType) + { + return Container.GetInjector(consumerType); + } + + /// + /// Removes the injector of the given consumer type. + /// Used primarily for testing. + /// + /// Consumer type + public void RemoveInjector(Type consumerType) + { + Container.RemoveInjector(consumerType); + } + } +} \ No newline at end of file diff --git a/Assets/UIComponents/Core/DependencyInjection/DiContext.cs.meta b/Assets/UIComponents/Core/DependencyInjection/DiContext.cs.meta new file mode 100644 index 00000000..6d92ab6b --- /dev/null +++ b/Assets/UIComponents/Core/DependencyInjection/DiContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34e98ef8061647b286688b680a73fadd +timeCreated: 1660841365 \ No newline at end of file diff --git a/Assets/UIComponents/Core/Testing.meta b/Assets/UIComponents/Core/Testing.meta new file mode 100644 index 00000000..86c79ff4 --- /dev/null +++ b/Assets/UIComponents/Core/Testing.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 345fbfb61f404e9882e8ac71da46cb3a +timeCreated: 1659287517 \ No newline at end of file diff --git a/Assets/UIComponents/Core/Testing/TestBed.cs b/Assets/UIComponents/Core/Testing/TestBed.cs new file mode 100644 index 00000000..3c386ac9 --- /dev/null +++ b/Assets/UIComponents/Core/Testing/TestBed.cs @@ -0,0 +1,82 @@ +using System; +using UIComponents.DependencyInjection; + +namespace UIComponents.Testing +{ + /// + /// is utility class for testing UIComponents with dependency injection. + /// + /// It allows overriding singleton and transient dependencies. + /// + /// You can initialize an instance with . + /// + public class TestBed + { + internal readonly DiContainer DiContainer = new DiContainer(); + + internal TestBed() {} + + /// + /// Starts the configuration of a test bed. + /// + public static TestBedBuilder Create() + { + return new TestBedBuilder(); + } + + /// + /// Returns a dependency instance as requested by consumer type . + /// + /// Consumer type + /// Dependency type + /// Dependency instance + /// Thrown if no provider exists + public TDependency Provide() + where TConsumer : class + where TDependency : class + { + var injector = DiContainer.GetInjector(typeof(TConsumer)); + + return injector.Provide(); + } + + /// + /// Creates a component of type using the given + /// predicate. + /// + /// Predicate for creating the component + /// Component type + /// Component instance + public TComponent CreateComponent(Func factoryPredicate) + where TComponent : UIComponent + { + var previousContainer = DiContext.Current.Container; + + DiContext.Current.SwitchContainer(DiContainer); + + TComponent component; + + try + { + component = factoryPredicate(); + } + finally + { + DiContext.Current.SwitchContainer(previousContainer); + } + + return component; + } + + + /// + /// Creates a component of type with a default constructor. + /// + /// Component type + /// Component instance + public TComponent CreateComponent() where TComponent : UIComponent, new() + { + return CreateComponent(() => new TComponent()); + } + } +} diff --git a/Assets/UIComponents/Core/Testing/TestBed.cs.meta b/Assets/UIComponents/Core/Testing/TestBed.cs.meta new file mode 100644 index 00000000..7bfe05f1 --- /dev/null +++ b/Assets/UIComponents/Core/Testing/TestBed.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9825c726c9ad4f998f208286463f171e +timeCreated: 1659287522 \ No newline at end of file diff --git a/Assets/UIComponents/Core/Testing/TestBedBuilder.cs b/Assets/UIComponents/Core/Testing/TestBedBuilder.cs new file mode 100644 index 00000000..c689def3 --- /dev/null +++ b/Assets/UIComponents/Core/Testing/TestBedBuilder.cs @@ -0,0 +1,53 @@ +namespace UIComponents.Testing +{ + /// + /// A class for configuring a instance with dependencies. + /// + public class TestBedBuilder + { + private readonly TestBed _testBed; + private bool _built; + + internal TestBedBuilder() + { + _testBed = new TestBed(); + } + + /// + /// Add a singleton dependency to the test bed. + /// + /// Singleton value + /// Dependency type + public TestBedBuilder WithSingleton(TDependency value) where TDependency : class + { + _testBed.DiContainer.SetSingletonOverride(value); + + return this; + } + + /// + /// Add an override for a transient dependencies to the test bed. + /// + /// Transient dependency value + /// Consumer type + /// Dependency type + public TestBedBuilder WithTransient(TDependency value) + where TConsumer : class + where TDependency : class + { + var injector = _testBed.DiContainer.GetInjector(typeof(TConsumer)); + + injector.SetDependency(value, Scope.Transient); + + return this; + } + + /// + /// Returns a instance with the configured dependencies. + /// + public TestBed Build() + { + return _testBed; + } + } +} diff --git a/Assets/UIComponents/Core/Testing/TestBedBuilder.cs.meta b/Assets/UIComponents/Core/Testing/TestBedBuilder.cs.meta new file mode 100644 index 00000000..6647e929 --- /dev/null +++ b/Assets/UIComponents/Core/Testing/TestBedBuilder.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: df5ec8d13a854558bd5f6921abfc389b +timeCreated: 1660928533 \ No newline at end of file diff --git a/Assets/UIComponents/Core/UIComponent.cs b/Assets/UIComponents/Core/UIComponent.cs index e8657d85..94d9e8c8 100644 --- a/Assets/UIComponents/Core/UIComponent.cs +++ b/Assets/UIComponents/Core/UIComponent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using UIComponents.Cache; +using UIComponents.DependencyInjection; using UIComponents.Internal; using Unity.Profiling; using UnityEngine.UIElements; @@ -73,11 +74,11 @@ protected UIComponent() { DependencySetupProfilerMarker.Begin(); _componentType = GetType(); - _dependencyInjector = DependencyInjector.GetInjector(_componentType); + _dependencyInjector = DiContext.Current.GetInjector(_componentType); DependencySetupProfilerMarker.End(); - AssetResolver = _dependencyInjector.Provide(); - Logger = _dependencyInjector.Provide(); + AssetResolver = Provide(); + Logger = Provide(); CacheSetupProfilerMarker.Begin(); if (!CacheDictionary.ContainsKey(_componentType)) diff --git a/Assets/UIComponents/Core/Utilities.meta b/Assets/UIComponents/Core/Utilities.meta deleted file mode 100644 index c172e0b9..00000000 --- a/Assets/UIComponents/Core/Utilities.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: a78a9638ef554307839cb1ba217f2332 -timeCreated: 1652281867 \ No newline at end of file diff --git a/Assets/UIComponents/Core/Utilities/DependencyScope.cs b/Assets/UIComponents/Core/Utilities/DependencyScope.cs deleted file mode 100644 index 1a5e994f..00000000 --- a/Assets/UIComponents/Core/Utilities/DependencyScope.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace UIComponents.Utilities -{ - /// - /// A helper class for setting a dependency temporarily. - /// When disposed, restores the previous provider, or the lack thereof. - /// Built for unit tests. - /// - /// Type of the consumer - /// Type of the dependency - public class DependencyScope : IDisposable - where TConsumer : class - where TDependency : class - { - private readonly TDependency _previousDependencyProvider; - - public DependencyScope(TDependency instance) - { - var injector = DependencyInjector.GetInjector(typeof(TConsumer)); - - if (injector.TryProvide(out var currentProvider)) - _previousDependencyProvider = currentProvider; - - DependencyInjector.SetDependency(instance); - } - - public void Dispose() - { - if (_previousDependencyProvider == null) - DependencyInjector.ClearDependency(); - else - DependencyInjector.SetDependency(_previousDependencyProvider); - } - } -} \ No newline at end of file diff --git a/Assets/UIComponents/Core/Utilities/DependencyScope.cs.meta b/Assets/UIComponents/Core/Utilities/DependencyScope.cs.meta deleted file mode 100644 index 6f7c33aa..00000000 --- a/Assets/UIComponents/Core/Utilities/DependencyScope.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 99ac67fc4aea4760aae5c17c8e3cb123 -timeCreated: 1652280505 \ No newline at end of file