Experience using AutoFixture to generate gRPC messages

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);

As you can see from the example above, the tool is capable of creating built-in types, user-defined types, and collections of arbitrary types. The main thing is that they have a constructor available, and the types of its parameters, in turn, fit the same conditions.

Problem

In my work, I needed to create test data for gRPC message types. These types themselves are generated automatically from proto-files.

First, let’s instantiate a message for such a contract:

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);

While everything works: an instance is created, the property is initialized with a non-empty string, class!

Let’s try to add a field with an attribute repeated. According to the protobuf specification, such fields can have any number of elements.

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

Bam!!! What happened? Collection LuckyNumbers in an instance of the generated type is empty. The point is that AutoFixture by default initializes an instance of a type by calling its constructor and then all available property setters. And the repeated-fields of the contract become properties that have only a getter, but no setter:

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

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

It can be seen from the code that the property LuckyNumbers there is no setter available, which is why AutoFixture couldn’t populate the collection with elements!

A quick googling suggested that you can tweak the AutoFixture settings like this:

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

This setting should tell the tool to populate the collection properties even if they don’t have an available setter. If only there was a getter, yes a method Add at the collection.

We try:

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

and get Bam #2!!! :

System.Reflection.AmbiguousMatchException: Ambiguous match found.

At this point, I confess, I am a little disheartened. Then I decided to check what was the matter: in AutoFixture or in the code generated by the contract. To do this, I sketched a small class with the same property without a setter, with the only difference being that this time the collection type was simple 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);

This time, no exception was thrown, the elements were in the collection, as expected. Suspicion that the last time the exception appeared due to the peculiarities of the class RepeatedField<T> everything got stronger.

I dug into the debugger, trying to figure out what ambiguous (ambiguous) was in RepeatedFieldwhich was not List. Set a breakpoint on the exception in the debugger System.Reflection.AmbiguousMatchException.

Quite quickly found out that the exception occurs in the method InstanceMethodQuery.SelectMethods. Fortunately, the source code of the tool is open, here is the text of the method:

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) };
}

And wherein MethodName has the value “Add”. The assembly browser in Rider showed (see picture) that the RepeaterField type has two public Add methods: one for a single element, the other for a sequence of them. Therefore, AutoFixture could not choose which method it needed and crashed with an error. And to be more precise, the method fell GetMethod in the guts of the dotnet runtime.

Solution

Well, the cause of the problem became clear. It remained to come up with a solution. I decided to add an additional setting to AutoFixture that allows you to initialize instances of type RepeatedField<T>. Luckily, this unfortunate fellow had a method AddRangewhich I was going to use to fill the collection.

I decided to go with the proven copy-paste method and duplicate the code ReadonlyCollectionPropertiesBehaviorchanging it only when necessary. It turned out that we would have to change quite a bit: the search for a suitable initialization method (the same AddRange) and preparing parameters for it. Because if ReadonlyCollectionPropertiesBehavior filled the collection element by element by calling Addthen I had to first prepare a sequence of elements, and only then call once AddRangepassing it in its entirety.

There are no more complications. Ready solution can be found in my repositories on github.

I am grateful to the authors of AutoFixture for such a useful tool and I encourage all Sharpists to consider using it in their practice.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *