Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Add integration test tools for MORYX modules to MORYX 8 #443

Merged
merged 4 commits into from
Aug 29, 2024
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
13 changes: 10 additions & 3 deletions MORYX-Framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.0.6
8.1.0
45 changes: 45 additions & 0 deletions docs/tutorials/HowToTestAModule.md
Original file line number Diff line number Diff line change
@@ -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<T>`.
With this class you can first create mocks for all module facades your module dependents on using the static `CreateModuleMock<FacadeType>` method.
Afterwards you can create the environment using an implementation of the `ServerModuleBase<T>` 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<IResourceManagement> _resourceManagementMock;
private Mock<IOperatorManagement> _operatorManagementMock;
private MoryxTestEnvironment _env;

[SetUp]
public void SetUp()
{
ReflectionTool.TestMode = true;
_config = new();
_resourceManagementMock = MoryxTestEnvironment.CreateModuleMock<IResourceManagement>();
_operatorManagementMock = MoryxTestEnvironment.CreateModuleMock<IOperatorManagement>();
_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));
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>Library with helper classes for integration tests.</Description>
<CreatePackage>true</CreatePackage>
<PackageTags>MORYX;Tests;IntegrationTest</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Moryx.Model.InMemory\Moryx.Model.InMemory.csproj" />
<ProjectReference Include="..\Moryx.Runtime.Kernel\Moryx.Runtime.Kernel.csproj" />
<ProjectReference Include="..\Moryx.TestTools.UnitTest\Moryx.TestTools.UnitTest.csproj" />
</ItemGroup>
</Project>
132 changes: 132 additions & 0 deletions src/Moryx.TestTools.IntegrationTest/MoryxTestEnvironment.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Type of the facade to be tested.</typeparam>
public class MoryxTestEnvironment
{
private readonly Type _moduleType;

public IServiceProvider Services { get; private set; }

/// <summary>
/// Creates an <see cref="IServiceProvider"/> for integration tests of moryx. We prepare the
/// service collection to hold all kernel components (a mocked IConfigManager providing only the <paramref name="config"/>,
/// <see cref="NotSoParallelOps"/>, an <see cref="InMemoryDbContextManager"/>, a <see cref="NullLoggerFactory"/> and the
/// <see cref="ModuleManager"/>). Additionally all provided mocks are registered as moryx modules.
/// </summary>
/// <param name="serverModuleType">Type of the ModuleController of the module to be tested</param>
/// <param name="dependencyMocks">An enumeration of mocks for all dependencies of the module to be tested.
/// We recommend using the <see cref="CreateModuleMock{T}"/> method to properly create the mocks.</param>
/// <param name="config">The config for the module to be tested.</param>
/// <exception cref="ArgumentException">Throw if <paramref name="serverModuleType"/> is not a server module</exception>
public MoryxTestEnvironment(Type serverModuleType, IEnumerable<Mock> 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<RequiredModuleApiAttribute>() 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<IConfigManager>();
configManagerMock.Setup(c => c.GetConfiguration(config.GetType(), It.IsAny<string>(), false)).Returns(config);
services.AddSingleton(configManagerMock.Object);

var parallelOpsDescriptor = services.Single(d => d.ServiceType == typeof(IParallelOperations));
services.Remove(parallelOpsDescriptor);
services.AddTransient<IParallelOperations, NotSoParallelOps>();
services.AddSingleton<IDbContextManager>(new InMemoryDbContextManager(Guid.NewGuid().ToString()));
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
services.AddSingleton(new Mock<ILogger<ModuleManager>>().Object);
services.AddMoryxModules();

Services = services.BuildServiceProvider();
_ = Services.GetRequiredService<IModuleManager>();
}

/// <summary>
/// Creates a mock of a server module with a facade interface of type <typeparamref name="T"/>.
/// The mock can be used in setting up a service collection for test purposes.
/// </summary>
/// <typeparam name="T">Type of the facade interface</typeparam>
/// <returns>The mock of the <typeparamref name="FacadeType"/></returns>
public static Mock<FacadeType> CreateModuleMock<FacadeType>() where FacadeType : class
{
var mock = new Mock<FacadeType>();
var moduleMock = mock.As<IServerModule>();
moduleMock.SetupGet(m => m.State).Returns(ServerModuleState.Running);
var containerMock = moduleMock.As<IFacadeContainer<FacadeType>>();
containerMock.SetupGet(x => x.Facade).Returns(mock.Object);
return mock;
}

/// <summary>
/// Initializes and starts the module with the facade interface of type
/// <typeparamref name="T"/>.
/// </summary>
/// <returns>The started module.</returns>
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<ConfigBase>.Strategies)).GetValue(module) as Dictionary<Type, string>;
if (strategies is not null && !strategies.Any(s => s.Value == nameof(NotSoParallelOps)))
strategies.Add(typeof(IParallelOperations), nameof(NotSoParallelOps));

module.Start();
return module;
}

/// <summary>
/// Stops the module with the facade interface of type <typeparamref name="T"/>.
/// </summary>
/// <returns>The stopped module.</returns>
public IServerModule StopTestModule()
{
var module = (IServerModule)Services.GetService(_moduleType);
module.Stop();

return module;
}

/// <summary>
/// Returns the service for the facade of type <typeparamref name="T"/> to be tested.
/// </summary>
public TModule GetTestModule<TModule>() => Services.GetRequiredService<TModule>();
}
}
Loading