Получение всех команд NET, часть 3. Первые тесты

Продолжаю часть 1 и часть 2. Профи, помидоры, уворачиваться - все помню, ничего не поменялось :)

Для начала поменяю имена AssemblyRepository, AssembliesViewModel на то, с чем они на самом деле работают: AssemblyInfoRepository и AssemblyInfosViewModel соответственно. Косметика, конечно - но правдивость имен того стоит )

Попробую написать тесты. Скорее всего, после этого массу кода придется переделывать/перемещать, но я ж учусь, гидрит вашу налево!

Общая идея тестов на данный момент для меня выглядит так: создаются какие-то "фейковые" данные, и проверяется логика получения этих данных. А также логика работы ViewModel, если это возможно.

Так, начну просто с создания тестового NUnit-проекта:
2025-01-06_19-30-08

Чтоб не сильно путатся в файловой системе, помещаю его в подкаталог \Tests. Если что - проект создается на NET6. .csproj проекта получился (для начала) таким:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
    <PackageReference Include="NUnit" Version="3.13.3" />
    <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
    <PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
    <PackageReference Include="coverlet.collector" Version="3.1.2" />
  </ItemGroup>

</Project>

Очень хочется туда прокинуть проверку AssemplyInfoRepository. Проблема в том, что класс этот болтается в сборке, связанной с nanoCAD. А пока что в тестах хочу от этой связки избавиться. Так что IAssemblyRepository отправляется в корзину, и AssemblyInfoRepository становится абстрактным классом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using KpblcCadInfrastructure.Abstractions.Entities;
using System;
using System.Collections.Generic;
using System.Linq;

namespace KpblcCadInfrastructure.Abstractions.Repositories
{
    public abstract class AssemblyInfoRepository
    {
        public abstract IEnumerable<AssemblyInfo> Get();

        public IEnumerable<AssemblyInfo> GetCustomAssemblies()
        {
            string programFiles = Environment.GetEnvironmentVariable("programfiles").ToUpper();
            return Get().Where(o =>
            {
                try
                {
                    return !o.Location.ToUpper().StartsWith(programFiles);
                }
                catch
                {
                    return true;
                }
            });
        }
    }
}

Метод Get придется в каждом наследнике переопределять, а вот GetCustomAssemblies уже переопределять нельзя. Логика прошита достаточно жестко.

Только ради того, чтоб избежать проблем с именованием, старый AssemblyInfoRepository переименовываю в CadAssemblyInfoRepository. Поправляю по ходу дела все обращения к старому интерфейсу и запускаю проверки. Основные изменения коснулись CadAssemblyInfoRepository (удивительно, правда?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Diagnostics;
using KpblcCadInfrastructure.Abstractions.Entities;
using KpblcCadInfrastructure.Abstractions.Repositories;

namespace KpblcCadInfrastructure.CAD.NET.Infrastructure
{
    internal class CadAssemblyInfoRepository : AssemblyInfoRepository
    {
        public override IEnumerable<AssemblyInfo> Get()
        {
            return AppDomain.CurrentDomain.GetAssemblies()
                .Select(o =>
                {
                    var version = FileVersionInfo.GetVersionInfo(o.Location).FileVersion;
                    if (Version.TryParse(version, out Version ver))
                    {
                        return new AssemblyInfo(o.Location, ver);
                    }

                    return new AssemblyInfo(o.Location, new Version(0, 0, 0, 0));
                });
        }
    }
}

Приводить остальной измененный код не буду, уж больно его много. С запуском nanoCAD, все по харду, все руками... Ну вроде бы все работает. Можно коммитить.

Из AssemblyInfo упоминание про Assembly пока не убираю, рановато. Но самое время прописать хотя бы один тест!

Для начала пробую прописать приватное поле, которое в себе будет хранить AssemblyInfoRepository (точнее, его тестовую реализацию):
2025-01-06_20-12-31

И вот тут становится немного неуютно. Может, стоит поменять или дополнить перегрузкой конструктор класса? Поскольку менять весь код на данный момент слеганца лениво, пишу альтернативный конструктор. Пока что это дешевле всего выглядит (может быть, потом придется и это менять...). Ну и комментарии - забыл я что-то про них:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;
using System.Reflection;

namespace KpblcCadInfrastructure.Abstractions.Entities
{
    /// <summary>
    /// Информация о загруженной в nanoCAD сборке
    /// </summary>
    public class AssemblyInfo
    {
        public AssemblyInfo(Assembly Assembly)
        {
            this.Assembly = Assembly;
            this.Location = Assembly.Location;
            this.Version = Assembly.GetName(false).Version;
        }

        public AssemblyInfo(string Location, Version Version)
        {
            this.Location = Location;
            this.Version = Version;
        }

        /// <summary>
        /// Расположение сборки
        /// </summary>
        public string Location { get; set; }
        /// <summary>
        /// Версия сборки
        /// </summary>
        public Version Version { get; set; }
        public Assembly Assembly { get; }
    }
}

Теперь можно и с тестами дальше играться:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using KpblcCadInfrastructure.Abstractions.Entities;
using KpblcCadInfrastructure.Abstractions.Repositories;
using System;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;

namespace KpblcCadInfrastructure.NUnitTests
{
    public class AssemblyInfoRepTest
    {
        [OneTimeSetUp]
        public void StartUp()
        {
            _assemblyInfoRepositoryTest = new AssemblyInfoRepositoryTest();
        }
        public class AssemblyInfoRepositoryTest : AssemblyInfoRepository
        {
            public AssemblyInfoRepositoryTest()
            {
                _assemblyInfos = new List<AssemblyInfo>()
                {
                    new AssemblyInfo(Path.Combine(_programFilesPath, "kpblc\\test1.dll"), new Version(0, 0, 1, 1)),
                    new AssemblyInfo(Path.Combine(_programFilesPath, "kpblc\\test2.dll"), new Version(0, 0, 1, 1)),
                    new AssemblyInfo(Path.Combine(Environment.GetEnvironmentVariable("appdata"), "kpblc\\test1.dll"),
                        new Version(0, 0, 1, 1)),
                };
            }

            public override IEnumerable<AssemblyInfo> Get()
            {
                return _assemblyInfos;
            }

            private List<AssemblyInfo> _assemblyInfos;
            private string _programFilesPath = Environment.GetEnvironmentVariable("programfiles");
        }

        private AssemblyInfoRepository _assemblyInfoRepositoryTest;
    }
}
Магические атрибуты
Чисто напоминание, для самого себя ;)

[OneTimeSetup] : метод выполняется всего один раз перед всеми тестами в классе
[SetUp] : метод выполняется каждый раз с нуля перед каждым тестом в классе
[Test] : собственно тест. Если описывается несколько тестов, можно определить порядок их выполнения: [Test, Order(<Порядковый номер теста>] [OneTimeTearDown] : метод выполняется после выполнения всех тестов, один раз. Не могу сказать, каково будет поведение, если тесты “развалились”
[TearDown] : метод выполняется после завершения каждого теста

Так, базовые вещи прописаны (как бы это криво ни выглядело), можно и нормальные тесты рисовать (комментарии написаны только для примерного понимания, чего должно быть в каждом тесте; дальше вряд ли буду такое писать):

1
2
3
4
5
6
7
8
9
10
11
12
13
[Test]
public void CheckAllAssemblyInfoRange()
{
    // Показать общее количество загружаемых "сборок". Должно быть 3 штуки
    Assert.AreEqual(_assemblyInfoRepositoryTest.Get().Count(), 3);
}

[Test]
public void CheckCustomAssemblyInfoRange()
{
    // Показать общее количество пользовательских загружаемых "сборок". Должна быть 1 штука.
    Assert.AreEqual(_assemblyInfoRepositoryTest.GetCustomAssemblies().Count(), 1);
}

Запускаю тесты, оба отрабатывают как и ожидалось:
2025-01-06_20-28-33

Чтобы показать панель тестов
В панели поиска (для русскоязычной версии VS) колотим “Обозреватель тестов” (без кавычек):2025-01-06_20-30-16

В общем и целом, пока что приложение выглядит даже слегка живым. Но - как минимум! - нет обработки команд (в режиме окна). Нет тестов на ViewModel'и.

Поскольку пока мозги в тестах, пропишу-ка тесты на ViewModel. И для начала беру ту, которая отвечает за AssemblyInfo (хотя бы потому, что для команд такой ViewModel пока что еще в природе не существует ;) ).

Помимо зависимости от KpblcCadInfrastructure.Core.NET в NUnit-проект придется указать целевую ОС. Если бы было выполнено разделение между окнами и ViewModel'ями по разным сборкам, этого можно было бы избежать.

Конечно, очень хочется скопипастить код с минимальными изменениями... Что ж, побуду сволочью и не стану препятствовать этому в общем-то преступному желанию ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using KpblcCadInfrastructure.Abstractions.Entities;
using KpblcCadInfrastructure.Abstractions.Repositories;
using System;
using System.Collections.Generic;
using System.IO;
using KpblcCadInfrastructure.Core.NET.ViewModels;
using NUnit.Framework;

namespace KpblcCadInfrastructure.NUnitTests
{
    public class AssemblyInfoVMTest
    {
        [OneTimeSetUp]
        public void StartUp()
        {
            _viewModel = new AssemblyInfosViewModel(new AssemblyInfoRepositoryTest());
        }

        [Test]
        public void CheckAllAssemblyInfoRange()
        {
            _viewModel.ShowCustomAssemblies = false;
            Assert.AreEqual(_viewModel.AssembliesList.Count, 3);
        }

        [Test]
        public void CheckCustomAssemblyInfoRange()
        {
            _viewModel.ShowCustomAssemblies = true;
            Assert.AreEqual(_viewModel.AssembliesList.Count, 1);
        }
        public class AssemblyInfoRepositoryTest : AssemblyInfoRepository
        {
            public AssemblyInfoRepositoryTest()
            {
                _assemblyInfos = new List<AssemblyInfo>()
                {
                    new AssemblyInfo(Path.Combine(_programFilesPath, "kpblc\\test1.dll"), new Version(0, 0, 1, 1)),
                    new AssemblyInfo(Path.Combine(_programFilesPath, "kpblc\\test2.dll"), new Version(0, 0, 1, 1)),
                    new AssemblyInfo(Path.Combine(Environment.GetEnvironmentVariable("appdata"), "kpblc\\test1.dll"),
                        new Version(0, 0, 1, 1)),
                };
            }

            public override IEnumerable<AssemblyInfo> Get()
            {
                return _assemblyInfos;
            }

            private List<AssemblyInfo> _assemblyInfos;
            private string _programFilesPath = Environment.GetEnvironmentVariable("programfiles");
        }
       
        private AssemblyInfosViewModel _viewModel;
    }
}

Запуск тестов...2025-01-06_20-44-21

И вот теперь - а на фига вообще было так заморачиваться. Допустим, в какой-то момент "пользовательскими" сборками будут считаться только те, которые грузятся из %AppData%. Или %RoamingAppData%. Не столь важно. Важно то, что будет изменен код в AssemblyInfoRepository. А запуск ранее работавших тестов моментально покажет, в каких местах можно получить неожиданно работающий (или вообще неработающий) код. Чисто по приколу поменяю фильтр в AssemblyInfoRepository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Collections.Generic;
using System.Linq;
using KpblcCadInfrastructure.Abstractions.Entities;

namespace KpblcCadInfrastructure.Abstractions.Repositories
{
    public abstract class AssemblyInfoRepository
    {
        public abstract IEnumerable<AssemblyInfo> Get();

        public IEnumerable<AssemblyInfo> GetCustomAssemblies()
        {
            string programFiles = @"D:\Files".ToUpper();
            return Get().Where(o =>
            {
                try
                {
                    return !o.Location.ToUpper().StartsWith(programFiles);
                }
                catch
                {
                    return true;
                }
            });
        }
    }
}

Т.е. теперь пользовательскими будут считаться сборки, загруженные из d:\files и его подкаталогов. Запустим тесты? А как же! 5 секунд на компиляцию сборок, и 0,2 секунды на проверки:2025-01-08_13-46-11

Как и ожидалось, часть тестов развалилась. Кликнув на любом тесте, помеченном как ошибочный, можно увидеть описание ошибки:
2025-01-08_13-47-33

Уж хоть что-то, и не надо вызывать nanoCAD, ждать его загрузки... А логику проверить все ж можно ;)

Код болтается на гитхабе: https://github.com/kpblc2000/KpblcCadInfrastructure, ветка master.

Размещено в .NET, MVVM, nanoCAD · Метки:



Поделитесь своим мнением


Я не робот.