diff --git a/MORYX-Framework.sln b/MORYX-Framework.sln index f3ba24f65..4b91aaf84 100644 --- a/MORYX-Framework.sln +++ b/MORYX-Framework.sln @@ -116,7 +116,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Resources.Management. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tests", "src\Tests\Moryx.Runtime.Endpoints.Tests\Moryx.Runtime.Endpoints.Tests.csproj", "{7792C4E0-6D07-42C9-AC29-BAB76836FC11}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.TestTools.IntegrationTest", "src\Moryx.TestTools.IntegrationTest\Moryx.TestTools.IntegrationTest.csproj", "{C949164C-0345-4893-9E4C-A79BC1F93F85}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -296,6 +298,10 @@ Global {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.Build.0 = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C949164C-0345-4893-9E4C-A79BC1F93F85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,10 +346,11 @@ Global {FEB3BA44-2CD9-445A-ABF2-C92378C443F7} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {7792C4E0-6D07-42C9-AC29-BAB76836FC11} = {0A466330-6ED6-4861-9C94-31B1949CDDB9} {4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6} + {C949164C-0345-4893-9E4C-A79BC1F93F85} = {953AAE25-26C8-4A28-AB08-61BAFE41B22F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} - RESX_TaskErrorCategory = Message RESX_ShowErrorsInErrorList = True + RESX_TaskErrorCategory = Message + SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243} EndGlobalSection EndGlobal diff --git a/VERSION b/VERSION index 68d92dd66..8104cabd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.6 +8.1.0 diff --git a/docs/tutorials/HowToTestAModule.md b/docs/tutorials/HowToTestAModule.md new file mode 100644 index 000000000..0ee512088 --- /dev/null +++ b/docs/tutorials/HowToTestAModule.md @@ -0,0 +1,45 @@ +# Setup a test environment for integration tests of a module + +In order to test a module in its lifecycle with its respective facade we offer the `Moryx.TestTools.IntegrationTest`. +The package brings a `MoryxTestEnvironment`. +With this class you can first create mocks for all module facades your module dependents on using the static `CreateModuleMock` method. +Afterwards you can create the environment using an implementation of the `ServerModuleBase` class, an instance of the `ConfigBase` and the set of dependency mocks. +The first two parameters are usually your `ModuleController` and your `ModuleConfig`. +The following example shows a setup for the `IShiftManagement` facade interface. The module depends on the `IResourceManagement` and `IOperatorManagement` facades. + +```csharp +private ModuleConfig _config; +private Mock _resourceManagementMock; +private Mock _operatorManagementMock; +private MoryxTestEnvironment _env; + +[SetUp] +public void SetUp() +{ + ReflectionTool.TestMode = true; + _config = new(); + _resourceManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _operatorManagementMock = MoryxTestEnvironment.CreateModuleMock(); + _env = new MoryxTestEnvironment(typeof(ModuleController), + new Mock[] { _resourceManagementMock, _operatorManagementMock }, _config); +} +``` + +Using the created environment you can start and stop the module as you please. +You can also retrieve the facade of the module to test all the functionalities the running module should provide. + +```csharp +[Test] +public void Start_WhenModuleIsStopped_StartsModule() +{ + // Arrange + var facade = _env.GetTestModule(); + + // Act + var module = _env.StartTestModule(); + var module = _env.StopTestModule(); + + // Assert + Assert.That(module.State, Is.EqualTo(ServerModuleState.Stopped)); +} +``` \ No newline at end of file diff --git a/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj new file mode 100644 index 000000000..bf257e324 --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/Moryx.TestTools.IntegrationTest.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Library with helper classes for integration tests. + true + MORYX;Tests;IntegrationTest + true + + + + + + + + + + + + diff --git a/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs new file mode 100644 index 000000000..3b83c7c08 --- /dev/null +++ b/src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs @@ -0,0 +1,132 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using Moq; +using Moryx.Configuration; +using Moryx.Model.InMemory; +using Moryx.Model; +using Moryx.Runtime.Kernel; +using Moryx.Runtime.Modules; +using Moryx.TestTools.UnitTest; +using Moryx.Threading; +using System; +using System.Linq; +using Moryx.Tools; +using System.Collections.Generic; + +namespace Moryx.TestTools.IntegrationTest +{ + /// + /// A test environment for MORYX modules to test the module lifecycle as well as its + /// facade and component orchestration. The environment must be filled with mocked + /// dependencies. + /// + /// Type of the facade to be tested. + public class MoryxTestEnvironment + { + private readonly Type _moduleType; + + public IServiceProvider Services { get; private set; } + + /// + /// Creates an for integration tests of moryx. We prepare the + /// service collection to hold all kernel components (a mocked IConfigManager providing only the , + /// , an , a and the + /// ). Additionally all provided mocks are registered as moryx modules. + /// + /// Type of the ModuleController of the module to be tested + /// An enumeration of mocks for all dependencies of the module to be tested. + /// We recommend using the method to properly create the mocks. + /// The config for the module to be tested. + /// Throw if is not a server module + public MoryxTestEnvironment(Type serverModuleType, IEnumerable dependencyMocks, ConfigBase config) + { + _moduleType = serverModuleType; + + if (!serverModuleType.IsAssignableTo(typeof(IServerModule))) + throw new ArgumentException("Provided parameter is no server module", nameof(serverModuleType)); + + var dependencyTypes = serverModuleType.GetProperties() + .Where(p => p.GetCustomAttribute() is not null) + .Select(p => p.PropertyType); + + var services = new ServiceCollection(); + foreach (var type in dependencyTypes) + { + var mock = dependencyMocks.SingleOrDefault(m => type.IsAssignableFrom(m.Object.GetType())) ?? + throw new ArgumentException($"Missing {nameof(Mock)} for dependency of type {type} of facade type {serverModuleType}", nameof(dependencyMocks)); + services.AddSingleton(type, mock.Object); + services.AddSingleton(typeof(IServerModule), mock.Object); + } + + services.AddMoryxKernel(); + var configManagerMock = new Mock(); + configManagerMock.Setup(c => c.GetConfiguration(config.GetType(), It.IsAny(), false)).Returns(config); + services.AddSingleton(configManagerMock.Object); + + var parallelOpsDescriptor = services.Single(d => d.ServiceType == typeof(IParallelOperations)); + services.Remove(parallelOpsDescriptor); + services.AddTransient(); + services.AddSingleton(new InMemoryDbContextManager(Guid.NewGuid().ToString())); + services.AddSingleton(new NullLoggerFactory()); + services.AddSingleton(new Mock>().Object); + services.AddMoryxModules(); + + Services = services.BuildServiceProvider(); + _ = Services.GetRequiredService(); + } + + /// + /// Creates a mock of a server module with a facade interface of type . + /// The mock can be used in setting up a service collection for test purposes. + /// + /// Type of the facade interface + /// The mock of the + public static Mock CreateModuleMock() where FacadeType : class + { + var mock = new Mock(); + var moduleMock = mock.As(); + moduleMock.SetupGet(m => m.State).Returns(ServerModuleState.Running); + var containerMock = moduleMock.As>(); + containerMock.SetupGet(x => x.Facade).Returns(mock.Object); + return mock; + } + + /// + /// Initializes and starts the module with the facade interface of type + /// . + /// + /// The started module. + public IServerModule StartTestModule() + { + var module = (IServerModule)Services.GetService(_moduleType); + + module.Initialize(); + module.Container.Register(typeof(NotSoParallelOps), [typeof(IParallelOperations)], nameof(NotSoParallelOps), Container.LifeCycle.Singleton); + + var strategies = module.GetType().GetProperty(nameof(ServerModuleBase.Strategies)).GetValue(module) as Dictionary; + if (strategies is not null && !strategies.Any(s => s.Value == nameof(NotSoParallelOps))) + strategies.Add(typeof(IParallelOperations), nameof(NotSoParallelOps)); + + module.Start(); + return module; + } + + /// + /// Stops the module with the facade interface of type . + /// + /// The stopped module. + public IServerModule StopTestModule() + { + var module = (IServerModule)Services.GetService(_moduleType); + module.Stop(); + + return module; + } + + /// + /// Returns the service for the facade of type to be tested. + /// + public TModule GetTestModule() => Services.GetRequiredService(); + } +} \ No newline at end of file