Skip to content

Commit

Permalink
Merge pull request #443 from PHOENIXCONTACT/feature/test-tool
Browse files Browse the repository at this point in the history
Port Add integration test tools for MORYX modules to MORYX 8
  • Loading branch information
1nf0rmagician authored Aug 29, 2024
2 parents 9679c27 + 15997b3 commit 9f40c76
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 4 deletions.
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>();
}
}

0 comments on commit 9f40c76

Please sign in to comment.