Files
hiha-arvio/tests/HihaArvio.Tests/Integration/ServiceIntegrationTests.cs
Ismo Vuorinen 7c3916dab1 feat: Complete Milestone 5 - Platform-Specific Implementations
Implemented platform-agnostic accelerometer abstraction with iOS and desktop implementations:

**IAccelerometerService Interface:**
- Platform-agnostic abstraction for sensor input
- SensorReading model (renamed from AccelerometerData to avoid MAUI conflict)
- X, Y, Z acceleration values in g-force units
- ReadingChanged event for continuous data stream
- Start/Stop methods for sensor control
- IsSupported property for platform capability detection

**IosAccelerometerService (10 tests):**
- Uses MAUI's Microsoft.Maui.Devices.Sensors.Accelerometer
- Works on iOS devices and simulator (provides simulated data)
- Conditional compilation (#if IOS || MACCATALYST)
- Converts MAUI AccelerometerData to our SensorReading
- Graceful failure when sensor unavailable
- Pragma directives for conditional warnings

**DesktopAccelerometerService (11 tests):**
- Timer-based simulated sensor readings (~60Hz)
- Generates realistic noise around 1g gravity baseline
- Device-at-rest simulation: X≈0, Y≈0, Z≈1
- Includes SimulateShake() method for manual testing
- Always reports IsSupported=true (simulation available)

**ShakeDetectionService Integration:**
- Updated constructor to require IAccelerometerService (breaking change)
- StartMonitoring: subscribes to ReadingChanged, calls accelerometer.Start()
- StopMonitoring: unsubscribes and calls accelerometer.Stop()
- OnAccelerometerReadingChanged: pipes readings to ProcessAccelerometerReading
- All existing shake detection logic unchanged

**Platform-Specific DI:**
- MauiProgram.cs uses conditional compilation
- iOS/macOS Catalyst: IosAccelerometerService
- Desktop/other platforms: DesktopAccelerometerService
- All services registered as Singleton

**Test Updates:**
- ShakeDetectionServiceTests: now uses NSubstitute mock accelerometer
- ServiceIntegrationTests: uses DesktopAccelerometerService
- AccelerometerServiceContractTests: base class for implementation tests
- IosAccelerometerServiceTests: 10 tests with platform-aware assertions
- DesktopAccelerometerServiceTests: 11 tests with async timing

**Technical Details:**
- SensorReading record type with required init properties
- Value equality for SensorReading (record type benefit)
- Timer disposal in DesktopAccelerometerService
- Event subscription safety (check for null, unsubscribe on stop)
- 189 tests passing (165 previous + 24 accelerometer)
- 0 warnings, 0 errors across all platforms

Build verified: net8.0, iOS (net8.0-ios), macOS Catalyst (net8.0-maccatalyst)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:11:14 +02:00

324 lines
11 KiB
C#

using FluentAssertions;
using HihaArvio.Models;
using HihaArvio.Services;
using HihaArvio.Services.Interfaces;
namespace HihaArvio.Tests.Integration;
/// <summary>
/// Integration tests verifying that services work together correctly.
/// </summary>
public class ServiceIntegrationTests : IDisposable
{
private readonly IEstimateService _estimateService;
private readonly IStorageService _storageService;
private readonly IShakeDetectionService _shakeDetectionService;
private readonly IAccelerometerService _accelerometerService;
private readonly string _testDbPath;
public ServiceIntegrationTests()
{
_estimateService = new EstimateService();
_testDbPath = Path.Combine(Path.GetTempPath(), $"test_integration_{Guid.NewGuid()}.db");
_storageService = new StorageService(_testDbPath);
_accelerometerService = new DesktopAccelerometerService();
_shakeDetectionService = new ShakeDetectionService(_accelerometerService);
}
public void Dispose()
{
if (File.Exists(_testDbPath))
{
File.Delete(_testDbPath);
}
}
#region EstimateService + StorageService Integration
[Fact]
public async Task GenerateAndSaveEstimate_ShouldPersistToStorage()
{
// Arrange
var intensity = 0.75;
var duration = TimeSpan.FromSeconds(5);
var mode = EstimateMode.Work;
// Act - Generate estimate
var estimate = _estimateService.GenerateEstimate(intensity, duration, mode);
// Save to storage
await _storageService.SaveEstimateAsync(estimate);
// Load from storage
var history = await _storageService.GetHistoryAsync();
// Assert
history.Should().ContainSingle();
var saved = history.First();
saved.Id.Should().Be(estimate.Id);
saved.EstimateText.Should().Be(estimate.EstimateText);
saved.Mode.Should().Be(estimate.Mode);
saved.ShakeIntensity.Should().Be(estimate.ShakeIntensity);
saved.ShakeDuration.Should().Be(estimate.ShakeDuration);
}
[Fact]
public async Task GenerateMultipleEstimates_ShouldRespectHistoryLimit()
{
// Arrange
var settings = new AppSettings { MaxHistorySize = 3 };
await _storageService.SaveSettingsAsync(settings);
// Act - Generate and save 5 estimates
for (int i = 0; i < 5; i++)
{
var estimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
await _storageService.SaveEstimateAsync(estimate);
await Task.Delay(10); // Ensure different timestamps
}
// Assert - Should only keep 3 newest
var count = await _storageService.GetHistoryCountAsync();
count.Should().Be(3);
}
[Fact]
public async Task LoadSettings_GenerateEstimate_ShouldUseCorrectMode()
{
// Arrange - Save settings with Generic mode
var settings = new AppSettings { SelectedMode = EstimateMode.Generic };
await _storageService.SaveSettingsAsync(settings);
// Act - Load settings and generate estimate
var loadedSettings = await _storageService.LoadSettingsAsync();
var estimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), loadedSettings.SelectedMode);
// Assert
estimate.Mode.Should().Be(EstimateMode.Generic);
}
#endregion
#region ShakeDetectionService + EstimateService Integration
[Fact]
public void DetectShake_GenerateEstimate_ShouldUseShakeData()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
var shakeData = _shakeDetectionService.CurrentShakeData;
// Generate estimate based on shake
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
// Assert
estimate.ShakeIntensity.Should().Be(shakeData.Intensity);
estimate.ShakeDuration.Should().Be(shakeData.Duration);
estimate.EstimateText.Should().NotBeNullOrEmpty();
}
[Fact]
public void DetectLongShake_GenerateEstimate_ShouldTriggerHumorousMode()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake and manually set long duration for test
_shakeDetectionService.ProcessAccelerometerReading(2.5, 2.0, 1.5);
// Simulate 16-second shake by generating estimate with long duration
var estimate = _estimateService.GenerateEstimate(
0.5,
TimeSpan.FromSeconds(16), // Exceeds 15-second threshold
EstimateMode.Work);
// Assert - Should switch to Humorous mode (easter egg)
estimate.Mode.Should().Be(EstimateMode.Humorous);
estimate.EstimateText.Should().BeOneOf(
"5 minutes", "tomorrow", "eventually", "next quarter",
"when hell freezes over", "3 lifetimes", "Tuesday", "never", "your retirement");
}
[Fact]
public void DetectVaryingIntensity_GenerateEstimates_ShouldProduceDifferentRanges()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Test low, medium, and high intensity shakes
var estimates = new List<EstimateResult>();
// Low intensity shake
_shakeDetectionService.ProcessAccelerometerReading(1.8, 1.5, 0.5);
var lowShake = _shakeDetectionService.CurrentShakeData;
lowShake.Intensity.Should().BeLessThan(0.3);
estimates.Add(_estimateService.GenerateEstimate(lowShake.Intensity, lowShake.Duration, EstimateMode.Work));
// High intensity shake
_shakeDetectionService.Reset();
_shakeDetectionService.ProcessAccelerometerReading(4.0, 3.5, 3.0);
var highShake = _shakeDetectionService.CurrentShakeData;
highShake.Intensity.Should().BeGreaterThan(0.7);
estimates.Add(_estimateService.GenerateEstimate(highShake.Intensity, highShake.Duration, EstimateMode.Work));
// Assert - Both should generate valid estimates
estimates.Should().AllSatisfy(e =>
{
e.EstimateText.Should().NotBeNullOrEmpty();
e.Mode.Should().Be(EstimateMode.Work);
});
}
#endregion
#region Full Flow Integration
[Fact]
public async Task CompleteFlow_ShakeToEstimateToStorage_ShouldWorkEndToEnd()
{
// Arrange
var settings = await _storageService.LoadSettingsAsync();
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake gesture
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
var shakeData = _shakeDetectionService.CurrentShakeData;
shakeData.IsShaking.Should().BeTrue();
// Generate estimate from shake
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
settings.SelectedMode);
// Save to storage
await _storageService.SaveEstimateAsync(estimate);
// Load from storage
var history = await _storageService.GetHistoryAsync();
// Assert - Complete flow succeeded
history.Should().ContainSingle();
var saved = history.First();
saved.Id.Should().Be(estimate.Id);
saved.EstimateText.Should().Be(estimate.EstimateText);
saved.ShakeIntensity.Should().Be(shakeData.Intensity);
// Reset for next shake
_shakeDetectionService.Reset();
_shakeDetectionService.CurrentShakeData.IsShaking.Should().BeFalse();
}
[Fact]
public async Task MultipleShakes_ShouldAccumulateHistory()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate 3 shake gestures
for (int i = 0; i < 3; i++)
{
// Shake
_shakeDetectionService.ProcessAccelerometerReading(2.5, 2.0, 1.5);
var shakeData = _shakeDetectionService.CurrentShakeData;
// Generate estimate
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
// Save
await _storageService.SaveEstimateAsync(estimate);
// Reset for next shake
_shakeDetectionService.Reset();
await Task.Delay(10); // Ensure different timestamps
}
// Assert
var history = await _storageService.GetHistoryAsync();
history.Should().HaveCount(3);
// Verify ordering (newest first)
for (int i = 0; i < history.Count - 1; i++)
{
history[i].Timestamp.Should().BeOnOrAfter(history[i + 1].Timestamp);
}
}
[Fact]
public async Task ChangeMode_GenerateEstimate_ShouldUseNewMode()
{
// Arrange
var initialSettings = new AppSettings { SelectedMode = EstimateMode.Work };
await _storageService.SaveSettingsAsync(initialSettings);
// Act - Generate estimate with Work mode
var workEstimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
await _storageService.SaveEstimateAsync(workEstimate);
// Change mode
var newSettings = new AppSettings { SelectedMode = EstimateMode.Humorous };
await _storageService.SaveSettingsAsync(newSettings);
// Generate estimate with new mode
var humorousEstimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Humorous);
await _storageService.SaveEstimateAsync(humorousEstimate);
// Assert
var history = await _storageService.GetHistoryAsync();
history.Should().HaveCount(2);
history[0].Mode.Should().Be(EstimateMode.Humorous); // Newest
history[1].Mode.Should().Be(EstimateMode.Work); // Oldest
}
#endregion
#region Event Integration
[Fact]
public async Task ShakeEvent_ShouldTriggerEstimateGeneration()
{
// Arrange
_shakeDetectionService.StartMonitoring();
EstimateResult? generatedEstimate = null;
_shakeDetectionService.ShakeDataChanged += async (sender, shakeData) =>
{
if (shakeData.IsShaking)
{
// Generate estimate when shake detected
generatedEstimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
await _storageService.SaveEstimateAsync(generatedEstimate);
}
};
// Act - Simulate shake
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
// Give event handler time to execute
await Task.Delay(50);
// Assert
generatedEstimate.Should().NotBeNull();
var history = await _storageService.GetHistoryAsync();
history.Should().ContainSingle();
history.First().Id.Should().Be(generatedEstimate!.Id);
}
#endregion
}