test: initial attempt at tests with Claude Code

This commit is contained in:
Sumanth Rayancha
2025-08-11 22:21:14 -04:00
parent e1e32cea6f
commit 90b6f258f0
11 changed files with 1211 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
using System;
using FluentAssertions;
using Moq;
using PepperDash.Essentials.Core.Abstractions;
using Xunit;
namespace PepperDash.Essentials.Core.Tests.Abstractions
{
public class DigitalInputTests
{
[Fact]
public void StateChange_WhenDigitalInputChanges_RaisesEvent()
{
// Arrange
var mockDigitalInput = new Mock<IDigitalInput>();
var eventRaised = false;
bool capturedState = false;
mockDigitalInput.Setup(d => d.State).Returns(true);
// Subscribe to the event
mockDigitalInput.Object.StateChange += (sender, args) =>
{
eventRaised = true;
capturedState = args.State;
};
// Act - Raise the event
mockDigitalInput.Raise(d => d.StateChange += null,
mockDigitalInput.Object,
new DigitalInputEventArgs(true));
// Assert
eventRaised.Should().BeTrue();
capturedState.Should().BeTrue();
}
[Fact]
public void State_ReturnsCorrectValue()
{
// Arrange
var mockDigitalInput = new Mock<IDigitalInput>();
mockDigitalInput.Setup(d => d.State).Returns(true);
// Act
var state = mockDigitalInput.Object.State;
// Assert
state.Should().BeTrue();
}
[Fact]
public void MultipleStateChanges_TrackStateCorrectly()
{
// Arrange
var mockDigitalInput = new Mock<IDigitalInput>();
var stateChanges = new System.Collections.Generic.List<bool>();
mockDigitalInput.Object.StateChange += (sender, args) =>
{
stateChanges.Add(args.State);
};
// Act - Simulate multiple state changes
mockDigitalInput.Raise(d => d.StateChange += null,
mockDigitalInput.Object,
new DigitalInputEventArgs(true));
mockDigitalInput.Raise(d => d.StateChange += null,
mockDigitalInput.Object,
new DigitalInputEventArgs(false));
mockDigitalInput.Raise(d => d.StateChange += null,
mockDigitalInput.Object,
new DigitalInputEventArgs(true));
// Assert
stateChanges.Should().HaveCount(3);
stateChanges[0].Should().BeTrue();
stateChanges[1].Should().BeFalse();
stateChanges[2].Should().BeTrue();
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using FluentAssertions;
using Moq;
using PepperDash.Essentials.Core.Abstractions;
using Xunit;
namespace PepperDash.Essentials.Core.Tests.Abstractions
{
public class VersiPortTests
{
[Fact]
public void DigitalIn_ReturnsCorrectValue()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
mockVersiPort.Setup(v => v.DigitalIn).Returns(true);
// Act
var digitalIn = mockVersiPort.Object.DigitalIn;
// Assert
digitalIn.Should().BeTrue();
}
[Fact]
public void SetDigitalOut_SetsCorrectValue()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
// Act
mockVersiPort.Object.SetDigitalOut(true);
// Assert
mockVersiPort.Verify(v => v.SetDigitalOut(true), Times.Once);
}
[Fact]
public void AnalogIn_ReturnsCorrectValue()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
ushort expectedValue = 32768;
mockVersiPort.Setup(v => v.AnalogIn).Returns(expectedValue);
// Act
var analogIn = mockVersiPort.Object.AnalogIn;
// Assert
analogIn.Should().Be(expectedValue);
}
[Fact]
public void VersiportChange_WhenDigitalChanges_RaisesEventWithCorrectType()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
var eventRaised = false;
VersiPortEventType? capturedEventType = null;
object capturedValue = null;
mockVersiPort.Object.VersiportChange += (sender, args) =>
{
eventRaised = true;
capturedEventType = args.EventType;
capturedValue = args.Value;
};
// Act
mockVersiPort.Raise(v => v.VersiportChange += null,
mockVersiPort.Object,
new VersiPortEventArgs
{
EventType = VersiPortEventType.DigitalInChange,
Value = true
});
// Assert
eventRaised.Should().BeTrue();
capturedEventType.Should().Be(VersiPortEventType.DigitalInChange);
capturedValue.Should().Be(true);
}
[Fact]
public void VersiportChange_WhenAnalogChanges_RaisesEventWithCorrectValue()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
var eventRaised = false;
VersiPortEventType? capturedEventType = null;
object capturedValue = null;
ushort expectedAnalogValue = 12345;
mockVersiPort.Object.VersiportChange += (sender, args) =>
{
eventRaised = true;
capturedEventType = args.EventType;
capturedValue = args.Value;
};
// Act
mockVersiPort.Raise(v => v.VersiportChange += null,
mockVersiPort.Object,
new VersiPortEventArgs
{
EventType = VersiPortEventType.AnalogInChange,
Value = expectedAnalogValue
});
// Assert
eventRaised.Should().BeTrue();
capturedEventType.Should().Be(VersiPortEventType.AnalogInChange);
capturedValue.Should().Be(expectedAnalogValue);
}
[Fact]
public void MultipleVersiportChanges_TracksAllChangesCorrectly()
{
// Arrange
var mockVersiPort = new Mock<IVersiPort>();
var changes = new System.Collections.Generic.List<(VersiPortEventType type, object value)>();
mockVersiPort.Object.VersiportChange += (sender, args) =>
{
changes.Add((args.EventType, args.Value));
};
// Act - Simulate multiple changes
mockVersiPort.Raise(v => v.VersiportChange += null,
mockVersiPort.Object,
new VersiPortEventArgs
{
EventType = VersiPortEventType.DigitalInChange,
Value = true
});
mockVersiPort.Raise(v => v.VersiportChange += null,
mockVersiPort.Object,
new VersiPortEventArgs
{
EventType = VersiPortEventType.AnalogInChange,
Value = (ushort)30000
});
mockVersiPort.Raise(v => v.VersiportChange += null,
mockVersiPort.Object,
new VersiPortEventArgs
{
EventType = VersiPortEventType.DigitalInChange,
Value = false
});
// Assert
changes.Should().HaveCount(3);
changes[0].type.Should().Be(VersiPortEventType.DigitalInChange);
changes[0].value.Should().Be(true);
changes[1].type.Should().Be(VersiPortEventType.AnalogInChange);
changes[1].value.Should().Be((ushort)30000);
changes[2].type.Should().Be(VersiPortEventType.DigitalInChange);
changes[2].value.Should().Be(false);
}
}
}

View File

@@ -0,0 +1,193 @@
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using PepperDash.Essentials.Core.Abstractions;
using PepperDash.Essentials.Core.Devices;
using Xunit;
namespace PepperDash.Essentials.Core.Tests.Devices
{
public class CrestronProcessorTests
{
[Fact]
public void Constructor_WithValidProcessor_InitializesSwitchedOutputs()
{
// Arrange
var mockProcessor = new Mock<ICrestronControlSystem>();
mockProcessor.Setup(p => p.SupportsRelay).Returns(false);
// Act
var processor = new CrestronProcessorTestable("test-processor", mockProcessor.Object);
// Assert
processor.Should().NotBeNull();
processor.Key.Should().Be("test-processor");
processor.SwitchedOutputs.Should().NotBeNull();
processor.SwitchedOutputs.Should().BeEmpty();
}
[Fact]
public void GetRelays_WhenProcessorSupportsRelays_CreatesRelayDevices()
{
// Arrange
var mockProcessor = new Mock<ICrestronControlSystem>();
var mockRelayPort1 = new Mock<IRelayPort>();
var mockRelayPort2 = new Mock<IRelayPort>();
var relayPorts = new Dictionary<uint, IRelayPort>
{
{ 1, mockRelayPort1.Object },
{ 2, mockRelayPort2.Object }
};
mockProcessor.Setup(p => p.SupportsRelay).Returns(true);
mockProcessor.Setup(p => p.NumberOfRelayPorts).Returns(2);
mockProcessor.Setup(p => p.RelayPorts).Returns(relayPorts);
// Act
var processor = new CrestronProcessorTestable("test-processor", mockProcessor.Object);
// Assert
processor.SwitchedOutputs.Should().HaveCount(2);
processor.SwitchedOutputs.Should().ContainKey(1);
processor.SwitchedOutputs.Should().ContainKey(2);
}
[Fact]
public void GetRelays_WhenProcessorDoesNotSupportRelays_DoesNotCreateRelayDevices()
{
// Arrange
var mockProcessor = new Mock<ICrestronControlSystem>();
mockProcessor.Setup(p => p.SupportsRelay).Returns(false);
// Act
var processor = new CrestronProcessorTestable("test-processor", mockProcessor.Object);
// Assert
processor.SwitchedOutputs.Should().BeEmpty();
mockProcessor.Verify(p => p.NumberOfRelayPorts, Times.Never);
mockProcessor.Verify(p => p.RelayPorts, Times.Never);
}
}
public class GenericRelayDeviceTests
{
[Fact]
public void OpenRelay_CallsRelayPortOpen()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.OpenRelay();
// Assert
mockRelayPort.Verify(r => r.Open(), Times.Once);
}
[Fact]
public void CloseRelay_CallsRelayPortClose()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.CloseRelay();
// Assert
mockRelayPort.Verify(r => r.Close(), Times.Once);
}
[Fact]
public void PulseRelay_CallsRelayPortPulseWithCorrectDelay()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
const int delayMs = 500;
// Act
device.PulseRelay(delayMs);
// Assert
mockRelayPort.Verify(r => r.Pulse(delayMs), Times.Once);
}
[Fact]
public void On_CallsCloseRelay()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.On();
// Assert
mockRelayPort.Verify(r => r.Close(), Times.Once);
}
[Fact]
public void Off_CallsOpenRelay()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.Off();
// Assert
mockRelayPort.Verify(r => r.Open(), Times.Once);
}
[Fact]
public void PowerToggle_WhenRelayIsOn_CallsOff()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
mockRelayPort.Setup(r => r.State).Returns(true); // Relay is ON
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.PowerToggle();
// Assert
mockRelayPort.Verify(r => r.Open(), Times.Once);
mockRelayPort.Verify(r => r.Close(), Times.Never);
}
[Fact]
public void PowerToggle_WhenRelayIsOff_CallsOn()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
mockRelayPort.Setup(r => r.State).Returns(false); // Relay is OFF
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.PowerToggle();
// Assert
mockRelayPort.Verify(r => r.Close(), Times.Once);
mockRelayPort.Verify(r => r.Open(), Times.Never);
}
[Fact]
public void IsOn_ReturnsRelayPortState()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
mockRelayPort.Setup(r => r.State).Returns(true);
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
var isOn = device.IsOn;
// Assert
isOn.Should().BeTrue();
}
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
<ProjectReference Include="..\..\src\PepperDash.Core\PepperDash.Core.csproj" />
</ItemGroup>
</Project>

170
tests/README.md Normal file
View File

@@ -0,0 +1,170 @@
# PepperDash Essentials Unit Testing Guide
## Overview
This guide demonstrates how to write unit tests for PepperDash Essentials despite the Crestron hardware dependencies. The key approach is to use **abstraction layers** and **dependency injection** to isolate Crestron-specific functionality.
## Architecture Pattern
### 1. Abstraction Layer
We create interfaces that abstract Crestron hardware components:
- `ICrestronControlSystem` - Abstracts the control system
- `IRelayPort` - Abstracts relay functionality
- `IDigitalInput` - Abstracts digital inputs
- `IVersiPort` - Abstracts VersiPorts
### 2. Adapters
Adapter classes wrap actual Crestron objects in production:
- `CrestronControlSystemAdapter` - Wraps `CrestronControlSystem`
- `RelayPortAdapter` - Wraps Crestron `Relay`
- `DigitalInputAdapter` - Wraps Crestron `DigitalInput`
- `VersiPortAdapter` - Wraps Crestron `Versiport`
### 3. Testable Classes
Create testable versions of classes that accept abstractions:
- `CrestronProcessorTestable` - Accepts `ICrestronControlSystem` instead of concrete type
- `GenericRelayDeviceTestable` - Accepts `IRelayPort` instead of concrete type
## Writing Tests
### Basic Test Example
```csharp
[Fact]
public void OpenRelay_CallsRelayPortOpen()
{
// Arrange
var mockRelayPort = new Mock<IRelayPort>();
var device = new GenericRelayDeviceTestable("test-relay", mockRelayPort.Object);
// Act
device.OpenRelay();
// Assert
mockRelayPort.Verify(r => r.Open(), Times.Once);
}
```
### Testing Events
```csharp
[Fact]
public void StateChange_WhenDigitalInputChanges_RaisesEvent()
{
// Arrange
var mockDigitalInput = new Mock<IDigitalInput>();
var eventRaised = false;
mockDigitalInput.Object.StateChange += (sender, args) => eventRaised = true;
// Act
mockDigitalInput.Raise(d => d.StateChange += null,
mockDigitalInput.Object,
new DigitalInputEventArgs(true));
// Assert
eventRaised.Should().BeTrue();
}
```
## Running Tests
### Locally
```bash
# Run all tests
dotnet test
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Run specific test project
dotnet test tests/PepperDash.Essentials.Core.Tests
```
### CI Pipeline
Tests run automatically on:
- Push to main/develop/net8-updates branches
- Pull requests
- GitHub Actions workflow generates coverage reports
## Migration Strategy
To migrate existing code to be testable:
1. **Identify Crestron Dependencies**
- Search for `using Crestron` statements
- Find direct hardware interactions
2. **Create Abstractions**
- Define interfaces for hardware components
- Keep interfaces focused and simple
3. **Implement Adapters**
- Wrap Crestron objects with adapters
- Map Crestron events to abstraction events
4. **Refactor Classes**
- Accept abstractions via constructor injection
- Create factory methods for production use
5. **Write Tests**
- Mock abstractions using Moq
- Test business logic independently
## Best Practices
### DO:
- Keep abstractions simple and focused
- Test business logic, not Crestron SDK behavior
- Use dependency injection consistently
- Mock at the abstraction boundary
- Test event handling and state changes
### DON'T:
- Try to mock Crestron types directly
- Include hardware-dependent code in tests
- Mix business logic with hardware interaction
- Create overly complex abstractions
## Tools Used
- **xUnit** - Test framework
- **Moq** - Mocking framework
- **FluentAssertions** - Assertion library
- **Coverlet** - Code coverage
- **GitHub Actions** - CI/CD
## Adding New Tests
1. Create test file in appropriate folder
2. Follow naming convention: `[ClassName]Tests.cs`
3. Use Arrange-Act-Assert pattern
4. Include both positive and negative test cases
5. Test edge cases and error conditions
## Troubleshooting
### Common Issues:
**Tests fail with "Type not found" errors**
- Ensure abstractions are properly defined
- Check project references
**Mocked events not firing**
- Use `Mock.Raise()` to trigger events
- Verify event subscription syntax
**Coverage not generating**
- Run with `--collect:"XPlat Code Coverage"`
- Check .gitignore isn't excluding coverage files
## Example Test Project Structure
```
tests/
├── PepperDash.Essentials.Core.Tests/
│ ├── Abstractions/
│ │ ├── DigitalInputTests.cs
│ │ └── VersiPortTests.cs
│ ├── Devices/
│ │ └── CrestronProcessorTests.cs
│ └── PepperDash.Essentials.Core.Tests.csproj
└── README.md
```