Поделиться через


Добавить модульные тесты

Теперь, когда viewModels и службы находятся в отдельной библиотеке классов, можно легко создавать модульные тесты. Добавление проектов модульного теста позволяет убедиться, что viewModels и службы работают должным образом, не опираясь на уровень пользовательского интерфейса или ручное тестирование. Модульные тесты можно запускать автоматически в рамках рабочего процесса разработки, обеспечивая надежность и обслуживание кода.

Создание проекта модульного теста

  1. Щелкните правой кнопкой мыши решение в обозревателе решений.
  2. Выберите "Добавить>новый проект...".
  3. Выберите шаблон приложения модульного теста WinUI и нажмите кнопку "Далее".
  4. Назовите проект WinUINotes.Tests и выберите Создать.

Добавление ссылок на проект

  1. Щелкните правой кнопкой мыши проект WinUINotes.Tests и выберите "Добавить>ссылку на проект...".
  2. Проверьте проект WinUINotes.Bus и нажмите кнопку "ОК".

Создание поддельных реализаций для тестирования

Для тестирования создайте поддельные реализации файловой службы и классов хранилища, которые фактически не записываются на диск. Упрощенные реализации, называемые фейками, имитируют поведение реальных зависимостей для целей тестирования.

  1. В проекте WinUINotes.Tests создайте новую папку с именем Fakes.

  2. Добавьте файл FakeFileService.cs класса в папку 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;
            }
        }
    }
    

    Этот компонент использует словарь в памяти (fileStorage) для имитации операций с файлами, не взаимодействуя с фактической файловой системой. К ключевым функциям относятся:

    • Асинхронное моделирование: используется Task.Delay(10) для имитации реальных асинхронных операций файлов
    • Проверка. Создает исключения для недопустимых входных данных, как и реальная реализация
    • Интеграция с поддельными классами хранилища: возвращает FakeStorageFolder и FakeStorageFile экземпляры, которые работают вместе для имитации API хранилища Windows
  3. Добавить 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, что позволяет работать с той же файловой системой в памяти, что и FakeFileService. Большинство членов интерфейса выдает исключение NotImplementedException, так как необходимо реализовать только свойства и методы, которые фактически используются тестами.

    Вы можете просмотреть полную реализацию FakeStorageFolder в репозитории кода GitHub для этого руководства.

  4. Добавить 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 обозначает отдельные файлы в фальшивой системе хранения. Он сохраняет имя файла и обеспечивает минимальную реализацию, необходимую для тестов. Например FakeStorageFolder, он реализует только элементы, которые фактически используются тестируемым кодом.

    Вы можете просмотреть полную реализацию FakeStorageFolder в репозитории кода GitHub для этого руководства.

Дополнительные сведения см. в документации:

Создание простого модульного теста

  1. Переименуйте UnitTest1.cs в NoteTests.cs и обновите его.

    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);
            }
        }
    }
    

    В этом тесте показано, как проводить модульное тестирование NoteViewModel, используя FakeFileService. Тест создает новое NoteViewModel, проверяет его начальное состояние (дата недавняя, имя файла соответствует ожидаемому шаблону), записывает текст в заметку, выполняет команду сохранения и подтверждает, что текст сохраняется. Так как служба поддельных файлов используется вместо реальной реализации, тест выполняется быстро без фактического ввода-вывода файлов и может выполняться многократно без побочных эффектов.

Дополнительные сведения см. в документации:

Выполнение тестов

  1. Откройте окно обозревателя тестов в Visual Studio (обозреватель тестов>).
  2. Выберите "Выполнить все тесты ", чтобы выполнить модульный тест.
  3. Убедитесь, что тест проходит.

Теперь у вас есть тестируемая архитектура, в которой можно протестировать ViewModels и службы независимо от пользовательского интерфейса.

Сводка

В этой серии учебников вы узнали, как:

  • Создайте отдельный проект библиотеки классов (Bus project) для хранения ваших ViewModels и служб, что позволяет проводить модульное тестирование отдельно от слоя пользовательского интерфейса.
  • Реализуйте шаблон MVVM с помощью набора инструментов MVVM, используя атрибуты ObservableObject, [ObservableProperty], и [RelayCommand] для уменьшения стандартного кода.
  • Используйте генераторы источников для автоматического создания уведомлений об изменении свойств и реализаций команд.
  • Используется [NotifyCanExecuteChangedFor] для автоматического обновления доступности команд при изменении значений свойств.
  • Интегрируйте внедрение зависимостей с помощью Microsoft.Extensions.DependencyInjection для управления жизненным циклом ViewModels и служб.
  • IFileService Создайте интерфейс и реализацию для обработки операций с файлами с помощью тестового метода.
  • Настройте контейнер DI в App.xaml.cs и получите ViewModels от поставщика услуг на ваших страницах.
  • WeakReferenceMessenger Реализуйте возможность свободного взаимодействия между компонентами, позволяя страницам реагировать на события ViewModel без прямых ссылок.
  • Создайте классы сообщений, наследуемые от ValueChangedMessage<T>, для передачи данных между компонентами.
  • Создайте поддельные реализации зависимостей для тестирования без касания фактической файловой системы.
  • Написание модульных тестов с помощью MSTest для проверки поведения ViewModel независимо от слоя пользовательского интерфейса.

Эта архитектура обеспечивает надежную основу для создания обслуживаемых, тестируемых приложений WinUI с четким разделением проблем между пользовательским интерфейсом, бизнес-логикой и уровнями доступа к данным. Вы можете скачать или просмотреть код для этого руководства из репозитория GitHub.

Дальнейшие шаги

Теперь, когда вы узнаете, как реализовать MVVM с помощью набора средств MVVM и внедрения зависимостей, вы можете изучить более сложные разделы:

  • Расширенный обмен сообщениями. Изучение дополнительных шаблонов обмена сообщениями, включая сообщения запроса и ответа и маркеры сообщений для выборочной обработки сообщений.
  • Проверка: Добавьте проверку данных в ViewModels с помощью аннотаций данных и функций проверки в инструментарии MVVM.
  • Асинхронные команды: узнайте больше об асинхронном выполнении команд, поддержке отмены и отчетах о ходе выполнения.AsyncRelayCommand
  • Расширенное тестирование. Изучите более сложные сценарии тестирования, включая обработку сообщений, асинхронное выполнение команд и уведомления об изменении свойств.
  • Наблюдаемые коллекции: эффективное использование ObservableCollection<T> и исследуйте ObservableRangeCollection<T> для массовых операций.