From dcb6a5b803451371c15150a54bff8d633f0e36d6 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 2 Jun 2026 15:20:40 -0500 Subject: [PATCH] Add DI container and view-model tests Add a composition-root test that builds the provider with ValidateOnBuild to catch missing registrations (without touching the real hardware monitor), plus per-service registration assertions. Add a MainWindowViewModel test that constructs the view model entirely from faked IHardwareMonitorService/ILogger/IRemoteWebServer, proving the testability win. To make that possible, inject the UI DispatcherQueue (captured on the UI thread by the composition factory) instead of calling the static DispatcherQueue.GetForCurrentThread() in the view-model constructor, which threw in the headless test host; the tree-rebuild marshalling now treats a null dispatcher as direct invocation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServiceCollectionExtensionsTests.cs | 49 +++++++++++++++++++ .../ViewModels/MainWindowViewModelTests.cs | 41 ++++++++++++++++ .../ServiceCollectionExtensions.cs | 4 +- .../ViewModels/MainWindowViewModel.cs | 9 ++-- 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 LibreHardwareMonitor.Windows.WinUI.Tests/Composition/ServiceCollectionExtensionsTests.cs create mode 100644 LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/MainWindowViewModelTests.cs diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Composition/ServiceCollectionExtensionsTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Composition/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..42d4d8e --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Composition/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System.Linq; +using LibreHardwareMonitor.Windows.WinUI.Composition; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace LibreHardwareMonitor.Windows.WinUI.Tests.Composition; + +// Guards the single composition root: every dependency referenced by a registered service must +// itself be registered, otherwise the app fails to resolve its graph at launch. ValidateOnBuild +// builds the dependency call sites without instantiating the services, so this stays off the real +// hardware-monitor / kernel-driver path while still catching missing registrations. +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddAppServices_BuildsProviderWithoutMissingRegistrations() + { + ServiceProvider provider = new ServiceCollection() + .AddAppServices(NoOpStartupTracer.Instance) + .BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + + Assert.NotNull(provider); + provider.Dispose(); + } + + [Theory] + [InlineData(typeof(AppSettings))] + [InlineData(typeof(IStartupTracer))] + [InlineData(typeof(IHardwareMonitorService))] + [InlineData(typeof(ILogger))] + [InlineData(typeof(IRemoteWebServer))] + [InlineData(typeof(SensorSelectionService))] + [InlineData(typeof(PlotTrackingService))] + [InlineData(typeof(StartupService))] + [InlineData(typeof(MainWindowViewModel))] + [InlineData(typeof(IMainWindowRuntimeFactory))] + public void AddAppServices_RegistersExpectedService(System.Type serviceType) + { + IServiceCollection services = new ServiceCollection().AddAppServices(NoOpStartupTracer.Instance); + + Assert.Contains(services, descriptor => descriptor.ServiceType == serviceType); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/MainWindowViewModelTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/MainWindowViewModelTests.cs new file mode 100644 index 0000000..7f5ea74 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/MainWindowViewModelTests.cs @@ -0,0 +1,41 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Moq; +using Xunit; + +namespace LibreHardwareMonitor.Windows.WinUI.Tests.ViewModels; + +// Demonstrates the headline win of the DI refactor: MainWindowViewModel can now be constructed with +// faked domain services (no real hardware monitor / kernel driver), and it reads its settings and +// wires the web-server root provider during construction. +public class MainWindowViewModelTests +{ + [Fact] + public void Constructor_WithFakedServices_ReadsSettingsAndWiresWebServerRoot() + { + AppSettings settings = AppSettings.LoadDefault(); + var hardwareMonitor = new Mock(); + var logger = new Mock(); + var webServer = new Mock(); + + using var viewModel = new MainWindowViewModel( + settings, + hardwareMonitor.Object, + logger.Object, + new SensorSelectionService(settings), + webServer.Object, + new PlotTrackingService(), + new StartupService(), + NoOpStartupTracer.Instance); + + Assert.True(viewModel.ShowValueColumn); + Assert.True(viewModel.ShowMaxColumn); + webServer.Verify(s => s.SetRootProvider(It.IsAny>()), Times.Once); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs b/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs index 14d9e0b..20a81c4 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using LibreHardwareMonitor.Windows.WinUI.Services; using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; using LibreHardwareMonitor.Windows.WinUI.ViewModels; using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Dispatching; namespace LibreHardwareMonitor.Windows.WinUI.Composition; @@ -54,7 +55,8 @@ internal static class ServiceCollectionExtensions sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService(), + DispatcherQueue.GetForCurrentThread())); // Runtime/window-tied service factory. The window itself is constructed by App (kept out of the // container so the XAML-tied Window is not a container-managed singleton). diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index a67bafa..3f6d081 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -83,7 +83,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable ]; private readonly IHardwareMonitorService _hardwareMonitor; - private readonly DispatcherQueue _dispatcherQueue; + private readonly DispatcherQueue? _dispatcherQueue; private readonly ILogger _logger; private readonly PlotTrackingService _plotTracking; private readonly SensorSelectionService _sensorSelection; @@ -125,12 +125,13 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable IRemoteWebServer remoteWebServer, PlotTrackingService plotTracking, StartupService startupService, - IStartupTracer startupTrace) + IStartupTracer startupTrace, + DispatcherQueue? dispatcherQueue = null) { Settings = settings; _startupTrace = startupTrace; _startupTrace.Mark("MainWindowViewModel.Constructor.Begin"); - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _dispatcherQueue = dispatcherQueue; _hardwareMonitor = hardwareMonitor; _logger = logger; _sensorSelection = sensorSelection; @@ -941,7 +942,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private void HardwareMonitor_TreeRebuilt(object? sender, EventArgs e) { - if (_dispatcherQueue.HasThreadAccess) + if (_dispatcherQueue is null || _dispatcherQueue.HasThreadAccess) { UpdateRoot(); UpdateStatus();