* Trivial - Add VS Code workspace * Trivial - Add VSCode build task & extra ignore On GNU/Linux, auto-generated .directory files should be ignored. * WIP #485 - Boilerplate for unit test project * WIP #485 - Add test coverage (Remove/RemoveAll) * WIP #485 - Implement RemoveAll * Remove redundant logic, covered by IDictionary Per the following, the contract for a generic IDictionary, the Remove method already deals with non-existent keys. https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2.remove?view=net-5.0#System_Collections_Generic_IDictionary_2_Remove__0_ * Resolve #485 - Add PurgeVariables methods These are convenience methods upon ActivityExecutionContext and WorkflowExecutionContext, consistent with their current APIs. Also in this commit are tests for those simple methods. There's a bit of test-scaffold as well included, such as: * Autofixture Xunit2 integration * New project for unit tests * Customize attribute for avoiding crashes on Autofixture recursion * Reusable specimen builder for creating IServiceProvider which resolves services from Autofixture * Custonize attribute for a parameter to use that ^^ specimen builder
This commit is contained in:
parent
b2d6859331
commit
b583c7f1a9
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -62,4 +62,7 @@ FodyWeavers.xsd
|
|||
# Elsa Stencil Components
|
||||
src/designer/elsa-workflows-studio/dist
|
||||
src/designer/elsa-workflows-studio/www
|
||||
src/designer/elsa-workflows-studio/loader
|
||||
src/designer/elsa-workflows-studio/loader
|
||||
|
||||
# Created by developers using GNU/Linux
|
||||
.directory
|
||||
33
.vscode/tasks.json
vendored
Normal file
33
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"/property:GenerateFullPaths=true"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"/property:GenerateFullPaths=true"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
Elsa.code-workspace
Normal file
8
Elsa.code-workspace
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
10
Elsa.sln
10
Elsa.sln
|
|
@ -232,6 +232,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Designer.Bindings.Blaz
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Timers.Quartz", "src\samples\worker\Elsa.Samples.Timers.Quartz\Elsa.Samples.Timers.Quartz.csproj", "{FE724143-B24C-43DA-BBF5-15431BCA6E27}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{F471267A-DA3A-48C7-8784-F8E4E46203A2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.UnitTests", "test\unit\Elsa.UnitTests\Elsa.UnitTests.csproj", "{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -554,6 +558,10 @@ Global
|
|||
{FE724143-B24C-43DA-BBF5-15431BCA6E27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE724143-B24C-43DA-BBF5-15431BCA6E27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE724143-B24C-43DA-BBF5-15431BCA6E27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -664,6 +672,8 @@ Global
|
|||
{5C005CC7-B5F2-49C3-8A8B-7640FCDBEE50} = {E42743A0-FBDD-4150-9D53-6000496D9B87}
|
||||
{45A06CA3-199B-4650-8880-30DDF2E38E19} = {8B6B40A8-DF21-4CD2-BABD-474B79D0C3AF}
|
||||
{FE724143-B24C-43DA-BBF5-15431BCA6E27} = {E42743A0-FBDD-4150-9D53-6000496D9B87}
|
||||
{F471267A-DA3A-48C7-8784-F8E4E46203A2} = {AB1AE008-6FD6-414C-8E88-D735F42E1FA6}
|
||||
{F822DE2F-A91D-416E-BF4A-A6C466C1BF0A} = {F471267A-DA3A-48C7-8784-F8E4E46203A2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Elsa.Models
|
||||
|
|
@ -29,11 +30,24 @@ namespace Elsa.Models
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a variable of the specified name if it is present.
|
||||
/// </summary>
|
||||
/// <param name="name">The variable name</param>
|
||||
/// <returns>A reference to this same <see cref="Variables"/> instance, so calls may be chained.</returns>
|
||||
public Variables Remove(string name)
|
||||
{
|
||||
if (Has(name))
|
||||
Data.Remove(name);
|
||||
Data.Remove(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all of the variables from the current instance, clearing it.
|
||||
/// </summary>
|
||||
/// <returns>A reference to this same <see cref="Variables"/> instance, so calls may be chained.</returns>
|
||||
public Variables RemoveAll()
|
||||
{
|
||||
Data.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,14 @@ namespace Elsa.Services.Models
|
|||
public object? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name);
|
||||
public T? GetVariable<T>(string name) => WorkflowExecutionContext.GetVariable<T>(name);
|
||||
public T? GetVariable<T>() => GetVariable<T>(typeof(T).Name);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all of the variables associated with the current <see cref="Elsa.Models.WorkflowInstance"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="WorkflowExecutionContext.PurgeVariables"/>
|
||||
/// <seealso cref="Variables.RemoveAll"/>
|
||||
public void PurgeVariables() => WorkflowExecutionContext.PurgeVariables();
|
||||
|
||||
public void SetTransientVariable(string name, object? value) => WorkflowExecutionContext.SetTransientVariable(name, value);
|
||||
public object? GetTransientVariable(string name) => WorkflowExecutionContext.GetTransientVariable(name);
|
||||
public T? GetTransientVariable<T>(string name) => WorkflowExecutionContext.GetTransientVariable<T>(name);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@ namespace Elsa.Services.Models
|
|||
/// </summary>
|
||||
public object? GetWorkflowVariable(string name) => WorkflowInstance.Variables.Get(name);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all of the variables associated with the current <see cref="WorkflowInstance"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Variables.RemoveAll"/>
|
||||
public void PurgeVariables() => WorkflowInstance.Variables.RemoveAll();
|
||||
|
||||
|
||||
public ActivityScope CurrentScope => WorkflowInstance.Scopes.Peek();
|
||||
public ActivityScope GetScope(string activityId) => WorkflowInstance.Scopes.First(x => x.ActivityId == activityId);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using AutoFixture.Xunit2;
|
||||
using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders;
|
||||
|
||||
namespace Elsa.Testing.Shared.AutoFixture.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Customizes the parameter so that it creates an <see cref="IServiceProvider"/>
|
||||
/// using Moq. That mock service provider gets services by resolving them from Autofixture.
|
||||
/// </summary>
|
||||
/// <seealso cref="AutofixtureServiceProviderSpecimenBuilder"/>
|
||||
public class AutofixtureServiceProviderAttribute : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
=> new AutofixtureServiceProviderCustomization(parameter);
|
||||
|
||||
class AutofixtureServiceProviderCustomization : ICustomization
|
||||
{
|
||||
readonly ParameterInfo parameter;
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customizations.Insert(0, GetSpecimenBuilder());
|
||||
}
|
||||
|
||||
ISpecimenBuilder GetSpecimenBuilder()
|
||||
{
|
||||
var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name);
|
||||
var specimenBuilder = new AutofixtureServiceProviderSpecimenBuilder();
|
||||
return new FilteringSpecimenBuilder(specimenBuilder, paramSpec);
|
||||
}
|
||||
|
||||
public AutofixtureServiceProviderCustomization(ParameterInfo parameter)
|
||||
{
|
||||
this.parameter = parameter;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
|
||||
namespace Elsa.Testing.Shared.AutoFixture.Attributes
|
||||
{
|
||||
public class OmitOnRecursionAttribute : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
=> new OmitOnRecursionCustomization();
|
||||
|
||||
class OmitOnRecursionCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var throwingBehaviours = fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList();
|
||||
foreach(var behaviour in throwingBehaviours)
|
||||
fixture.Behaviors.Remove(behaviour);
|
||||
|
||||
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using AutoFixture.Kernel;
|
||||
using Moq;
|
||||
|
||||
namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a mock <see cref="IServiceProvider"/> which gets services
|
||||
/// by resolving them from Autofixture.
|
||||
/// </summary>
|
||||
public class AutofixtureServiceProviderSpecimenBuilder : ISpecimenBuilder
|
||||
{
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
{
|
||||
if(Equals(request, typeof(IServiceProvider)))
|
||||
return GetServiceProvider(context);
|
||||
if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(IServiceProvider))
|
||||
return GetServiceProvider(context);
|
||||
|
||||
return new NoSpecimen();
|
||||
}
|
||||
|
||||
static object GetServiceProvider(ISpecimenContext context)
|
||||
{
|
||||
var provider = new Mock<IServiceProvider>();
|
||||
|
||||
provider.Name = $"Autofixture_{nameof(IServiceProvider)}-{Guid.NewGuid()}";
|
||||
provider
|
||||
.Setup(x => x.GetService(It.IsAny<Type>()))
|
||||
.Returns((Type t) => context.Resolve(t));
|
||||
|
||||
return provider.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" Version="4.15.0" />
|
||||
<PackageReference Include="Autofixture.AutoMoq" Version="4.15.0" />
|
||||
<PackageReference Include="autofixture.xunit2" Version="4.15.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.3" />
|
||||
|
|
|
|||
13
test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs
Normal file
13
test/unit/Elsa.UnitTests/AutoMoqDataAttribute.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using AutoFixture;
|
||||
using AutoFixture.AutoMoq;
|
||||
using AutoFixture.Xunit2;
|
||||
|
||||
namespace Elsa
|
||||
{
|
||||
public class AutoMoqDataAttribute : AutoDataAttribute
|
||||
{
|
||||
public AutoMoqDataAttribute() : base(() => new Fixture().Customize(new AutoMoqCustomization()))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
31
test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj
Normal file
31
test/unit/Elsa.UnitTests/Elsa.UnitTests.csproj
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Elsa</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofixture" Version="4.15.0" />
|
||||
<PackageReference Include="Autofixture.AutoMoq" Version="4.15.0" />
|
||||
<PackageReference Include="autofixture.xunit2" Version="4.15.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../src/core/Elsa.Abstractions/Elsa.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
60
test/unit/Elsa.UnitTests/Models/VariablesTests.cs
Normal file
60
test/unit/Elsa.UnitTests/Models/VariablesTests.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Elsa.Models
|
||||
{
|
||||
public class VariablesTests
|
||||
{
|
||||
[Theory(DisplayName = "The Remove method should remove a variable if such a variable exists"), AutoMoqData]
|
||||
public void Remove_removes_a_variable_by_name_if_present(string variableName, object variableData)
|
||||
{
|
||||
var sut = new Variables(new Dictionary<string,object> { { variableName, variableData } });
|
||||
|
||||
sut.Remove(variableName);
|
||||
|
||||
Assert.Empty(sut.Data);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "The Remove method should not throw if used with a non-existent variable"), AutoMoqData]
|
||||
public void Remove_does_not_throw_when_trying_to_remove_a_variable_which_does_not_exist(string variableName)
|
||||
{
|
||||
var sut = new Variables();
|
||||
|
||||
sut.Remove(variableName);
|
||||
|
||||
// No assertion, if the line about doesn't throw then this test passed
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "The Remove method should support call-chaining by returning a self-reference"), AutoMoqData]
|
||||
public void Remove_returns_a_reference_to_itself(Variables sut, string variableName)
|
||||
{
|
||||
var result = sut.Remove(variableName);
|
||||
Assert.Same(sut, result);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "The RemoveAll method should leave the variables collection empty"), AutoMoqData]
|
||||
public void RemoveAll_clears_all_variables(string variableName1,
|
||||
string variableName2,
|
||||
string variableName3,
|
||||
object variableData)
|
||||
{
|
||||
var sut = new Variables(new Dictionary<string,object> {
|
||||
{ variableName1, variableData },
|
||||
{ variableName2, variableData },
|
||||
{ variableName3, variableData },
|
||||
});
|
||||
|
||||
sut.RemoveAll();
|
||||
|
||||
Assert.Empty(sut.Data);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "The RemoveAll method should support call-chaining by returning a self-reference"), AutoMoqData]
|
||||
public void RemoveAll_returns_a_reference_to_itself(Variables sut)
|
||||
{
|
||||
var result = sut.RemoveAll();
|
||||
Assert.Same(sut, result);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Xunit;
|
||||
using Elsa.Testing.Shared.AutoFixture.Attributes;
|
||||
using AutoFixture.Xunit2;
|
||||
using System;
|
||||
|
||||
namespace Elsa.Services.Models
|
||||
{
|
||||
public class ActivityExecutionContextTests
|
||||
{
|
||||
[Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance associated with the Workflow Execution Context"), AutoMoqData]
|
||||
public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider,
|
||||
[OmitOnRecursion,NoAutoProperties] ActivityExecutionContext sut,
|
||||
string variableName,
|
||||
object variableValue)
|
||||
{
|
||||
sut.WorkflowExecutionContext.WorkflowInstance.Variables.Set(variableName, variableValue);
|
||||
|
||||
sut.PurgeVariables();
|
||||
|
||||
Assert.Empty(sut.WorkflowExecutionContext.WorkflowInstance.Variables.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Xunit;
|
||||
using Elsa.Testing.Shared.AutoFixture.Attributes;
|
||||
using AutoFixture.Xunit2;
|
||||
using System;
|
||||
|
||||
namespace Elsa.Services.Models
|
||||
{
|
||||
public class WorkflowExecutionContextTests
|
||||
{
|
||||
[Theory(DisplayName = "The PurgeVariables method should clear the Variables instance associated with the WorkflowInstance"), AutoMoqData]
|
||||
public void PurgeVariables_clears_workflow_execution_context_workflow_instance_variables([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider,
|
||||
[OmitOnRecursion,NoAutoProperties] WorkflowExecutionContext sut,
|
||||
string variableName,
|
||||
object variableValue)
|
||||
{
|
||||
sut.WorkflowInstance.Variables.Set(variableName, variableValue);
|
||||
|
||||
sut.PurgeVariables();
|
||||
|
||||
Assert.Empty(sut.WorkflowInstance.Variables.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue