mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-12 11:15:08 +00:00
Implement basic unit test infrastructure with abstraction patterns
Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for logging operations to enable testing
|
||||
/// </summary>
|
||||
public interface ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs a debug message
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the log message</param>
|
||||
/// <param name="message">Message to log</param>
|
||||
/// <param name="args">Format arguments</param>
|
||||
void LogDebug(object source, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Logs a verbose message
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the log message</param>
|
||||
/// <param name="message">Message to log</param>
|
||||
/// <param name="args">Format arguments</param>
|
||||
void LogVerbose(object source, string message, params object[] args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for queue operations to enable testing
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of items in the queue</typeparam>
|
||||
public interface IQueue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of items in the queue
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to the queue
|
||||
/// </summary>
|
||||
/// <param name="item">Item to add</param>
|
||||
void Enqueue(T item);
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the next item from the queue
|
||||
/// </summary>
|
||||
/// <returns>The next item, or default(T) if queue is empty</returns>
|
||||
T Dequeue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for thread operations to enable testing
|
||||
/// </summary>
|
||||
public interface IThreadService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sleeps the current thread for the specified milliseconds
|
||||
/// </summary>
|
||||
/// <param name="milliseconds">Time to sleep in milliseconds</param>
|
||||
void Sleep(int milliseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and starts a new thread
|
||||
/// </summary>
|
||||
/// <param name="threadFunction">The function to execute in the thread</param>
|
||||
/// <param name="parameter">Parameter to pass to the thread function</param>
|
||||
/// <returns>Thread identifier or handle</returns>
|
||||
object CreateAndStartThread(Func<object, object> threadFunction, object parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the specified thread
|
||||
/// </summary>
|
||||
/// <param name="thread">The thread to abort</param>
|
||||
void AbortThread(object thread);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the thread is currently running
|
||||
/// </summary>
|
||||
/// <param name="thread">The thread to check</param>
|
||||
/// <returns>True if the thread is running, false otherwise</returns>
|
||||
bool IsThreadRunning(object thread);
|
||||
}
|
||||
}
|
||||
5
tests/PepperDash.Essentials.Core.Tests/GlobalUsings.cs
Normal file
5
tests/PepperDash.Essentials.Core.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using Xunit;
|
||||
global using Moq;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.69" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.TestableComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// A simplified, testable action sequence that demonstrates abstraction patterns
|
||||
/// This shows how we can separate business logic from SDK dependencies
|
||||
/// </summary>
|
||||
public class TestableActionSequence
|
||||
{
|
||||
private readonly IQueue<TestableSequencedAction> _actionQueue;
|
||||
private readonly IThreadService _threadService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly List<TestableSequencedAction> _configuredActions;
|
||||
|
||||
private object _workerThread;
|
||||
private bool _allowActionsToExecute;
|
||||
|
||||
public TestableActionSequence(
|
||||
IQueue<TestableSequencedAction> actionQueue,
|
||||
IThreadService threadService,
|
||||
ILogger logger,
|
||||
List<TestableSequencedAction> actions)
|
||||
{
|
||||
_actionQueue = actionQueue ?? throw new ArgumentNullException(nameof(actionQueue));
|
||||
_threadService = threadService ?? throw new ArgumentNullException(nameof(threadService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_configuredActions = actions ?? new List<TestableSequencedAction>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts executing the sequenced actions
|
||||
/// </summary>
|
||||
public void StartSequence()
|
||||
{
|
||||
if (_workerThread != null && _threadService.IsThreadRunning(_workerThread))
|
||||
{
|
||||
_logger.LogDebug(this, "Thread already running. Cannot Start Sequence");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug(this, "Starting Action Sequence");
|
||||
_allowActionsToExecute = true;
|
||||
AddActionsToQueue();
|
||||
_workerThread = _threadService.CreateAndStartThread(ProcessActions, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops executing the sequenced actions
|
||||
/// </summary>
|
||||
public void StopSequence()
|
||||
{
|
||||
_logger.LogDebug(this, "Stopping Action Sequence");
|
||||
_allowActionsToExecute = false;
|
||||
if (_workerThread != null)
|
||||
{
|
||||
_threadService.AbortThread(_workerThread);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current status of the sequence
|
||||
/// </summary>
|
||||
public bool IsRunning => _allowActionsToExecute && _threadService.IsThreadRunning(_workerThread);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of pending actions
|
||||
/// </summary>
|
||||
public int PendingActionsCount => _actionQueue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the queue from the configuration information
|
||||
/// </summary>
|
||||
private void AddActionsToQueue()
|
||||
{
|
||||
_logger.LogDebug(this, "Adding {0} actions to queue", _configuredActions.Count);
|
||||
|
||||
foreach (var action in _configuredActions)
|
||||
{
|
||||
_actionQueue.Enqueue(action);
|
||||
}
|
||||
}
|
||||
|
||||
private object ProcessActions(object obj)
|
||||
{
|
||||
while (_allowActionsToExecute && _actionQueue.Count > 0)
|
||||
{
|
||||
var action = _actionQueue.Dequeue();
|
||||
if (action == null)
|
||||
break;
|
||||
|
||||
// Delay before executing
|
||||
if (action.DelayMs > 0)
|
||||
_threadService.Sleep(action.DelayMs);
|
||||
|
||||
ExecuteAction(action);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExecuteAction(TestableSequencedAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(this, "Executing action: {0} with delay: {1}ms", action.Name, action.DelayMs);
|
||||
action.Execute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogVerbose(this, "Error Executing Action: {0}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A testable action that can be sequenced
|
||||
/// </summary>
|
||||
public class TestableSequencedAction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int DelayMs { get; set; }
|
||||
public Action ActionToExecute { get; set; }
|
||||
|
||||
public TestableSequencedAction(string name, int delayMs = 0, Action actionToExecute = null)
|
||||
{
|
||||
Name = name;
|
||||
DelayMs = delayMs;
|
||||
ActionToExecute = actionToExecute ?? (() => { });
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
ActionToExecute?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||
using PepperDash.Essentials.Core.Tests.TestableComponents;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Unit
|
||||
{
|
||||
public class TestableActionSequenceTests
|
||||
{
|
||||
private readonly Mock<IQueue<TestableSequencedAction>> _mockQueue;
|
||||
private readonly Mock<IThreadService> _mockThreadService;
|
||||
private readonly Mock<ILogger> _mockLogger;
|
||||
private readonly TestableActionSequence _actionSequence;
|
||||
private readonly List<TestableSequencedAction> _testActions;
|
||||
|
||||
public TestableActionSequenceTests()
|
||||
{
|
||||
_mockQueue = new Mock<IQueue<TestableSequencedAction>>();
|
||||
_mockThreadService = new Mock<IThreadService>();
|
||||
_mockLogger = new Mock<ILogger>();
|
||||
|
||||
_testActions = new List<TestableSequencedAction>
|
||||
{
|
||||
new TestableSequencedAction("Action1", 100),
|
||||
new TestableSequencedAction("Action2", 200)
|
||||
};
|
||||
|
||||
_actionSequence = new TestableActionSequence(
|
||||
_mockQueue.Object,
|
||||
_mockThreadService.Object,
|
||||
_mockLogger.Object,
|
||||
_testActions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullQueue_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(null, _mockThreadService.Object, _mockLogger.Object, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullThreadService_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(_mockQueue.Object, null, _mockLogger.Object, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(_mockQueue.Object, _mockThreadService.Object, null, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_WhenNoThreadRunning_StartsNewThread()
|
||||
{
|
||||
// Arrange
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(It.IsAny<object>())).Returns(false);
|
||||
|
||||
// Act
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Starting Action Sequence"), Times.Once);
|
||||
_mockThreadService.Verify(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_WhenThreadAlreadyRunning_DoesNotStartNewThread()
|
||||
{
|
||||
// Arrange
|
||||
var mockThread = new object();
|
||||
_mockThreadService.Setup(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null))
|
||||
.Returns(mockThread);
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(mockThread)).Returns(true);
|
||||
|
||||
// First call to set up the worker thread
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Reset invocations to verify only the second call behavior
|
||||
_mockLogger.Invocations.Clear();
|
||||
_mockThreadService.Invocations.Clear();
|
||||
|
||||
// Act - Second call should detect running thread
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Thread already running. Cannot Start Sequence"), Times.Once);
|
||||
_mockThreadService.Verify(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_AddsConfiguredActionsToQueue()
|
||||
{
|
||||
// Arrange
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(It.IsAny<object>())).Returns(false);
|
||||
|
||||
// Act
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Adding {0} actions to queue", 2), Times.Once);
|
||||
_mockQueue.Verify(x => x.Enqueue(It.IsAny<TestableSequencedAction>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopSequence_SetsAllowActionsToFalseAndAbortsThread()
|
||||
{
|
||||
// Arrange
|
||||
var mockThread = new object();
|
||||
|
||||
// Act
|
||||
_actionSequence.StopSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Stopping Action Sequence"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingActionsCount_ReturnsQueueCount()
|
||||
{
|
||||
// Arrange
|
||||
_mockQueue.Setup(x => x.Count).Returns(5);
|
||||
|
||||
// Act
|
||||
var count = _actionSequence.PendingActionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestableSequencedAction_ExecutesActionWhenCalled()
|
||||
{
|
||||
// Arrange
|
||||
bool actionExecuted = false;
|
||||
var action = new TestableSequencedAction("TestAction", 0, () => actionExecuted = true);
|
||||
|
||||
// Act
|
||||
action.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.True(actionExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestableSequencedAction_WithNullAction_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var action = new TestableSequencedAction("TestAction", 0, null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Record.Exception(() => action.Execute());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user