I am currently trying to introduce consumer driven contract tests to an existing C# codebase. Currently I am only looking into testing messages!
While trying to create the consumer tests for some messages, I have run into a problem with abstract types.
Let me explain with an example.
Assume we have the following message:
public abstract record KeyValuePair(string Key);
public record IntegerKeyValuePair(string Key, int IntegerValue) : KeyValuePair(Key);
public record DoubleKeyValuePair(string Key, double DoubleValue) : KeyValuePair(Key);
public record Message(KeyValuePair[] KeyValuePairs);
Now we write the following consumer test:
public class StackOverflowConsumerTest
{
private readonly IMessagePactBuilderV3 _pact;
public StackOverflowConsumerTest()
{
this._pact = Pact.V3(
"StackOverflowConsumer",
"StackOverflowProvider",
new PactConfig { PactDir = "./pacts/" })
.WithMessageInteractions();
}
[Fact]
public void ShouldReceiveExpectedMessage()
{
var message = new
{
KeyValuePairs = Match.MinType(
new
{
Key = Match.Type("SomeValue"),
DoubleValue = Match.Decimal(12.3)
}, 1)
};
_pact
.ExpectsToReceive(nameof(Message))
.WithJsonContent(message)
.Verify<Message>(msg =>
{
Assert.Equal(new DoubleKeyValuePair("SomeValue", 12.3), msg.KeyValuePairs.Single());
});
}
}
Running this sonsumer test will then fail, because I can't deserialize the message from the JSON representation that Pact.Net + Newtonsoft generate:
Newtonsoft.Json.JsonSerializationException: Could not create an instance of type PactNet.Learning.KeyValuePair. Type is an interface or abstract class and cannot be instantiated.
I would usually handle situations like this by changing the serializer settings to include the actual type that gets serialized. However, here the actual objects are never really passed to the serializer. Instead it receives some dynamic type structure containing the matchers that I defined with the expected values. Is there a way to handle abstract types with Pact.Net's matchers that I overlooked or is this simply not possible?
One solution to my problems might be to "just refactor the messages", but that is not what I can do right now.
While it is not the perfect solution, I have an answer to my own question. It does have some shortcomings, but it will address the basic problem.
The solution starts by creating my own type that extends a
Dictionary<string, object>
, similar toExpandoObject
, but also allows you to specifiy the contained type (first shortcoming: the class could check if its properties actually match the expected type). I have also added and interface for easier handling.As a next step we will need a custom
JsonConverter
, that takes the value of theContentType
property and encodes it into the$type
JSON property (if type name handling is activated)In the test setup we need to make sure that we use the correct serializer settings. (Next shortcoming: this example is very reliant on using exactly these settings. However, in the provider tests you can skip the converter.)
And now we can finally create a test based on our new wrapper class:
Here we see the next shortcoming, the test specifies exactly which sub-type of my abstract type to use and the provider will need to match it. You can probably work around this, but it is outside the scope of this answer.
Hope it helps.
Edit: You can also have a look at my github repo for a working example: Link