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