mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-04 23:35:02 +00:00
Compare commits
2 Commits
mc-connect
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1676ba7649 | ||
|
|
046b6fdb3b |
36
.github/workflows/unit-tests.yml
vendored
Normal file
36
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj
|
||||||
|
|
||||||
|
- name: Build tests
|
||||||
|
run: dotnet build tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj --no-restore
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: dotnet test tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj --no-build --verbosity normal --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./tests/PepperDash.Essentials.Core.Tests/TestResults/*/coverage.cobertura.xml
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
fail_ci_if_error: false
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -60,6 +60,15 @@ bld/
|
|||||||
[Ll]og/
|
[Ll]og/
|
||||||
[Ll]ogs/
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Test results and coverage
|
||||||
|
TestResults/
|
||||||
|
coverage/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
coverage.cobertura.xml
|
||||||
|
coverage-report/
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
.vs/
|
.vs/
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C8EAB8E7-4F14-4E5C-8D23-1A3956D46DE8}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core.Tests", "tests\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{A1234567-8901-2345-6789-ABCDEF012345}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
||||||
@@ -79,6 +83,12 @@ Global
|
|||||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -90,6 +100,7 @@ Global
|
|||||||
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||||
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||||
|
{A1234567-8901-2345-6789-ABCDEF012345} = {C8EAB8E7-4F14-4E5C-8D23-1A3956D46DE8}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
||||||
|
|||||||
188
docs/testing/QuickStart.md
Normal file
188
docs/testing/QuickStart.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Quick Start Guide: Unit Testing
|
||||||
|
|
||||||
|
This guide helps you get started with writing unit tests for PepperDash Essentials components.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- .NET 8 SDK
|
||||||
|
- Visual Studio 2022 or VS Code with C# extension
|
||||||
|
|
||||||
|
## Running Existing Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to the test project
|
||||||
|
cd tests/PepperDash.Essentials.Core.Tests
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
dotnet test --collect:"XPlat Code Coverage"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Your First Test
|
||||||
|
|
||||||
|
### 1. Identify the Component
|
||||||
|
|
||||||
|
Choose a component that has business logic you want to test. Look for classes that:
|
||||||
|
- Have clear inputs and outputs
|
||||||
|
- Contain conditional logic or algorithms
|
||||||
|
- Are used by multiple parts of the system
|
||||||
|
|
||||||
|
### 2. Create Test File
|
||||||
|
|
||||||
|
Create a new test file in `tests/PepperDash.Essentials.Core.Tests/Unit/`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.Tests.Unit
|
||||||
|
{
|
||||||
|
public class YourComponentTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void YourMethod_WithValidInput_ReturnsExpectedResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var component = new YourComponent();
|
||||||
|
var input = "test data";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = component.YourMethod(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("expected result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Abstract Dependencies
|
||||||
|
|
||||||
|
If your component uses Crestron SDK or other external dependencies:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Create interface in Abstractions/
|
||||||
|
public interface IYourDependency
|
||||||
|
{
|
||||||
|
string DoSomething(string input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify your component to use the interface
|
||||||
|
public class YourComponent
|
||||||
|
{
|
||||||
|
private readonly IYourDependency _dependency;
|
||||||
|
|
||||||
|
public YourComponent(IYourDependency dependency)
|
||||||
|
{
|
||||||
|
_dependency = dependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string YourMethod(string input)
|
||||||
|
{
|
||||||
|
return _dependency.DoSomething(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with mocks
|
||||||
|
[Fact]
|
||||||
|
public void YourMethod_CallsDependency_ReturnsResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockDependency = new Mock<IYourDependency>();
|
||||||
|
mockDependency.Setup(x => x.DoSomething("input")).Returns("output");
|
||||||
|
var component = new YourComponent(mockDependency.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = component.YourMethod("input");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("output", result);
|
||||||
|
mockDependency.Verify(x => x.DoSomething("input"), Times.Once);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Patterns
|
||||||
|
|
||||||
|
### Testing Exceptions
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WithNullDependency_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new YourComponent(null));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Async Methods
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task YourAsyncMethod_WithValidInput_ReturnsExpectedResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var component = new YourComponent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await component.YourAsyncMethod();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Collections
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void GetItems_ReturnsExpectedCount()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var component = new YourComponent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var items = component.GetItems();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, items.Count());
|
||||||
|
Assert.Contains(items, x => x.Name == "Expected Item");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
1. **Testing Implementation Details** - Test behavior, not internal implementation
|
||||||
|
2. **Overly Complex Tests** - Keep tests simple and focused on one thing
|
||||||
|
3. **Not Testing Edge Cases** - Include null values, empty collections, boundary conditions
|
||||||
|
4. **Ignoring Test Performance** - Tests should run quickly to enable fast feedback
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
### In Visual Studio
|
||||||
|
- Set breakpoints in test methods
|
||||||
|
- Use Test Explorer to run individual tests
|
||||||
|
- Check test output window for detailed information
|
||||||
|
|
||||||
|
### From Command Line
|
||||||
|
```bash
|
||||||
|
# Run specific test
|
||||||
|
dotnet test --filter "YourComponentTests.YourMethod_WithValidInput_ReturnsExpectedResult"
|
||||||
|
|
||||||
|
# Run with detailed output
|
||||||
|
dotnet test --verbosity diagnostic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Review Examples** - Look at `TestableActionSequenceTests.cs` for comprehensive examples
|
||||||
|
2. **Start Small** - Begin with simple components and build up complexity
|
||||||
|
3. **Read Documentation** - Check `docs/testing/README.md` for detailed patterns
|
||||||
|
4. **Get Feedback** - Have your tests reviewed by team members
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Check existing test examples in the codebase
|
||||||
|
- Review the full testing documentation
|
||||||
|
- Ask team members for guidance on complex scenarios
|
||||||
|
- Consider pair programming for your first few tests
|
||||||
255
docs/testing/README.md
Normal file
255
docs/testing/README.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# Unit Testing Infrastructure
|
||||||
|
|
||||||
|
This document outlines the unit testing infrastructure and patterns for PepperDash Essentials.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The testing infrastructure is designed to enable comprehensive testing of business logic while abstracting away Crestron SDK dependencies. This allows for:
|
||||||
|
|
||||||
|
- Fast, reliable unit tests that don't require hardware
|
||||||
|
- Isolated testing of business logic
|
||||||
|
- Better code quality through testability
|
||||||
|
- Easier debugging and maintenance
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── PepperDash.Essentials.Core.Tests/
|
||||||
|
│ ├── Abstractions/ # Interface abstractions for SDK dependencies
|
||||||
|
│ ├── TestableComponents/ # Demonstration components showing abstraction patterns
|
||||||
|
│ ├── Unit/ # Unit test files
|
||||||
|
│ └── GlobalUsings.cs # Global using statements for tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Project Configuration
|
||||||
|
|
||||||
|
The test projects use:
|
||||||
|
- **.NET 8** - Modern testing framework with better performance
|
||||||
|
- **xUnit** - Primary testing framework
|
||||||
|
- **Moq** - Mocking framework for dependencies
|
||||||
|
- **Coverlet** - Code coverage analysis
|
||||||
|
|
||||||
|
## Abstraction Patterns
|
||||||
|
|
||||||
|
### Interface Segregation
|
||||||
|
|
||||||
|
Create focused interfaces that abstract SDK functionality:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IThreadService
|
||||||
|
{
|
||||||
|
void Sleep(int milliseconds);
|
||||||
|
object CreateAndStartThread(Func<object, object> threadFunction, object parameter);
|
||||||
|
void AbortThread(object thread);
|
||||||
|
bool IsThreadRunning(object thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ILogger
|
||||||
|
{
|
||||||
|
void LogDebug(object source, string message, params object[] args);
|
||||||
|
void LogVerbose(object source, string message, params object[] args);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Components should accept dependencies via constructor injection:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class TestableActionSequence
|
||||||
|
{
|
||||||
|
private readonly IQueue<TestableSequencedAction> _actionQueue;
|
||||||
|
private readonly IThreadService _threadService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
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));
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
|
||||||
|
- **One test class per component**
|
||||||
|
- **Descriptive test method names** that describe the scenario and expected outcome
|
||||||
|
- **Arrange-Act-Assert pattern** for clear test structure
|
||||||
|
|
||||||
|
### Example Test Structure
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class TestableActionSequenceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IQueue<TestableSequencedAction>> _mockQueue;
|
||||||
|
private readonly Mock<IThreadService> _mockThreadService;
|
||||||
|
private readonly Mock<ILogger> _mockLogger;
|
||||||
|
private readonly TestableActionSequence _actionSequence;
|
||||||
|
|
||||||
|
public TestableActionSequenceTests()
|
||||||
|
{
|
||||||
|
// Arrange - Set up mocks and dependencies
|
||||||
|
_mockQueue = new Mock<IQueue<TestableSequencedAction>>();
|
||||||
|
_mockThreadService = new Mock<IThreadService>();
|
||||||
|
_mockLogger = new Mock<ILogger>();
|
||||||
|
|
||||||
|
_actionSequence = new TestableActionSequence(
|
||||||
|
_mockQueue.Object,
|
||||||
|
_mockThreadService.Object,
|
||||||
|
_mockLogger.Object,
|
||||||
|
new List<TestableSequencedAction>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Naming Convention
|
||||||
|
|
||||||
|
Use descriptive names that follow the pattern:
|
||||||
|
`MethodName_Scenario_ExpectedBehavior`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `StartSequence_WhenNoThreadRunning_StartsNewThread`
|
||||||
|
- `Constructor_WithNullQueue_ThrowsArgumentNullException`
|
||||||
|
- `StopSequence_SetsAllowActionsToFalseAndAbortsThread`
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### From Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
dotnet test --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
# Run specific test class
|
||||||
|
dotnet test --filter "TestableActionSequenceTests"
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
dotnet test --verbosity normal
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Visual Studio
|
||||||
|
|
||||||
|
- Use Test Explorer to run and debug tests
|
||||||
|
- Right-click on test methods to run individually
|
||||||
|
- Use "Run Tests in Parallel" for faster execution
|
||||||
|
|
||||||
|
## Code Coverage
|
||||||
|
|
||||||
|
The test projects include Coverlet for code coverage analysis. After running tests with coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate coverage report
|
||||||
|
dotnet tool install -g reportgenerator
|
||||||
|
reportgenerator -reports:"coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:Html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Test Independence
|
||||||
|
- Each test should be independent and able to run in isolation
|
||||||
|
- Use fresh mock objects for each test
|
||||||
|
- Don't rely on test execution order
|
||||||
|
|
||||||
|
### 2. Mocking Guidelines
|
||||||
|
- Mock external dependencies (SDK calls, file system, network)
|
||||||
|
- Don't mock the component under test
|
||||||
|
- Use strict mocks when behavior verification is important
|
||||||
|
|
||||||
|
### 3. Assertions
|
||||||
|
- Make assertions specific and meaningful
|
||||||
|
- Test both positive and negative scenarios
|
||||||
|
- Verify state changes and behavior calls
|
||||||
|
|
||||||
|
### 4. Test Data
|
||||||
|
- Use meaningful test data that represents real scenarios
|
||||||
|
- Consider edge cases and boundary conditions
|
||||||
|
- Use constants or factory methods for complex test data
|
||||||
|
|
||||||
|
## Migrating Existing Code
|
||||||
|
|
||||||
|
To make existing components testable:
|
||||||
|
|
||||||
|
1. **Identify SDK Dependencies** - Look for direct Crestron SDK usage
|
||||||
|
2. **Extract Interfaces** - Create abstractions for SDK functionality
|
||||||
|
3. **Inject Dependencies** - Modify constructors to accept abstractions
|
||||||
|
4. **Create Tests** - Write comprehensive unit tests for business logic
|
||||||
|
5. **Implement Wrappers** - Create concrete implementations for production use
|
||||||
|
|
||||||
|
## Example Implementation Wrapper
|
||||||
|
|
||||||
|
When implementing the abstractions for production use:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CrestronThreadService : IThreadService
|
||||||
|
{
|
||||||
|
public void Sleep(int milliseconds)
|
||||||
|
{
|
||||||
|
Thread.Sleep(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object CreateAndStartThread(Func<object, object> threadFunction, object parameter)
|
||||||
|
{
|
||||||
|
var thread = new Thread(threadFunction, parameter, Thread.eThreadStartOptions.Running);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AbortThread(object thread)
|
||||||
|
{
|
||||||
|
if (thread is Thread crestronThread)
|
||||||
|
{
|
||||||
|
crestronThread.Abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsThreadRunning(object thread)
|
||||||
|
{
|
||||||
|
return thread is Thread crestronThread &&
|
||||||
|
crestronThread.ThreadState == Thread.eThreadStates.ThreadRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
The testing infrastructure can be extended with:
|
||||||
|
|
||||||
|
- **Integration test support** for end-to-end scenarios
|
||||||
|
- **Performance testing** utilities
|
||||||
|
- **Test data builders** for complex object creation
|
||||||
|
- **Custom assertions** for domain-specific validations
|
||||||
|
- **Automated test generation** for common patterns
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
For questions about testing patterns or infrastructure:
|
||||||
|
|
||||||
|
1. Review existing test examples in the `tests/` directory
|
||||||
|
2. Check this documentation for guidance
|
||||||
|
3. Consult the team for complex abstraction scenarios
|
||||||
|
4. Consider the impact on existing plugin interfaces before major changes
|
||||||
@@ -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