Опыт использования AutoFixture для генерации gRPC сообщений

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Вступление

Думаю, большинство читателей согласится, что автоматизированное тестирование - полезный, а во многих областях даже необходимый, этап создания программ. А так как программисты - народ ленивый, то и инструментов, облегчающих этот этап существует немало. Одним из таких инструментов является AutoFixture - средство для генерации тестовых экземпляров. Этот инструмент уже не раз упомянался на Хабре, например тут. Далее я расскажу, с какой проблемой столкнулся в попытке применить AutoFixture в своей работе и как решил эту проблему.

Вкратце напомню, как выглядит использование AutoFixture на практике.

using AutoFixture;

var fixture = new Fixture();

var intValue = fixture.Create<int>();
Console.WriteLine(intValue);

var complexType = fixture.Create<ComplexType>();
Console.WriteLine(complexType);

var collection = fixture.Create<List<ComplexType>>();
Console.WriteLine(string.Join(", ", collection));

record ComplexType(int IntValue, string StringValue);

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

Проблема

Мне в работе понадобилось создавать тестовые данные типов gRPC сообщений. Сами эти типы генерируются автоматически по proto-файлам.

Для начала, давайте создадим экземпляр сообщения для такого контракта:

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
using AutoFixture;
using AutoFixtureWithGrpc;

var fixture = new Fixture();

var message = fixture.Create<HelloRequest>();
Console.WriteLine(message);

Пока всё работает: экземпляр создаётся, свойство инициализируется непустой строкой, класс!

Попробуем добавить поле с атрибутом repeated. По спецификации protobuf такие поля могут иметь любое количество элементов.

message HelloRequest {
  string name = 1;
  repeated int32 lucky_numbers = 2;
}

Бам!!! Что случилось? Коллекция LuckyNumbers в экземпляре сгенерированного типа оказывается пустой. Дело в том, что AutoFixture по умолчанию инициализирует экземпляр типа, вызывая его конструктор, а затем все доступные сеттеры свойств. А repeated-поля контракта становятся свойствами, у которых есть только геттер, а сеттера нет:

public sealed partial class HelloRequest : pb::IMessage<HelloRequest>
{
    // .. часть кода пропущена для краткости
    public HelloRequest() { }

    public pbc::RepeatedField<int> LuckyNumbers {
      get { /* ... */ }
    }
}

Из кода видно, что у свойства LuckyNumbers отсутсвует доступный сеттер, поэтому-то AutoFixture и не смог заполнить коллекцию элементами!

Быстрое "гугление" подсказало, что можно покрутить настройки AutoFixture таким образом:

var fixture = new Fixture();
fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());

Такая настройка должна сообщить инструменту, что нужно заполнять свойства-коллекции даже если у них отсутствует доступный сеттер. Лишь бы был геттер, да метод Add у коллекции.

Пробуем:

fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());
var message = fixture.Create<HelloRequest>();
Console.WriteLine(message.LuckyNumbers.Count);

и получаю Бам №2!!! :

System.Reflection.AmbiguousMatchException: Ambiguous match found.

Тут я, признаюсь, немного приуныл. Затем решил проверить, в чём же дело: в AutoFixture или в сгенерированном по контракту коде. Для этого я набросал небольшой класс с таким же свойством без сеттера с той лишь разницей, что в этот раз типом коллекции был простой List<int>.

class Investigation
{
    private readonly List<int> _values = new();
    public List<int> Ints => _values;
}
fixture.Behaviors.Add(new ReadonlyCollectionPropertiesBehavior());
var message = fixture.Create<Investigation>();
Console.WriteLine(message.Ints.Count);

На этот раз никакого исключения не вылетело, в коллеции лежали элементы, как и положено. Подозрение, что в прошлый раз исключение появилось из-за особенностей класса RepeatedField<T> всё крепло.

Я зарылся в отладчик, пытаясь понять, что же такого неоднозначного (ambiguous) было в RepeatedField, чего не было у List. В отладчике ставлю точку останова на исключение System.Reflection.AmbiguousMatchException.

Довольно быстро выяснилось, что исключение происходит в методе InstanceMethodQuery.SelectMethods. Благо, исходный код инструмента открыт, привожу текст метода:

public IEnumerable<IMethod> SelectMethods(Type type = default)
{
    var method = this.Owner.GetType().GetTypeInfo().GetMethod(this.MethodName);

    return method == null
        ? new IMethod[0]
        : new IMethod[] { new InstanceMethod(method, this.Owner) };
}

И при этом MethodName имеет значение "Add". Обозреватель сборок в Rider-е показал (см. картинку), что у типа RepeaterField есть два публичных метода Add: один для одиночного элемента, другой - для их последовательности. Поэтому-то AutoFixture не мог выбрать, какой именно метод ему нужен и падал с ошибкой. А если точнее, то падал метод GetMethod в кишках дотнетовского рантайма.

Решение

Ну что же, причина проблемы стала ясна. Оставалось придумать решение. Я решил добавить в AutoFixture дополнительную настройку, позволяющую инициализировать именно экземпляры типа RepeatedField<T>. По счастью, у этого злополучного типа оказался метод AddRange, который я и собрался использовать для наполнения коллекции.

Я решил идти проверенным методом copy-paste и продублировать код ReadonlyCollectionPropertiesBehavior, меняя его лишь по необходимости. Оказалось, что менять придётся совсем немного: поиск подходящего метода инициализации (того самого AddRange) и подготовку параметров для него. Потому что если ReadonlyCollectionPropertiesBehavior заполнял коллекцию поэлементно, вызывая Add, то мне предстояло сперва подготовить последовательность элементов, и лишь затем единожды вызвать AddRange, передав её всю целиком.

Тут уже никаких сложностей не осталось. Готовое решение можно найти в моём репозитории на гитхабе.

Я благодарен авторам AutoFixture за такой полезный инструмент и призываю всех шарпистов рассмотреть возможность использовать его в своей практике.

Источник: https://habr.com/ru/post/686386/


Интересные статьи

Интересные статьи

Сайт The Daily WTF уже 16 лет собирает курьёзные, дикие и печальные истории из мира ИТ. Я перевёл несколько рассказов, показавшихся мне интересными. Все имена и названия компаний изменены. Предыдущи...
Привет, меня зовут Диана. Мне 25 лет, по образованию я математик, сейчас работаю продакт-менеджером. Хочу рассказать о том, как я пришла в IT-компанию на вакансию «Просто хороший человек», какой опыт ...
Иногда работая работая с кодом, таблицами, схемами и графиками хочется прикоснуться к чему-то физическому. Создать продукт, который можно увидеть, попробовать. Снять с се...
Хочу поделиться опытом автоматизации экспорта заказов из Aliexpress в несколько CRM. Приведенные примеры написаны на PHP, но библиотеки для работы с Aliexpress есть и для...
Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...