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>
This commit is contained in:
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<IHardwareMonitorService>();
|
||||
var logger = new Mock<ILogger>();
|
||||
var webServer = new Mock<IRemoteWebServer>();
|
||||
|
||||
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<Func<SensorTreeItemViewModel?>>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -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<IRemoteWebServer>(),
|
||||
sp.GetRequiredService<PlotTrackingService>(),
|
||||
sp.GetRequiredService<StartupService>(),
|
||||
sp.GetRequiredService<IStartupTracer>()));
|
||||
sp.GetRequiredService<IStartupTracer>(),
|
||||
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).
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user