Partager via


Ajouter des tests unitaires

Maintenant que vos viewModels et services se trouvent dans une bibliothèque de classes distincte, vous pouvez facilement créer des tests unitaires. L’ajout de projets de test unitaire vous permet de vérifier que vos ViewModels et services se comportent comme prévu sans compter sur la couche d’interface utilisateur ou les tests manuels. Vous pouvez exécuter automatiquement des tests unitaires dans le cadre de votre workflow de développement, ce qui garantit que votre code reste fiable et gérable.

Créer un projet de test unitaire

  1. Cliquez avec le bouton droit sur la solution dans l’Explorateur de solutions.
  2. Sélectionnez Ajouter>un nouveau projet....
  3. Choisissez le modèle d’application de test unitaire WinUI , puis sélectionnez Suivant.
  4. Nommez le projet WinUINotes.Tests et sélectionnez Créer.

Ajouter des références de projet

  1. Cliquez avec le bouton droit sur le projet WinUINotes.Tests , puis sélectionnez Ajouter une>référence de projet....
  2. Vérifiez le projet WinUINotes.Bus , puis sélectionnez OK.

Créer des implémentations factices pour les tests

Pour les tests, créez des implémentations factices du service de fichiers et des classes de stockage qui n’écrivent pas réellement sur le disque. Les faux sont des implémentations légères qui simulent le comportement des dépendances réelles à des fins de test.

  1. Dans le projet WinUINotes.Tests , créez un dossier nommé Fakes.

  2. Ajoutez un fichier de classe FakeFileService.cs dans le dossier Fakes :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Windows.Storage;
    using WinUINotes.Services;
    
    namespace WinUINotes.Tests.Fakes
    {
        internal class FakeFileService : IFileService
        {
            private Dictionary<string, string> fileStorage = [];
    
            public async Task CreateOrUpdateFileAsync(string filename, string contents)
            {
                if (fileStorage.ContainsKey(filename))
                {
                    fileStorage[filename] = contents;
                }
                else
                {
                    fileStorage.Add(filename, contents);
                }
    
                await Task.Delay(10); // Simulate some async work
            }
    
            public async Task DeleteFileAsync(string filename)
            {
                if (fileStorage.ContainsKey(filename))
                {
                    fileStorage.Remove(filename);
                }
    
                await Task.Delay(10); // Simulate some async work
            }
    
            public bool FileExists(string filename)
            {
                if (string.IsNullOrEmpty(filename))
                {
                    throw new ArgumentException("Filename cannot be null or empty", nameof(filename));
                }
    
                if (fileStorage.ContainsKey(filename))
                {
                    return true;
                }
    
                return false;
            }
    
            public IStorageFolder GetLocalFolder()
            {
                return new FakeStorageFolder(fileStorage);
            }
    
            public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync()
            {
                await Task.Delay(10);
                return GetStorageItemsInternal();
            }
    
            public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync(IStorageFolder storageFolder)
            {
                await Task.Delay(10);
                return GetStorageItemsInternal();
            }
    
            private IReadOnlyList<IStorageItem> GetStorageItemsInternal()
            {
                return fileStorage.Keys.Select(filename => CreateFakeStorageItem(filename)).ToList();
            }
    
            private IStorageItem CreateFakeStorageItem(string filename)
            {
                return new FakeStorageFile(filename);
            }
    
            public async Task<string> GetTextFromFileAsync(IStorageFile file)
            {
                await Task.Delay(10);
    
                if (fileStorage.ContainsKey(file.Name))
                {
                    return fileStorage[file.Name];
                }
    
                return string.Empty;
            }
        }
    }
    

    Il FakeFileService utilise un dictionnaire en mémoire (fileStorage) pour simuler des opérations de fichier sans toucher le système de fichiers réel. Les principales fonctionnalités sont les suivantes :

    • Simulation asynchrone : utilise Task.Delay(10) pour imiter des opérations de fichiers asynchrones réelles
    • Validation : lève des exceptions pour les entrées non valides, tout comme l’implémentation réelle
    • Intégration à des classes de stockage factices : retourne FakeStorageFolder et FakeStorageFile instances qui fonctionnent ensemble pour simuler l’API de stockage Windows
  3. Ajouter FakeStorageFolder.cs:

    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices.WindowsRuntime;
    using Windows.Foundation;
    using Windows.Storage;
    using Windows.Storage.FileProperties;
    using Windows.Storage.Search;
    
    namespace WinUINotes.Tests.Fakes
    {
        internal class FakeStorageFolder : IStorageFolder
        {
            private string name;
            private Dictionary<string, string> fileStorage = [];
    
            public FakeStorageFolder(Dictionary<string, string> files)
            {
                fileStorage = files;
            }
    
            public FileAttributes Attributes => throw new NotImplementedException();
            public DateTimeOffset DateCreated => throw new NotImplementedException();
            public string Name => name;
            public string Path => throw new NotImplementedException();
    
            public IAsyncOperation<StorageFile> CreateFileAsync(string desiredName)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CreateFileAsync(string desiredName, CreationCollisionOption options)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFolder> CreateFolderAsync(string desiredName)
            {
                throw new NotImplementedException();
            }
    
            // Only partial implementation shown for brevity
            ...
        }
    }
    

    `FakeStorageFolder` prend le dictionnaire de stockage de fichiers dans son constructeur, ce qui lui permet de fonctionner avec le même système de fichiers en mémoire que `FakeFileService`. La plupart des membres de l’interface lèvent NotImplementedException, car seules les propriétés et méthodes réellement utilisées par les tests doivent être implémentées.

    Vous pouvez consulter l’implémentation complète de FakeStorageFolder dans le référentiel GitHub pour ce tutoriel.

  4. Ajouter FakeStorageFile.cs:

    using System;
    using System.IO;
    using System.Runtime.InteropServices.WindowsRuntime;
    using Windows.Foundation;
    using Windows.Storage;
    using Windows.Storage.FileProperties;
    using Windows.Storage.Streams;
    
    namespace WinUINotes.Tests.Fakes
    {
        public class FakeStorageFile : IStorageFile
        {
            private string name;
    
            public FakeStorageFile(string name)
            {
                this.name = name;
            }
    
            public string ContentType => throw new NotImplementedException();
            public string FileType => throw new NotImplementedException();
            public FileAttributes Attributes => throw new NotImplementedException();
            public DateTimeOffset DateCreated => throw new NotImplementedException();
            public string Name => name;
            public string Path => throw new NotImplementedException();
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace)
            {
                throw new NotImplementedException();
            }
    
            // Only partial implementation shown for brevity
            ...
        }
    }
    

    FakeStorageFile représente des fichiers individuels dans le système de stockage factice. Il stocke le nom de fichier et fournit l’implémentation minimale nécessaire pour les tests. Comme FakeStorageFolder, il implémente uniquement les membres qui sont réellement utilisés par le code en cours de test.

    Vous pouvez afficher l’implémentation complète de FakeStorageFolder dans le dépôt de code GitHub pour ce tutoriel.

En savoir plus dans la documentation :

Écrire un test unitaire simple

  1. Renommez UnitTest1.cs en NoteTests.cs et mettez-le à jour.

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using WinUINotes.Tests.Fakes;
    
    namespace WinUINotes.Tests
    {
        [TestClass]
        public partial class NoteTests
        {
            [TestMethod]
            public void TestCreateUnsavedNote()
            {
                var noteVm = new ViewModels.NoteViewModel(new FakeFileService());
                Assert.IsNotNull(noteVm);
                Assert.IsTrue(noteVm.Date > DateTime.Now.AddHours(-1));
                Assert.IsTrue(noteVm.Filename.EndsWith(".txt"));
                Assert.IsTrue(noteVm.Filename.StartsWith("notes"));
                noteVm.Text = "Sample Note";
                Assert.AreEqual("Sample Note", noteVm.Text);
                noteVm.SaveCommand.Execute(null);
                Assert.AreEqual("Sample Note", noteVm.Text);
            }
        }
    }
    

    Ce test montre comment effectuer un test unitaire du NoteViewModel à l’aide du FakeFileService fichier. Le test crée un nouveau NoteViewModelfichier, vérifie son état initial (la date est récente, le nom de fichier suit le modèle attendu), définit du texte sur la note, exécute la commande enregistrer et confirme que le texte persiste. Étant donné que le service de fichiers factice est utilisé au lieu de l’implémentation réelle, le test s’exécute rapidement sans E/S de fichier réel et peut s’exécuter à plusieurs reprises sans effets secondaires.

En savoir plus dans la documentation :

Exécuter les tests

  1. Ouvrez la fenêtre Explorateur de tests dans Visual Studio (Test>Explorateur de tests).
  2. Sélectionnez Exécuter tous les tests pour exécuter votre test unitaire.
  3. Vérifiez que le test réussit.

Vous disposez maintenant d’une architecture testable où vous pouvez tester vos ViewModels et services indépendamment de l’interface utilisateur !

Résumé

Dans cette série de tutoriels, vous avez appris à :

  • Créez un projet de bibliothèque de classes distinct (projet Bus) pour contenir vos viewModels et services, ce qui permet aux tests unitaires distincts de la couche d’interface utilisateur.
  • Implémentez le modèle MVVM en utilisant le Kit d'outils MVVM, en profitant des attributs ObservableObject, [ObservableProperty] et [RelayCommand] pour réduire le code standard.
  • Utilisez des générateurs sources pour créer automatiquement des notifications de modification de propriété et des implémentations de commandes.
  • Permet [NotifyCanExecuteChangedFor] de mettre à jour automatiquement la disponibilité des commandes lorsque les valeurs de propriété changent.
  • Intégrez l'injection de dépendances en utilisant Microsoft.Extensions.DependencyInjection pour gérer le cycle de vie des ViewModels et des services.
  • Créez une interface et une IFileService implémentation pour gérer les opérations de fichier de manière testable.
  • Configurez le conteneur d’adresses di dans App.xaml.cs et récupérez ViewModels à partir du fournisseur de services dans vos pages.
  • Implémentez le WeakReferenceMessenger pour permettre un couplage libre entre les composants, afin de permettre aux pages de répondre aux événements du ViewModel sans références directes.
  • Créez des classes de message qui héritent de ValueChangedMessage<T> pour transporter des données entre les composants.
  • Créez des implémentations factices de dépendances pour les tests sans toucher le système de fichiers réel.
  • Écrivez des tests unitaires à l’aide de MSTest pour vérifier le comportement viewModel indépendamment de la couche d’interface utilisateur.

Cette architecture fournit une base solide pour créer des applications WinUI maintenables et testables avec une séparation claire des préoccupations entre l’interface utilisateur, la logique métier et les couches d’accès aux données. Vous pouvez télécharger ou afficher le code de ce didacticiel à partir du dépôt GitHub.

Étapes suivantes

Maintenant que vous comprenez comment implémenter MVVM avec le kit de ressources MVVM et l’injection de dépendances, vous pouvez explorer des rubriques plus avancées :

  • Messagerie avancée : explorez des modèles de messagerie supplémentaires, notamment les messages de demande/réponse et les jetons de message pour la gestion sélective des messages.
  • Validation : ajoutez la validation d’entrée à vos ViewModels à l’aide d’annotations de données et des fonctionnalités de validation du kit de ressources MVVM.
  • Commandes asynchrones : En savoir plus sur l’exécution de commandes asynchrones, la prise en charge de l’annulation et la création de rapports de progression avec AsyncRelayCommand.
  • Tests avancés : explorez des scénarios de test plus avancés, notamment le test de la gestion des messages, l’exécution de commandes asynchrones et les notifications de modification de propriété.
  • Regroupements observables : utilisez ObservableCollection<T> efficacement et explorez ObservableRangeCollection<T> les opérations en bloc.