mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-09 17:54:59 +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:
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
|
||||
Reference in New Issue
Block a user