Implement basic unit test infrastructure with abstraction patterns

Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-07-25 01:23:02 +00:00
parent 046b6fdb3b
commit 1676ba7649
12 changed files with 922 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
global using Xunit;
global using Moq;
global using System;
global using System.Collections.Generic;
global using System.Linq;

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}