diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index fefe6c3ebf..f90e7be445 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -4,8 +4,19 @@ using AutoFixture.Kernel; namespace Bit.Test.Common.AutoFixture; +/// +/// A utility class that encapsulates a system under test (sut) and its dependencies. +/// By default, all dependencies are initialized as mocks using the NSubstitute library. +/// SutProvider provides an interface for accessing these dependencies in the arrange and assert stages of your tests. +/// +/// The concrete implementation of the class being tested. public class SutProvider : ISutProvider { + /// + /// A record of the configured dependencies (constructor parameters). The outer Dictionary is keyed by the dependency's + /// type, and the inner dictionary is keyed by the parameter name (optionally used to disambiguate parameters with the same type). + /// The inner dictionary value is the dependency. + /// private Dictionary> _dependencies; private readonly IFixture _fixture; private readonly ConstructorParameterRelay _constructorParameterRelay; @@ -23,9 +34,21 @@ public class SutProvider : ISutProvider _fixture.Customizations.Add(_constructorParameterRelay); } + /// + /// Registers a dependency to be injected when the sut is created. You must call after + /// this method to (re)create the sut with the dependency. + /// + /// The dependency to register. + /// An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this. + /// The type to register the dependency under - usually an interface. This should match the type expected by the sut's constructor. + /// public SutProvider SetDependency(T dependency, string parameterName = "") => SetDependency(typeof(T), dependency, parameterName); - public SutProvider SetDependency(Type dependencyType, object dependency, string parameterName = "") + + /// + /// An overload for which takes a runtime object rather than a compile-time type. + /// + private SutProvider SetDependency(Type dependencyType, object dependency, string parameterName = "") { if (_dependencies.ContainsKey(dependencyType)) { @@ -39,46 +62,69 @@ public class SutProvider : ISutProvider return this; } + /// + /// Gets a dependency of the sut. Can only be called after the SutProvider has been initialized with . + /// As dependencies are initialized with NSubstitute mocks by default, this is often used to retrieve those mocks in order to + /// configure them during the arrange stage, or check received calls in the assert stage. + /// + /// An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this. + /// The type of the dependency you want to get - usually an interface. + /// The dependency. public T GetDependency(string parameterName = "") => (T)GetDependency(typeof(T), parameterName); - public object GetDependency(Type dependencyType, string parameterName = "") + + /// + /// An overload for which takes a runtime object rather than a compile-time type. + /// + private object GetDependency(Type dependencyType, string parameterName = "") { if (DependencyIsSet(dependencyType, parameterName)) { return _dependencies[dependencyType][parameterName]; } - else if (_dependencies.ContainsKey(dependencyType)) + + if (_dependencies.ContainsKey(dependencyType)) { var knownDependencies = _dependencies[dependencyType]; if (knownDependencies.Values.Count == 1) { return _dependencies[dependencyType].Values.Single(); } - else - { - throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ", - $"{parameterName} does not exist. Available dependency names are: ", - string.Join(", ", knownDependencies.Keys))); - } - } - else - { - throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set."); + + throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ", + $"{parameterName} does not exist. Available dependency names are: ", + string.Join(", ", knownDependencies.Keys))); } + + throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set."); } + /// + /// Clear all the dependencies and the sut. This reverts the SutProvider back to a fully uninitialized state. + /// public void Reset() { _dependencies = new Dictionary>(); Sut = default; } + /// + /// Recreate a new sut with all new dependencies. This will reset all dependencies, including mocked return values + /// and any dependencies set with . + /// public void Recreate() { _dependencies = new Dictionary>(); Sut = _fixture.Create(); } + /// > ISutProvider ISutProvider.Create() => Create(); + + /// + /// Creates the sut, injecting any dependencies configured via and falling back to + /// NSubstitute mocks for any dependencies that have not been explicitly configured. + /// + /// public SutProvider Create() { Sut = _fixture.Create(); @@ -90,6 +136,19 @@ public class SutProvider : ISutProvider private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null; + /// + /// A specimen builder which tells Autofixture to use the dependency registered in + /// when creating test data. If no matching dependency exists in , it creates + /// an NSubstitute mock and registers it using + /// so it can be retrieved later. + /// This is the link between and Autofixture. + /// + /// + /// Autofixture knows how to create sample data of simple types (such as an int or string) but not more complex classes. + /// We create our own and register it with the in + /// to provide that instruction. + /// + /// The type of the sut. private class ConstructorParameterRelay : ISpecimenBuilder { private readonly SutProvider _sutProvider; @@ -103,6 +162,7 @@ public class SutProvider : ISutProvider public object Create(object request, ISpecimenContext context) { + // Basic checks to filter out irrelevant requests from Autofixture if (context == null) { throw new ArgumentNullException(nameof(context)); @@ -117,16 +177,20 @@ public class SutProvider : ISutProvider return new NoSpecimen(); } + // Use the dependency set under this parameter name, if any if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name)) { return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name); } - // Return default type if set - else if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, "")) + + // Use the default dependency set for this type, if any (i.e. no parameter name has been specified) + if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, "")) { return _sutProvider.GetDependency(parameterInfo.ParameterType, ""); } + // Fallback: create an NSubstitute mock (assuming the WithAutoNSubstitutions customization has been used) + // and register it using SetDependency so it can be retrieved later. // This is the equivalent of _fixture.Create, but no overload for // Create(Type type) exists. var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs index 3ca7004e70..426389f33c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs @@ -288,7 +288,7 @@ public class SavePolicyCommandTests { return new SutProvider() .WithFakeTimeProvider() - .SetDependency(typeof(IEnumerable), policyValidators ?? []) + .SetDependency(policyValidators ?? []) .Create(); }