.NET Разработчик
6.6K subscribers
446 photos
4 videos
14 files
2.15K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2028. #ЧтоНовенького #CSharp13
Альтернативный Доступ к Коллекциям

До C#13 ref-структуры не могли быть аргументами параметра-типа в обобщённых типах. Теперь в объявление обобщённого типа после ключевого слова where добавлено «анти-ограничение» allows ref struct, которое сообщает, что аргументом для параметра типа может быть ref-структура. Анти-ограничение оно, потому что по факту оно не ограничивает возможный набор параметров типа, а расширяет его. Это анти-ограничение налагает все доступные для ref-структур правила (вроде запрета размещения в куче) на все экземпляры параметра-типа. Смысл этого изменения также в том, чтобы можно было использовать типы Span в обобщённых-алгоритмах.

Например, таблицы поиска (lookup-таблицы) типа Dictionary<TKey,TValue> и HashSet<T> часто используются в качестве своего рода кэша. Однако, так как ключи таблиц обычно строкового типа, раньше приходилось выделять строку, чтобы обратиться в lookup-таблицу по ключу. Теперь упомянутое выше «анти-ограничение» добавляет новые возможности для этих типов коллекций.

Следующий код считает количество различных слов в тексте, используя так называемые альтернативные ключи словаря:
static Dictionary<string, int> 
CountWords(ReadOnlySpan<char> input)
{
Dictionary<string, int> counts =
new(StringComparer.OrdinalIgnoreCase);

Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>
lookup = counts.GetAlternateLookup<string, int, ReadOnlySpan<char>>();

foreach (Range r in Regex.EnumerateSplits(input, @"\b\W+\b"))
{
ReadOnlySpan<char> word = input[r];
lookup[word] = lookup
.TryGetValue(word, out int count) ? count + 1 : 1;
}

return counts;
}

Сначала мы создаём lookup-словарь counts и указываем, что нам не важен регистр ключей.
Затем, чтобы искать в словаре по spanам, а не по строкам, мы создаём структуру lookup для альтернативного доступа к элементам. Здесь она вот такого длинного типа Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>. В примере указан тип полностью для демонстрации. В реальном коде это можно заменить на var. Заметьте, что тут используется обобщённый тип AlternateLookup с параметром типа виде ref-структуры ReadOnlySpan<char>.
Далее мы разбиваем текст по «не-словам», используя Regex.EnumerateSplits. В результате получаем перечислитель диапазонов результатов. С его помощью мы делаем срезы исходного текста, которые представляют собой отдельные слова. Это переменная word типа ReadOnlySpan<char>. И вот здесь, чтобы не создавать строк для доступа в словарь counts по строковому ключу, нам и потребуется структура lookup, которая обеспечит доступ к словарю по альтернативному ключу типа ReadOnlySpan<char>. В данном случае мы увеличиваем счётчик для текущего слова.

Теперь мы можем взять текст и посчитать в нём слова:
var text = "<какой-то длинный текст>";
foreach (var word in CountWords(text))
Console.WriteLine(word);


Источник: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13
👍20
День 2029. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Начало
Все мы мечтаем работать с передовыми технологиями, современными системами, чистым и понятным кодом. Однако, если вы приходите в компанию, где кодовой базе исполнилось 10+ лет, вы неизбежно столкнётесь с тем, что взорвёт ваш мозг (не спрашивайте, откуда я знаю). Но эта история поразила даже меня. Текст довольно длинный, будет разбит на несколько постов, но я решил его не сокращать, чтобы не лишать вас удовольствия.

Когда я начал программировать в детстве, я не знал, что людям платят за программирование. Даже когда я окончил среднюю школу, я предполагал, что мир «профессиональной разработки» выглядит совсем иначе, чем код, который я писал в свободное время. Когда мне повезло попасть на свою первую работу в сфере ПО, я быстро понял, насколько я был прав и неправ. Моя первая работа была испытанием огнём, и по сей день эта кодовая база остается худшей и лучшей кодовой базой, над которой мне когда-либо доводилось работать. Хотя кодовая база навсегда останется в этой конкретной компании, я хочу поделиться с вами некоторыми из её самых забавных и страшных историй.

База данных живёт вечно
В большой устаревшей системе БД — больше, чем место для хранения данных, она — источник культуры. БД устанавливает ограничения на то, как работает система в целом. Это точка, где встречается весь код. БД — это место водопоя, и в нашем случае он был довольно загрязнён.

Вы знаете, что у SQL Server есть ограничение на количество столбцов в таблице? Я тоже не знал. В то время их было 1024, сегодня, похоже, 4096. Само собой, большинству людей это знать не нужно. Мы знали. Причина в том, что в Merchants (наша таблица для хранения информации о клиентах) давно закончились столбцы. Решением стала Merchants2. Таблица с (если правильно помню) 500+ столбцами.

Merchants (и её лучшая подруга Merchants2) были источником жизненной силы системы. Всё крутилось вокруг Merchants так или иначе. Но Merchants не была единственной (или двойной) таблицей. Было много правильно нормализованных таблиц, все с внешними ключами к Merchants. Но одна из них всегда будет занимать особое место в моем сердце, SequenceKey.

SequenceKey
------------
SequenceKey
------------
1251238
------------

Для простоты понимания я воссоздал всю таблицу SequenceKey выше. Да. Вы правильно прочитали, это вся таблица. Таблица с одним ключом и одним значением. Можно сказать, что SequenceKey - идеальная таблица. Что может быть проще?

Но вы можете спросить себя, какое возможное применение может иметь таблица с одним столбцом и строкой? Генерация идентификаторов. Как мне сказали в то время, когда-то давно SQL Server не поддерживал автоинкрементные идентификаторы. Это был принятое, правильное решение. Мои попытки выяснить, правда ли это, были безрезультатными. Но на практике её роль была гораздо большей.

SequenceKey была связующим звеном. В каждой хранимой процедуре, которая создавала новые сущности, вы сначала брали ключ из SequenceKey, увеличивали его. А затем вставляли его в качестве идентификатора в N разных таблиц. Теперь у вас было неявное соединение между всеми этими таблицами сущностей. Если вы видели идентификатор в системе, то была большая вероятность, что связанные таблицы будут иметь строку с точно таким же идентификатором. Честно говоря, довольно умно.

Календарь
База данных может существовать вечно, но наша система входа была ограничена календарём. Я не имею в виду настоящий календарь. Я имею в виду таблицу базы данных под названием calendar. Что она содержала? Заполненный вручную календарь. Когда я спросил нашего местного гуру (которого звали Мунч), он сообщил мне, что, когда календарь заканчивается, мы не можем входить в систему. Это произошло несколько лет назад. Поэтому они заставили стажёра заполнить ещё 5 лет, чтобы убедиться, что этого не произойдет в ближайшее время. Какая система использовала этот календарь? Никто не знал.

Продолжение следует…

Источник:
https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍29
День 2030. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Продолжение

Начало

Сотрудники
Каждое утро в 7:15 таблица сотрудников удалялась. Все данные полностью исчезали. Затем в таблицу загружался csv-файл из adp. В это время мы не могли войти в систему. Иногда этот процесс давал сбой. Но это был не конец процесса. Данные нужно было реплицировать в штаб-квартиру. Поэтому электронное письмо отправлялось человеку, который каждый день нажимал кнопку, чтобы скопировать данные.

Заменяющая база данных
Вы могли бы подумать: неужели никому не пришло в голову почистить эту БД? Сделать её более удобной для работы? Да. Была копия БД. Данные в этой копии отставали примерно на 10 минут. Синхронизация работала только в одну сторону, но эта база данных была нормализована. Насколько нормализована? Чтобы перейти от продавца к его номеру телефона, требовалось соединить 7 таблиц.

Цифры продаж
У каждого продавца была квота, которую ему нужно было выполнить в месяц, называемая «победой». Таблицы, в которых хранились эти данные (не финансовые данные, а способ учёта продаж), были невероятно сложными. Каждый день назначенное задание выясняло, какие строки были добавлены и обновлены, и синхронизировало их с какой-то системой в штаб-квартире. Это не было проблемой, пока один продавец не понял, что можно попросить вручную изменить эти записи.

Он уже получил свою «победу» и заключил ещё одну большую сделку в том же месяце. Он хотел, чтобы её перенесли на следующий месяц. Это было поручено стажёру. Слухи разошлись, и в течение следующих 3 лет запросы росли в геометрической прогрессии. В какой-то момент у нас было 3 стажёра, чья постоянная работа заключалась в написании этих SQL-запросов. Написание приложения для этого посчитали слишком сложным. Однако перед уходом я помог этим стажерам создать его. Хотя понятия не имею, использовали ли они его.

Кодовая база
Но что такое БД без кодовой базы. Какая же это была великолепная кодовая база. Когда я пришёл, всё было в Team Foundation Server. Если вы не в курсе, это централизованная система управления исходным кодом от Microsoft. Основная кодовая база, с которой я работал, была наполовину VB, наполовину C#. Она работала на IIS и использовала состояние сеанса для всего. Что это означало на практике? Если вы переходили на страницу по пути A или пути B, вы видели на ней совсем разные вещи.

Но описать эту кодовую базу как просто половина VB, половина C# - это не сказать ничего. Наверное, все существующие тогда JS-фреймворки были добавлены в этот репозиторий. Как правило, с некоторыми пользовательскими изменениями, которые автор посчитал необходимыми. В частности, knockout, backbone и marionette. Ну и конечно, было немного jquery и плагинов jquery.

Но эта кодовая база не стояла особняком. Рядом с ней было около дюжины SOAP-сервисов и несколько нативных приложений Windows. Самым примечательным был менеджер по доставке. В документации сказано, что всё приложение было создано за выходные разработчиком-одиночкой. Назовем его Гилфойлом. Гилфойл был, по общему мнению, невероятно быстрым программистом. Я никогда не встречался с ним, но я чувствовал, что знаю его не только по его коду в репозиториях, но и по всему коду, оставшемуся на его жестких дисках.

Жесткие диски Гилфойла
Манч (да, это было его настоящее имя) держал жёсткий диск Гилфойла в RAID-массиве на своём столе много лет после того, как Гилфойл ушёл из компании. Почему? Потому что Гилфойл был известен тем, что не коммитил код. Более того, он создавал случайное одноразовое приложение Windows для одного пользователя. Поэтому не было редкостью, когда пользователь приходил к нам с отчётом об ошибке для приложения, которое существовало только на жёстком диске Гилфойла, а мы понятия не имели о его существовании.

Окончание следует…

Источник:
https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍23
День 2031. #Оффтоп
«У Нас Кончились Столбцы» — Лучшая Худшая Кодовая База. Окончание

Начало
Продолжение

Баг в доставке
Одна особенно неприятная ошибка всплывала раз в несколько месяцев. После того, как мы отправляли товары, в очереди доставки застревали заказы одновременно и как отправленные, и как не отправленные. Я пробовал ряд обходных путей, чтобы исправить это.

Попутно я узнал, как мыслит Гилфойл. Приложение доставки извлекало всю БД, фильтровало по дате и сохраняло все заказы после даты запуска приложения. Оно полагалось на SOAP-сервис. Нет, сервис был чистой функцией. Все побочные эффекты были на клиенте. Там я обнаружил иерархию в 120 классов с различными методами и 10-уровневое наследование. Единственная проблема… ВСЕ МЕТОДЫ БЫЛИ ПУСТЫМИ.

В конце концов я узнал, что это было необходимо для создания структуры, к которой он мог бы затем применить рефлексию. Рефлексия позволила бы ему создать строку с разделителями (структура которой управлялась базой данных), которую он мог бы отправлять через сокет. Оказывается, всё это в итоге отправлялось в Kewill (сервис, который общался с перевозчиками). Почему происходила ошибка? Kewill повторно использовал 9-значные числа каждый месяц, а кто-то отключил задание cron, которое удаляло старые заказы.

Прекрасный беспорядок
Ещё много можно рассказать об этой кодовой базе. Например, о команде супер-старших разработчиков, которые переписывали всё это, не делая коммитов кода в течение 5 лет. Или о консультантах Red Hat, создавших одну БД, чтобы управлять всеми. Или об улучшении Джастином страницы поиска продавцов. Она была точкой входа во всё приложение. Каждый представитель службы поддержки клиентов звонил продавцу и вводил либо его ID, либо название, чтобы найти его информацию. Это приводило на огромную страницу с любой информацией, которая вам могла бы понадобиться, и всеми ссылками, которые вы могли бы захотеть посетить. Но она была чертовски медленной.

Однажды Джастину надоело слушать о том, как медленно работает страница, и он исправил её. Каждое поле стало конечной точкой. При загрузке страницы каждый компонент запрашивался отдельно. Когда загружался один, запрашивался следующий. Но время загрузки страницы сократилось с нескольких минут до доли секунды.

Два способа разделения
Почему Джастин смог это сделать? Потому что у проекта не было генерального плана, формата API, документации и т.п. Система была полным и абсолютным хаосом. Никто не мог его исправить, поэтому никто и не пытался. Что мы делали?

Это монолитное приложение из-за чистой необходимости превратилось в экосистему из хороших маленьких приложений по краям. Каждый кому поручали улучшить какую-то часть приложения, неизбежно отказывался от распутывания паутины и создавал своё уютное микро-приложение.

Работать так было на удивление приятно. Исчезли проблемы дублирования кода, согласованности и расширяемости. Код как можно меньше затрагивал окружающую его область и был легко заменяемым. Он был слабо связан, просто потому что связывать его было сложнее.

Итого
За всю мою карьеру с тех пор мне не доводилось работать в такой замечательно уродливой кодовой базе. Возможно, тот код был давно заброшен старшими разработчиками, а дописывали его стажеры и джуны. Или, возможно, это было потому, что не было никакой прослойки между разработчиками и пользователями, никакого сбора требований, никаких ТЗ. Просто вы стоите за столом представителя службы поддержки клиентов и спрашиваете его, как улучшить его жизнь.

Я скучаю по этой простой прямой связи между проблемой и кодом. Возможно, это ностальгия. Но каждый раз, когда я сталкиваюсь с очередным «корпоративным шаблоном проектирования», мой разум возвращается к той прекрасной, но ужасной кодовой базе.

Источник: https://jimmyhmiller.github.io/ugliest-beautiful-codebase
Автор оригинала: Jimmy Miller
👍18
День 2032. #ЧтоНовенького #CSharp13
Флаги функций с поддержкой тримминга
Два новых атрибута позволяют определять флаги функций, которые можно использовать для включения/отключения областей функциональности, а также для автоматического включения/отключения функций при тримминге или AOT-компиляции.

FeatureSwitchDefinitionAttribute
Атрибут FeatureSwitchDefinition может быть использован, чтобы флаг функции после сборки определялся как константа.
public class Feature
{
[FeatureSwitchDefinition("Feature.IsSupported")]
internal static bool IsSupported =>
AppContext.TryGetSwitch("Feature.IsSupported", out bool enabled)
? enabled
: true;

internal static void Implementation() => …;
}

Здесь мы извлекаем значение IsSupported из конфигурации сборки. В данном случае оно определено в файле проекта (.csproj):
<ItemGroup>
<RuntimeHostConfigurationOption Include="Feature.IsSupported"
Value="false" Trim="true" />
</ItemGroup>

Мы добавляем RuntimeHostConfigurationOption с именем нужной функции (соответствующим параметру атрибута) и булевым значением, включена она или нет.

При сборке с включённым триммингом недосягаемый код, находящийся под флагом, удаляется. Когда приложение собирается с такой настройкой в файле проекта, Feature.IsSupported расценивается как константа false и Feature.Implementation удаляется из сборки.

Предложение описывает использование атрибута FeatureSwitchDefinition в библиотеках и для тримминга. Возможно ли его использование как флага функции в бизнес-логике пока не понятно, но, думаю, можно попробовать.

FeatureGuardAttribute
Атрибут FeatureGuard можно использовать для свойства флага функции в качестве защитной конструкции для кода, аннотированного атрибутами RequiresUnreferencedCode, RequiresAssemblyFiles или RequiresDynamicCode:
public class Feature
{
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
internal static bool IsSupported =>
RuntimeFeature.IsDynamicCodeSupported;

[RequiresDynamicCode("Feature requires dynamic code support.")]
internal static void DynamicImplementation()
=> …; // Использует dynamic
}

Здесь RuntimeFeature определяет API, которые доступны в среде выполнения. В данном случае свойство IsDynamicCodeSupported показывает, поддерживается ли в среде выполнения динамический код.
Поскольку свойство IsSupported возвращает false, когда динамический код не поддерживается, его можно использовать в качестве защиты для методов, которым требуется динамический код во время выполнения.
if (Feature.IsSupported)
Feature.DynamicImplementation();

Атрибут FeatureGuard сообщает об этом анализатору тримминга и инструментам тримминга. Поэтому, если мы в файле проекта укажем
<PublishAot>true</PublishAot>

то Feature.DynamicImplementation будет удалён при AOT-публикации.

Источник: https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview4/runtime.md
6👍7
День 2033. #BlazorBasics
Основы Blazor. (Ре-)Рендеринг Компонентов. Начало

Рендеринг компонентов — основная механика приложений Blazor, превращающая компоненты C# в HTML и CSS. Методы жизненного цикла позволяют нам выполнять пользовательский код синхронно или асинхронно.

Приложения Blazor состоят из дерева компонентов, которые создают интерактивный UI. Рендеринг начинается на вершине дерева и переходит к дочерним элементам каждого компонента. Так родительский компонент решает, какие дочерние компоненты создавать.

Методы жизненного цикла компонентов Blazor
Процесс рендеринга не может быть прерван; в противном случае UI бы «завис». Поэтому компоненты Blazor используют методы жизненного цикла, чтобы позволить выполнять пользовательский код в течение их жизненного цикла.

Компонент Blazor впервые отрисовывается после создания его экземпляра при включении в качестве дочернего компонента см. диаграмму ниже.

Метод SetParametersAsync
Это первый метод обратного вызова в жизненном цикле компонента. Он получает параметры, предоставленные родительским компонентом. Переопределяя SetParametersAsync, мы можем использовать пользовательский код для изменения значений, предоставляемых родительским компонентом. По умолчанию параметры будут установлены в свойства, декорированные атрибутом Parameter. Если мы хотим использовать параметры компонента по-другому, мы можем использовать пользовательский код и переопределить метод SetParametersAsync, чтобы определить желаемое поведение.
@code {
// параметры компонента

public override async Task
SetParametersAsync(ParameterView pv)
{
// пользовательский код

await base.SetParametersAsync(pv);
}
}


Методы OnInitialized и OnInitializedAsync
После того, как компонент Blazor завершает выполнение метода SetParametersAsync и, следовательно, все параметры из родительского компонента установлены, вызываются методы жизненного цикла OnInitialized и OnInitializedAsync. Это идеальное место для загрузки данных из БД и назначения их свойствам, используемым в шаблоне компонента.
@code {
private string? message;

[Parameter]
public string Name { get; set; }

protected override void OnInitialized()
{
message = $"Привет, {Name}!";
}
}

Как показано в этом примере, мы также можем использовать значения, параметров компонента в синхронном методе жизненного цикла OnInitialized.

Окончание следует…

Источник:
https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
1👍8
Родительский компонент запускает создание дочернего компонента. Метод жизненного цикла SetParametersAsync и методы OnInitialized(Async) запускаются только для первой отрисовки. Метод OnParametersSet(Async) запускается, когда родительский компонент предоставляет другие значения параметров своему дочернему компоненту. Метод StateHasChanged может использоваться разработчиком для запуска повторной отрисовки компонента вручную.
👍8
День 2034. #BlazorBasics
Основы Blazor. (Ре-)Рендеринг Компонентов. Окончание

Начало

Методы OnParametersSet и OnParametersSetAsync
SetParametersAsync вызывается только при первом рендеринге компонента Blazor. А методы OnParametersSet и OnParametersSetAsync запускаются всякий раз, когда родительский компонент повторно отрисовывается, предоставляя другие значения для параметров дочернего компонента.

Переопределение этих методов предоставляет нам доступ к обновлению значений в компоненте на основе нового набора параметров.
@code {
private string? message;

[Parameter]
public string Name { get; set; }

protected override void OnParametersSet()
{
message = $"Привет, {Name}!";
}
}

После завершения методов OnParametersSet или OnParametersSetAsync запускается автоматическая повторная отрисовка компонента.

Метод StateHasChanged
Мы можем вызвать метод StateHasChanged класса ComponentBase, чтобы сообщить компоненту Blazor об изменении состояния компонента. Он запустит повторную отрисовку компонента, которая вызовет все применимые методы жизненного цикла в процессе отрисовки.

Не следует вызывать метод StateHasChanged в следующих сценариях:
- Обработка событий. Неважно, являются ли события синхронными или асинхронными. Класс ComponentBase запускает отрисовку для большинства обработчиков событий.
- Реализация методов жизненного цикла, таких как OnParametersSetAsync или OnInitialized (синхронных или асинхронных). Класс ComponentBase запускает отрисовку для большинства событий жизненного цикла.

Как правило, обычно вам не нужно вызывать метод StateHasChanged вручную. Однако, если вы ожидаете, что UI покажет обновленное значение, а этого не происходит, велики шансы, что вызов StateHasChanged в правильном месте исправит вашу проблему.

Подробнее о крайних случаях, когда вызов StateHasChanged требуется для получения желаемого поведения, читайте в документации по рендерингу компонентов.

Асинхронные и синхронные методы жизненного цикла
Для синхронного кода родительские компоненты всегда визуализируются перед дочерними компонентами. Асинхронный код более сложныq. При использовании асинхронных методов жизненного цикла порядок завершения родительского и дочернего компонента не является детерминированным, поскольку он зависит от кода инициализации. Однако Blazor обеспечивает доступность и корректное обновление параметров компонента при визуализации дочернего компонента.

Используйте синхронные методы жизненного цикла, когда не нужно ожидать асинхронных операций. Когда же нужно вызвать БД или использовать другие асинхронные операции, используйте асинхронные методы.

Итого
Компоненты Blazor используют методы жизненного цикла для предотвращения блокировки пользовательского интерфейса и позволяют разработчикам выполнять пользовательский код:
- SetParametersAsync позволяет выполнять пользовательский код, обрабатывающий параметры, предоставленные родительским компонентом, только для первого рендеринга компонента.
- OnInitialized и OnInitializedAsync часто используются для загрузки данных из БД или для инициализации свойств, отображаемых как часть шаблона компонента.
- OnParametersSet и OnParametersSetAsync вызываются, когда родительский компонент повторно отрисовывается, предоставляя другой параметр дочернему компоненту.
- Если UI не отображает ожидаемое значение на экране, вы можете вызвать StateHasChanged в соответствующем месте кода.

Источник: https://www.telerik.com/blogs/blazor-basics-blazor-component-re-rendering
👍8
День 2035. #ЧтоНовенького
Прекращается Поддержка BinaryFormatter
Начиная с .NET 9, BinaryFormatter больше не будет включён в среду выполнения. Его API все ещё присутствует, но реализация всегда будет выдавать исключение PlatformNotSupportedException, независимо от типа проекта. Установка существующего сейчас флага обратной совместимости больше недостаточна для использования BinaryFormatter.

Есть два варианта решения:
1. Настоятельно рекомендуется отказаться от BinaryFormatter из-за связанных ним рисков безопасности и выбрать другой сериализатор.

2. Продолжить использовать BinaryFormatter. Для этого нужно добавить неподдерживаемый пакет совместимости System.Runtime.Serialization.Formatters, который заменяет реализацию, вызывающую исключение в среде выполнения, но подвержен описанным ниже уязвимостям.

Проблема с BinaryFormatter
Дело в том, что любой десериализатор, позволяющий иметь во входных данных информацию о создаваемых объектах, является проблемой безопасности. BinaryFormatter, включённый в первый выпуск .NET Framework в 2002 году, является таким десериализатором. Из-за известных рисков эта функциональность была исключена из .NET Core 1.0. Но в отсутствие чёткого пути миграции на что-то более безопасное, спрос среди клиентов привёл к тому, что BinaryFormatter был обратно включен в .NET Core 2.0. С тех пор команда .NET шла по пути к его удалению, постепенно отключая его по умолчанию в разных типах проектов, но позволяя потребителям включать его с помощью флагов, если было необходимо для обратной совместимости.

Миграция
Выбор другого сериализатора возможен, только если вы контролируете как производителя, так и потребителя сериализованных данных. Если это ваш случай, то в зависимости от ваших потребностей рекомендуются следующие сериализаторы:
- System.Text.Json (JSON)
- DataContractSerializer (XML)
- MessagePack (binary)
- protobuf-net (binary)

Если вы не контролируете производителя, либо десериализуете данные, которые были сериализованы ранее и сохранены (и нет возможности их преобразовать) вы можете использовать новый API для чтения данных BinaryFormatter без выполнения универсальной и уязвимой десериализации с помощью NuGet-пакета System.Formats.Nrbf.

Источник: https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-migration-guide/
👍9
День 2036. #УрокиРазработки
Уроки 50 Лет Разработки ПО

Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Начало
«Приложение, которое я хотел бы получить, не должно иметь ошибок, не должно использовать много памяти или замедлять компьютер, должно быть полностью безопасным, мгновенно реагировать на каждую мою команду и быть абсолютно надёжным, работать на любом устройстве, мгновенно загружаться и быть бесплатным.»

Похоже на сказочное приложение. Желания клиента неразумны. Невозможно объединить в одном приложении все лучшие достижения и передовые возможности. Различные качественные характеристики неизбежно вступают в конфликт друг с другом: улучшение одной часто приводит к ухудшению другой. Поэтому важной частью анализа требований является определение наиболее важных характеристик, чтобы разработчики могли учесть это.

Нефункциональные требования не реализуются напрямую. Они служат лишь источником производной функциональности, архитектурных решений или подходов к проектированию и реализации. Некоторые нефункциональные требования ограничивают выбор вариантов, доступных дизайнеру или разработчику. Например, из-за требования функциональной совместимости, возможно, придётся использовать только стандартные API.

Ниже перечислены атрибуты качества, которые каждая команда разработчиков ПО должна учитывать, изучая значение понятия качества для их продукта:
1. Доступность
Смогу ли я использовать систему, когда и где мне нужно?
2. Соответствие стандартам
Соответствует ли система всем применимым стандартам в отношении функциональности, безопасности, сертификации и т.п.?
3. Эффективность
Экономно ли система использует ресурсы компьютера?
4. Возможность установки
Смогу ли я легко установить, удалить и переустановить систему и её обновления?
5. Целостность
Имеет ли система защиту от неточных данных, от их повреждения и потери?
6. Совместимость
Достаточно ли хорошо система взаимодействует с окружением для обмена данными и услугами?
7. Сопровождаемость
Смогут ли разработчики легко менять, исправлять и улучшать систему?
8. Производительность
Достаточно ли быстро система реагирует на действия пользователя и внешние события?
9. Переносимость
Можно ли легко перенести систему на другие платформы?
10. Надёжность
Отсутствуют ли сбои в работе системы?
11. Повторное использование
Смогут ли разработчики повторно использовать части системы в других продуктах?
12. Устойчивость
Реагирует ли система на ошибочные входные данные и непредвиденные условия работы?
13. Безопасность
Защищает ли система пользователей от вреда и имущество от повреждения?
14. Масштабируемость
Может ли система легко расширяться для обслуживания большего количества пользователей, данных или транзакций?
15. Защищённость
Защищена ли система от атак вредоносных программ, злоумышленников, неавторизованных пользователей и кражи данных?
16. Удобство использования
Смогут ли пользователи быстро научиться работать с системой и эффективно выполнять свои задачи?
17. Проверяемость
Смогут ли тестировщики определить, насколько правильно реализовано ПО?

По аналогии с функциональностью разработчики должны сбалансировать ценность некоего качества и стоимость его достижения. Например, всем хотелось бы, чтобы ПО всегда было доступно для использования, но достижение этого может стоить очень дорого. Если есть требование нулевого времени простоя, вариант – добавить резервную систему. При обновлении сначала обновляется резервная система, тестируется и переключается в рабочий режим, а бывшая основная система обновляется после. Иметь две независимые системы — дорого, но это дешевле, чем останавливать производство, если система выходит из строя.

Окончание следует…

Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍10
День 2037. #УрокиРазработки
Уроки 50 Лет Разработки ПО

Урок 20. Невозможно оптимизировать все желаемые атрибуты качества. Окончание

Начало

Определение атрибутов качества
Разработчики должны знать, какие атрибуты качества наиболее важны. Недостаточно просто сказать: «Система должна быть надёжной» или «Система должна быть удобной для пользователя». На этапе выявления требований бизнес-аналитик должен выяснить, что именно заинтересованные стороны имеют в виду под надёжностью или удобством. По каким характеристикам можно судить об этом? Какие примеры ненадёжности или неудобства можно привести?

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

Проектирование для качества
Разработчики могут оптимизировать свой подход к решению для практически любого параметра качества в зависимости от степени его важности. На этапе исследования требований нужно определить наиболее важные атрибуты, чтобы направлять усилия разработчиков туда, где они наиболее важны для бизнеса, т.е. необходимо расставлять приоритеты для нефункциональных требований по аналогии с функциональными.

Улучшение одних атрибутов качества может ухудшить другие. Вот несколько примеров конфликтов:
- Многофакторная аутентификация более надёжна, но снижает удобство использования из-за дополнительных шагов и, возможно, задействованных устройств.
- Продукт, предназначенный для повторного использования, может оказаться менее эффективным, чем если бы код функциональности был оптимизирован для одного приложения.
- Оптимизация производительности может ухудшить переносимость системы.
- Внедрение решений, упрощающих обучение новых пользователей, может сделать систему неудобной для экспертов.

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

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

Архитектура и атрибуты качества
Поскольку необходимость компромиссов — частое явление, архитекторы должны знать, какие атрибуты наиболее важны, иначе не смогут принять решения, ведущие к желаемым результатам.

Возвращение на поздних стадиях разработки или после выпуска и переделка архитектуры системы в рамках исправления недостатков качества — дорогое удовольствие. Поэтапное построение систем при отсутствии предварительного знания наиболее важных целей в области качества может привести к проблемам, которые трудно исправить. Как это часто бывает с программными проектами, потратив чуть больше времени на то, чтобы лучше понять цели в области качества, можно прийти к менее дорогим и более надёжным решениям.

Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍4
День 2038. #ЗаметкиНаПолях
Удалять или не Удалять
Мягкое удаление — это метод, используемый для того, чтобы пометить запись как удалённую, не удаляя её на самом деле. Во многих случаях вы будете использовать что-то вроде столбца IsDeleted, который устанавливается в true, когда запись удалена. Таким образом, запись на самом деле не будет удалена из БД, но будет помечена как удалённая.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsDeleted { get; set; }
}

Чаще всего «удалённые» записи не нужны, поэтому их отфильтровывают:
public async Task<IEnumerable<User>> GetUsers()
{
return await _context.Users
.Where(u => !u.IsDeleted).ToArrayAsync();
}

Вместо того, чтоб добавлять Where(…) к каждому запросу, можно использовать глобальный фильтр в Entity Framework:
protected override void 
OnModelCreating(ModelBuilder mb)
{
mb.Entity<User>().HasQueryFilter(u => !u.IsDeleted);
}

И в каждом запросе из таблицы Users удалённые записи будут отфильтрованы. Конечно, вы можете изменить это, если хотите:
public async Task<IEnumerable<User>> GetAllUsers()
{
return await _context.Users
.IgnoreQueryFilters().ToArrayAsync();
}


Подводные камни
1. Нужны ли удалённые записи?
Во многих случаях нет. А если они не нужны, зачем хранить их в базе? Они просто занимают место и замедляют запросы. То, что не нужно, можно удалить. При составлении отчётов может потребоваться просмотр удалённых записей, но как часто создаются отчёты?

2. Глобальные фильтры запросов
Это и хорошо, и плохо. Когда вы смотрите на запрос, он не говорит вам всей правды! Вы должны знать, что к запросу применяется глобальный фильтр. Это может сбивать с толку и приводить к ошибкам.

3. Производительность
Если в таблице много записей, запрос должен будет отфильтровывать удалённые каждый раз. Можно добавить индекс, но это повлияет на производительность вставки и обновления.

Альтернативы
1. Архивная таблица или схема
Так вы сможете сохранить удалённые записи, но они не будут находиться в той же таблице, что и активные. Таким образом можно поддерживать чистоту и быстроту таблицы активных записей. Также можно создать представление, которое объединит активные и архивные записи, если нужно увидеть их вместе в определённых обстоятельствах.

2. Архивная база данных
Так вы сможете поддерживать чистоту и быстроту БД активных записей. Это сложнее в настройке, но может быть хорошим решением, если у вас много удалённых записей. А если данных много, это может сэкономить деньги, если вы используете управляемые экземпляры в Azure или AWS. Например, Azure предлагает уровень архива, где даже терабайты данных стоят намного меньше, чем в активной БД (если вы не читаете оттуда много данных и вам не нужен «живой» архив).

Источник: https://steven-giesel.com/blogPost/a807373c-dcc6-42f9-995f-e69dcea1cd47/to-soft-delete-or-not-to-soft-delete
👍17
День 2039. #Оффтоп
Баг в Течение Часа в Год
Было 8 ноября 2021, я работал сортировщиком ошибок в команде Google Docs. День начался как любой другой. Я заварил кофе и начал просматривать отчёты об ошибках за предыдущий день. Кое-что привлекло моё внимание.

Было необычно много сообщений об ошибках, и все они говорили об одном и том же. Пользователь создал ответ или новый комментарий в документе, но его метка времени на странице говорила, что он был создан «завтра». Быстро выявилась закономерность. Все ошибки появлялись:
- У пользователей в тихоокеанском часовом поясе (PT)
- Между 23:00 и 23:59 (PT) 7 ноября
Я бы посчитал это совпадением, но перевод на зимнее время случился в 2:00 (PT) 7 ноября!

Расследование
Баг меня заинтересовал, поэтому я решил заняться его устранением самостоятельно. Не потребовалось много времени, чтобы прийти к выводу, что ошибка, должно быть, в логике форматирования относительной даты Closure Library, которую использовал Docs. Код начинался с попытки вычислить количество дней между текущим временем и временем ввода через:
1. Получение текущего времени (new Date()).
2. Сброс часов, минут, секунд и миллисекунд объекта до нуля, чтобы получить время начала текущего дня.
3. Вычисление количества миллисекунд между временем начала текущего дня и временем ввода, деления его на количество миллисекунд в дне и округления в меньшую сторону.

Я некоторое время смотрел на код, и тут меня осенило! Введенное время между 23:00 и 23:59 (PT) 7 ноября было в поясе тихоокеанского стандартного времени (PST) после окончания летнего времени, но начало текущего дня (00:00) было в поясе тихоокеанского летнего времени (PDT) до окончания летнего времени. Поэтому в тот день между 00:00 и 23:00 не было 23 часов. Два времени были в двух разных часовых поясах с разницей в час, поэтому между двумя временами было 24 часа (целый день)! А что делал код, когда время ввода на день позже начала текущего дня? Он форматировал время как «завтра»…

Исправление
К счастью, класс Date в JS предоставляет удобный метод getTimezoneOffset(), который возвращает количество минут между часовым поясом объекта Date и часовым поясом UTC. Я использовал его для вычисления разницы. По сути, это удаляло любые различия, возникавшие только из-за изменения летнего/зимнего времени между текущим временем и временем начала текущего дня. Так что теперь количество часов между 00:00 и 23:00 в этот день вычислялось как 23!

Бонусный баг
Эта ошибка фактически распространялась и на время начала летнего времени. В 2021 году летнее время началось в 2:00 (по тихоокеанскому времени) 14 марта. Что же произойдёт, если оставить комментарий в 00:00 15 марта, когда текущее время было 23:00 14 марта?

Можно было бы ожидать, что между 00:00 и 23:00 вечера 14 марта будет 23 часа, но их всего 22. Из-за того, что летнее время начинается в 2:00, что на самом деле 3:00, между временем начала дня 14 и 15 марта всего 23 часа!

И что делает код для этого количества часов? Он вычисляет количество дней между двумя временами как ноль из-за округления в меньшую сторону и форматирует время как «сегодня»…
На самом деле этот баг не проявлялся в Google Docs, поскольку нельзя написать комментарий в будущем, и посмотреть его в прошлом.

Почему 1 час в год только по тихоокеанскому времени?
На самом деле эта ошибка теоретически могла произойти в любом часовом поясе. Возможно, все отчёты об ошибках в тот день были случайно получены от пользователей в тихоокеанском часовом поясе. Или, возможно, есть какой-то другой фактор, о котором я не подумал.

Источник: https://tomeraberba.ch/the-1-hour-per-year-bug
Автор оригинала: Tomer Aberbach
👍9👎2
День 2040. #УрокиРазработки
Уроки 50 Лет Разработки ПО


Урок 21. Проблемы легче предупредить, чем исправить
Время, потраченное на вдумчивое рассмотрение проекта, с лихвой окупается временем, которое не было потрачено на исправление проблем позже. Вам может понадобиться дополнительное время на то, чтобы усовершенствовать проект в условиях неопределённости, поэтому старайтесь распределять усилия в соответствии с характером проблемы. Однако даже приложив максимум усилий для создания безупречного проектного решения, позднее вы можете обнаружить недостатки и придётся их корректировать.

Технический долг и рефакторинг
Проекты, выполненные в спешке, могут привести к появлению технического долга — так называют недостатки, которые кто-то должен устранить в будущем, чтобы обеспечить надлежащее функционирование продукта и его расширяемость. Небольшой технический долг может быть приемлемым компромиссом, если экономия на проектировании и разработке кода ускоряет достижение текущей бизнес-цели. Однако недостатки остаются. Чем дольше команда откладывает их решение, тем более масштабной, дорогостоящей и разрушительной будет доработка. Как и любой кредит, технический долг следует рассматривать как временный и требующий постепенного погашения.

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

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

Внесение изменений в проект требует усилий, тогда как уровень ценности для клиента остаётся прежним, но этот труд помогает поддерживать стабильную основу, необходимую для дальнейшего развития продукта. Хорошее проектирование сводит к минимуму возникающий технический долг, а рефакторинг устраняет его. Разумный баланс этих двух факторов даёт наилучшие результаты. Недостаточное проектирование может привести к значительным доработкам; но на слишком детальную проработку проекта может потребоваться чересчур много времени, при этом хороший результат не гарантирован.

Постоянное совершенствование проектного решения упрощает работу с кодом. Однако на практике обычно выбирается другой путь: небольшой рефакторинг и большое внимание к целесообразности добавления новых функций.

Продумать или узнать все в начале проекта практически невозможно. Однако вы можете использовать свой опыт и опыт других, чтобы выбрать определённое направление. Сегодня вы можете принимать решения, которые завтра минимизируют потребность внесения изменений.

Цель проектирования в том, чтобы принять разумные решения сейчас и предотвратить ненужные изменения в будущем. Опираясь на свои рассуждения и мнение заинтересованных сторон, выбирайте такие решения, которые уменьшат вероятность появления необходимости изменять те или иные части продукта в будущем.

Архитектурные недостатки
Внесение небольших изменений в проект по мере продвижения — не слишком болезненный процесс, при этом продукт непрерывно и постепенно улучшается. Серьёзная реорганизация архитектуры ради увеличения устойчивости продукта или улучшения пользовательского опыта дастся гораздо сложнее.

Разработчики ПО всегда создают проект либо в процессе работы, либо в ходе тщательного обдумывания. Накопление технического долга из-за того, что у команды нет времени на надлежащее проектирование, просто отодвигает проблему в будущее, где её влияние продолжает расти. Инвестирование в проектное решение позволяет сэкономить силы и время на реструктуризацию и повторную реализацию в будущем, когда предпочтительнее работать над чем-то другим.

Источник: Карл Вигерс “Жемчужины Разработки”. СПб.: Питер, 2024. Глава 3.
👍10
День 2041. #юмор
👍20
День 2042.
DotNext 2024. 10-11 сентября. Москва + трансляция.
10 сентября выступаю на DotNext с докладом «Что нового в .NET 9», которая выйдет в ноябре этого года. Расскажу о новинках в C#, ASP.NET, Entity Framework, MAUI и даже WinForms.

Кроме того, в программе конференции доклады про кроссплатформенность, JIT, архитектуру, безопасность, перформанс, инструменты в разработке и многое другое. Кроме докладов на конференции мастер-классы, интервью и круглые столы от спикеров из разных компаний.

Буду рад всех видеть и пообщаться лично! Также можете задавать вопросы к докладу в онлайне. Если решите купить билет самостоятельно, пишите в личку — постараюсь помочь.

До встречи!
👍48👎1
День 2043. #Testing
Автоматизированные Тесты. Начало
Т
есты важны для обеспечения качества приложения. Существует множество видов тестов: модульные, интеграционные, функциональные, дымовые и т.п. Но некоторые тесты не вписываются ни в одну категорию, и создатели придают им другое значение, создавая их.

Тесты — это компромиссы:
- Уверенность: Гарантирует ли тест, что приложение работает так, как ожидалось?
- Продолжительность: Сколько времени требуется для выполнения теста?
- Надёжность: Выдаёт ли тест случайные сбои?
- Усилия по написанию: Легко ли реализовать тестовый сценарий?
- Стоимость обслуживания: Сколько времени нужно на обновление теста при изменении кода?

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

Теперь я принял новую стратегию, которая оказалась очень эффективной для разработанного мной приложения. Мои текущие практики включают:
1. Тестирование в основном публичного API, поскольку внутренние компоненты неявно тестируются и включаются в метрики покрытия кода. Публичный API – не обязательно классы или методы, а может быть точками входа приложения или конечными точками HTTP.
2. Минимизация использования моков, резервирование их для особых случаев, таких как взаимодействие клиентов с внешними сервисами.
3. Использование строгого статического анализа, чтобы избежать написания тестов для того, что может быть обнаружено во время компиляции.
4. Больше утверждений в коде (например, Debug.Assert).

Это даёт несколько преимуществ:
1. Меньше тестов нужно поддерживать.
2. Пользовательские сценарии чётко определены и протестированы.
3. Не нужно переписывать тесты при рефакторинге кода или изменениях в реализации, пока API не изменился.
4. Возможно безопасно рефакторить код, обеспечивая неизменное поведение.
5. Помогает выявлять устаревшие сегменты кода.

Тестирование публичного API
Публичный API — это то, как ваше приложение отображается для пользователей. Публичный API может отличаться в зависимости от типа проекта:
- Библиотека — классы и методы, доступные пользователям библиотеки.
- Веб-API — конечные точки HTTP.
- Приложение CLI — аргументы командной строки.

Зачем тестировать публичный API?
- Вы гарантируете, что приложение работает, как ожидается, и в тестах, и когда его будут использовать пользователи.
- Вы не связаны с деталями реализации, поэтому вы можете рефакторить код, не обновляя тесты, пока поведение не изменится.
- Меньше тестов, чем при тестировании многих отдельных классов или методов.
- Увеличивается покрытие кода, т.к. тестируется больше частей приложения.
- Легче отлаживать приложение, т.к. можно воспроизводить полные сценарии.

Например, если вы пишете приложение ASP.NET Core, не тестируйте класс Controller напрямую. Используйте WebApplicationFactory для настройки и запуска сервера, затем отправляйте запрос и проверяйте ответ. Так вы тестируете приложение в целом, включая маршрутизацию, привязку модели, фильтры, промежуточное ПО, внедрение зависимостей и т. д. Также возможен рефакторинг на Minimal API или изменение промежуточного ПО без обновления тестов.

Это не значит, что не следует тестировать детали реализации. Но это должно быть ограничено несколькими случаями, когда нужно проверить определённое поведение или когда есть сложный метод, который трудно тестировать.

Тестирование общедоступного API не предполагает тестирование методом черного ящика. Можно заменить некоторые зависимости. Например, проверить, как ведёт себя приложение, когда сторонний API возвращает ошибку.

Продолжение следует…

Источник:
https://www.meziantou.net/automated-tests.htm
Автор оригинала: Gérald Barré
👍14👎1
День 2044. #Testing
Автоматизированные Тесты. Продолжение

Начало

Подразумевает ли термин «модульное тестирование» тестирование каждой единицы реализации? Не совсем. Кент Бек, автор книги «Экстремальное программирование: разработка через тестирование», признал, что название «модульный тест», возможно, не лучший выбор. В этом контексте «модуль» относится к определённому поведению, которое может включать взаимодействие нескольких модулей реализации. Некоторые даже определяют «модуль» как целый пакет или библиотеку, и эта точка зрения также верна. Некоторые используют термины «изолированные» и «коммуникабельные» тесты, чтобы различать тесты, использующие и не использующие моки зависимостей.

Пишите код помогающий писать тесты
Написание теста должно быть простым, а тестовый код — понятным. Важно создать тестовую инфраструктуру, которая упрощает процесс написания тестов. Часто используют класс TestContext, содержащий общий код для написания тестов. Например, он может содержать код запуска приложения, отправки запроса, подтверждения ответа, регистрации мока и т. д. Так вы сможете сосредоточиться на написании теста, а не на шаблонном коде:
// Arrange
using var ctx = new TestContext();
ctx.ReplaceService<IService, MockImplementation>();
ctx.SetConfigurationFile("""{ "path: "/tmp" }""");
ctx.SeedDatabase(db =>
db.Users.Add(new User { Username = "user" }));
var user = ctx.SetCurrentUser("user");

// Act
var response = await ctx.Get("/user");

// Assert
Assert.AreEqual(response, """
Id: 1
UserName: user
""");


Нужно ли имитировать зависимости?
Моки (Moq) — способ протестировать приложение без использования реальных зависимостей. Поэтому, используя моки, вы тестируете не реальное приложение, а его разновидность. Важно максимально приблизить эту разновидность к реальности. Может быть сложно поддерживать тесты с моками. Сервисы, которые вы имитируете, могут изменить поведение, и вы можете не заметить этого, поскольку не используете их напрямую. Также приходится писать много кода для настройки тестов.

Разработчики, как правило, используют слишком много моков. Нужно ли имитировать файловую систему? Это не так-то просто, и у многих неправильные предположения о ней. Например, в Windows она чувствительна к регистру, некоторые имена файлов или символы в них недопустимы. В случае кроссплатформенного приложения может потребоваться протестировать поведение, специфичное для ОС. Почему бы просто не писать во временный каталог? То же самое касается БД. Можно использовать Docker для запуска БД, чтобы протестировать SQL и ограничения базы?

В большинстве случаев нужно имитировать внешние сервисы, которые не находятся под вашим контролем. Рассмотрите возможность использования фактических сервисов, когда это возможно. Конечно, желательно, чтобы тесты были изолированы. Здесь есть много стратегий. Например, если нужно читать или записывать файлы, можно создать временный каталог для каждого теста. Если вы запускаете БД в контейнере, можно использовать разные имена баз для изоляции тестов. Можно положиться на эмуляторы, предоставляемые поставщиками облачных услуг, для локального запуска некоторых зависимостей. Так вы будете уверены, что код работает, как ожидалось, и не нужно писать сложные моки. Убедитесь, что приложение можно настроить, например, легко подставлять разные строки соединений или папки для хранения данных приложения.

Полезные инструменты для избежания моков
- TestContainers или .NET Aspire. Если запуск Docker-контейнера медленный, можно переиспользовать его между несколькими тестовыми запусками. .NET Aspire также может предоставлять ресурсы в облаке (Azure, AWS и т. д.).
- Эмуляторы облачных сервисов. Например, Azure предоставляет эмулятор для хранилища.

Продолжение следует…

Источник:
https://www.meziantou.net/automated-tests.htm
👍16👎1
День 2045. #Testing
Автоматизированные Тесты. Продолжение

Начало
Продолжение

Фреймворки-имитаторы
Фреймворки-имитаторы могут быть полезны для тестирования сценариев, которые трудно воспроизвести. Например, проверить, может ли приложение обрабатывать определённую ошибку, возвращаемую сторонним API. Тогда можно имитировать API, чтобы вернуть ожидаемую ошибку.

Я стараюсь избегать использования фреймворков-имитаторов, а создавать тестовые двойники вручную. Тестовые двойники могут более точно имитировать реальные сценарии, чем фреймворки-имитаторы. Они могут сохранять состояние между несколькими вызовами методов, что может быть сложно с фреймворками-имитаторами. Может потребоваться убедиться, что мок вызывается в правильном порядке (например, Save перед Get – хотя это можно сделать), или что вызывается правильное количество раз.

Кроме того, при отладке тестов можно легко перейти в код тестового двойника, тогда как это не всегда осуществимо с фреймворками-имитаторами. Добавляя пользовательские методы к тестовым двойникам для настройки или проверки содержимого, вы можете повысить выразительность, сделав тесты более читаемыми и простыми в обслуживании. Тестовые двойники также можно повторно использовать в нескольких тестах.

Это может показаться более трудоёмким, но эти усилия не существенны. Обычно вы не имеете дело с чрезмерным количеством фиктивных классов, и можете повторно использовать обобщённую реализацию в различных тестах. Так вы избегаете дублирования блоков кода для настройки в каждом тесте.
// Используем фреймворк-имитатор FakeItEasy
// Что, если сервис вызовет GetAsync до SaveAsync?
var service = A.Fake<IService>();
A.CallTo(() => service.SaveAsync()).Returns(true);
A.CallTo(() => service.GetAsync()).Returns(["value"]);

var sut = new MyClass(service);
sut.DoSomething();

Сравните с:
// Используем собственный двойник 
var service = new StubService();
service.AddItem("value"); // Добавляем данные

var sut = new MyClass(service);
sut.DoSomething();


Нужно ли добавлять интерфейсы, чтобы иметь возможность имитировать?
Интерфейсы с единственной реализацией бесполезны. Они просто добавляют сложности коду. По возможности не усложняйте код только для того, чтобы иметь возможность использовать моки. Если в коде будет единственная реализация, то вам может не понадобиться интерфейс. Кроме того, можно создать мок самого класса, а не интерфейса.

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

Окончание следует…

Источник:
https://www.meziantou.net/automated-tests.htm
Автор оригинала: Gérald Barré
👍7👎1
День 2046. #Testing
Автоматизированные Тесты. Окончание

Начало
Продолжение 1
Продолжение 2

Пишите больше утверждений в коде
Хотя утверждения (Assert) в тестах являются наиболее распространённым способом проверки поведения кода, можно писать утверждения и в коде. Например, использовать Debug.Assert для проверки состояния приложения. Это может быть полезно для проверки некоторых предположений, которые у вас есть относительно кода. Одним из преимуществ является то, что ошибка будет возникать на ранней стадии и также при отладке приложения, а не только при запуске тестов.

Утверждения также могут улучшить читаемость кода. Вы можете увидеть ожидаемое поведение непосредственно в коде, что может быть полезно при написании сложного кода или когда вы хотите проверить определённое поведение:
public void DoSomething()
{
Debug.Assert(_service != null,
"Этого быть не должно!");
_service.DoSomething();
}


Тестовые фреймворки
Используйте тот, который вам больше нравится: xUnit, NUnit и MSTests очень похожи по функциям. Синтаксис может отличаться, но концепции одинаковы. Для утверждений можно использовать встроенные или библиотеку вроде FluentAssertions.

Покрытие
Не стоит стремиться к 100% покрытию тестами. Вот некоторые проблемы с этой метрикой:
- 100% покрытие означает, что ошибок нет? Нет, это просто означает, что весь код покрыт тестами. И чаще всего только ваш код, но не зависимости.
- Сколько времени вы потратите на написание тестов, чтобы покрыть последние 10% кода? Стоит ли оно того?
- Нужно ли покрывать все пути в коде? Например, если есть метод, который выдаёт ArgumentNullException, нужно ли его тестировать? Добавляет ли это больше уверенности в коде или это просто пустая трата времени?

Вместо этого сосредоточьтесь на покрытии большей части кода. 70-80% часто является хорошим компромиссом с точки зрения уверенности и усилий. Это означает, что есть тесты, которые покрывают большую часть кода, и вы не тратите слишком много времени на их написание. Не забывайте, что тесты предназначены для того, чтобы придать достаточно уверенности в коде. Не нужно тестировать всё.

Статический анализ
Инструменты статического анализа могут помочь найти проблемы в коде перед запуском тестов, например, нулевые ссылки, неиспользуемые переменные и т. д. Инструменты могут помочь обнаружить проблемы во время компиляции, что может сократить количество тестов, которые нужно написать.

Нестабильность тестов
Тесты должны быть надёжными. Если тест не проходит, это должно быть связано с ошибкой в коде, а не в тесте. Если тест не проходит случайно, вы потеряете доверие к тестовому набору и начнёте игнорировать не пройденные тесты.

Есть несколько способов справиться с нестабильными тестами:
- Исправить тест - лучшее решение.
- Поместить тест в карантин и создать задачу для его исправления позже.
- Удалить тест, если он бесполезен.

Некоторые инструменты CI могут обнаружить нестабильность теста. Например, Azure DevOps может это делать.

См. также:
-
Мутационное тестирование с помощью Stryker
-
Пишите Тесты Быстрее с Approval Tests

Источник: https://www.meziantou.net/automated-tests.htm
👍5