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

от автора

Вступление

Думаю, большинство читателей согласится, что автоматизированное тестирование — полезный, а во многих областях даже необходимый, этап создания программ. А так как программисты — народ ленивый, то и инструментов, облегчающих этот этап существует немало. Одним из таких инструментов является 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *