Open edeandrea opened 2 years ago
Hi - I know I only created this late last week, but it is somewhat time sensitive.
@holly-cummins and I are doing a talk at Devoxx Belgium in a couple of weeks (https://devoxx.be/talk/?id=6661) and we wanted to use the Quarkus Sample application (https://github.com/quarkusio/quarkus-super-heroes) as a demo for "showing off" Pact as well as its integration with pactflow.io.
This issue is a blocker for that. Neither she nor I can figure out how to get the provider tests to pass using the matchers specified in the Pact generated from the consumers.
Any help would be greatly appreciated!
matchHeader
sets up a regex to match the header value. With regexes, they need to match the whole string. So setting .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN, MediaType.TEXT_PLAIN)
won't work. You should try something like .matchHeader(HttpHeaders.CONTENT_TYPE, "text/plain.*", MediaType.TEXT_PLAIN)
However, Pact understands how to match content-type headers, so you don't need to use matchHeader
at all. If you specify just the base header value without the attributes, it will ignore the charset attribute fro the provider. However, if you set a specific attribute (say charset=US-ASCII
) it will check the charset value matches correctly.
Coming to the body, most of the "type" matchers will only work with structured documents (like JSON and XML). Think about what you are trying to so. You body is set as text/plain
, and you want to ensure it is always the same type as a string. But a plain text body can only ever be represented as a string.
The only matcher that works with plain text is the regex one. Remember the regex will need to match the entire body contents. You may also want to consider using a JSON body instead of plain text.
Thank you for the comments. I will give it a try tomorrow.
Unfortunately I can't change the APIs. They are what they are.
So for matching a text/plain body my matcher should essentially be .+
?
That will match everything except the empty string, looking at your example you could also try something like Hello villains.*
But then wouldn't that make it so the consumer understood what the provider's test data was? In a real world situation I'm not sure the consumer would know what text to expect other than "some text". The consumer has no control over what the provider might send back (and honestly, this consumer really doesn't care as long as it's something.
I'm trying to be as loose as possible and not have the provider and consumer not know too much about each other, other than the contract.
That makes sense, essentially you are stating the body must not be empty.
Thank you. I will play with this more in the morning!
@rholshausen I did get the body to validate using the regex!
However, Pact understands how to match content-type headers, so you don't need to use matchHeader at all. If you specify just the base header value without the attributes, it will ignore the charset attribute fro the provider. However, if you set a specific attribute (say charset=US-ASCII) it will check the charset value matches correctly.
I'm not sure I quite understand? If I don't use matchHeader
then no matching seems to take place on the header.
In my consumer test I do
@Pact(consumer = "rest-fights")
public V4Pact helloPact(PactDslWithProvider builder) {
return builder
.uponReceiving("A hello request")
.path(VILLAIN_HELLO_URI)
.method(HttpMethod.GET)
.headers(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN)
.willRespondWith()
.headers(Map.of(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN))
.status(Status.OK.getStatusCode())
.body(PactDslRootValue.stringMatcher(".+", DEFAULT_HELLO_RESPONSE))
.toPact(V4Pact.class);
}
which generates this Pact
{
"consumer": {
"name": "rest-fights"
},
"interactions": [
{
"comments": {
"testname": "io.quarkus.sample.superheroes.fight.client.VillainConsumerContractTests.helloVillains()",
"text": [
]
},
"description": "A hello request",
"key": "2ec6e2e8",
"pending": false,
"request": {
"headers": {
"Accept": [
"text/plain"
]
},
"method": "GET",
"path": "/api/villains/hello"
},
"response": {
"body": {
"content": "Hello villains!",
"contentType": "text/plain",
"contentTypeHint": "DEFAULT",
"encoded": false
},
"headers": {
"Content-Type": [
"text/plain"
]
},
"matchingRules": {
"body": {
"$": {
"combine": "AND",
"matchers": [
{
"match": "regex",
"regex": ".+"
}
]
}
}
},
"status": 200
},
"type": "Synchronous/HTTP"
}
],
"metadata": {
"pact-jvm": {
"version": "4.3.14"
},
"pactSpecification": {
"version": "4.0"
}
},
"provider": {
"name": "rest-villains"
}
}
When I verify on the provider I get
[from Pact Broker https://quarkus-super-heroes.pactflow.io/pacts/provider/rest-villains/consumer/rest-fights/pact-version/e7b9f3ef4a9a6651d81db46e546d2b19902a75d5/metadata/c1tdW2JdPW1haW4mc1tdW2xdPXRydWUmc1tdW2N2XT0xNg]
A hello request
Test Name: io.quarkus.sample.superheroes.fight.client.VillainConsumerContractTests.helloVillains()
Comments:
07:48:19 DEBUG [io.qu.sa.su.vi.re.VillainResource] (executor-thread-0) Hello Villain Resource
returns a response which
has status code 200 (OK)
has a matching body (OK)
It doesn't seem to do any kind of verification on the Content-Type
header.
Or do you mean to say that if the Content-Type
header is not text/plain
(or any derivative of it), then the verification test will automatically fail?
I think i've moved on past the "hello world" one to something a little more complicated.
I have a GET
endpoint where the provider returns a JSON payload and a 200
in some conditions but a 404
in other conditions.
I've gotten the provider to verify the 200
condition fine. For the 404
condition, I used a state in the Pact. In the provider test I am doing this:
@State("No random villain found")
public void clearData() {
}
It doesn't seem, though, that I have access to any of the beans in my application when that method fires. I tried to use mocking and I also tried to inject my application's datasource as a CDI bean and that doesn't seem to work either. Any class-level attribute that I've injected into my test class seems to be null
. I'm not sure how I can alter the state of the application without being able to interact with some of the beans in the application.
When I go to run the tests I end up with a NullPointerException
because the bean injection is null
at the time the @State
method runs.
I was actually able to get this work by doing some stuff in the @BeforeEach
method. At that point the CDI context is fully available.
@QuarkusTest
@Provider("rest-villains")
@PactBroker(url = "https://quarkus-super-heroes.pactflow.io")
@EnabledIfSystemProperty(named = "pactbroker.auth.token", matches = ".+", disabledReason = "pactbroker.auth.token system property not set")
public class ContractVerificationTests {
@ConfigProperty(name = "quarkus.http.test-port")
int quarkusPort;
@InjectSpy
VillainService villainService;
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@BeforeEach
void beforeEach(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", this.quarkusPort));
var isNoRandomVillainFoundState = Optional.ofNullable(context.getInteraction().getProviderStates())
.orElseGet(List::of)
.stream()
.filter(state -> "No random villain found".equals(state.getName()))
.count() > 0;
if (isNoRandomVillainFoundState) {
when(this.villainService.findRandomVillain())
.thenReturn(Optional.empty());
}
}
@PactBrokerConsumerVersionSelectors
public static SelectorBuilder consumerVersionSelectors() {
return new SelectorBuilder()
.branch(System.getProperty("pactbroker.consumer.branch", "main"));
}
@State("No random villain found")
public void clearData() {
// Already handled in beforeEach
}
}
This could totally be user error as I'm somewhat new to this, but for the life of me I can not figure out how to get the provider to verify a pact without it trying to do exact matching.
I have a consumer which generates this pact:
It is generated by a JUnit 5 test (
au.com.dius.pact.consumer:junit5:4.3.14
)The pact is published to pactflow.io.
On the provider side I have this (
au.com.dius.pact.provider:junit5:4.3.14
):When I run the verification I'm getting this:
It looks like its doing exact matching on the header and body, even though the pact has matching rules where 1) The
Content-Type
header should containtext/plain
, not be equal totext/plain
2) The body should match anyString
and shouldn't be trying to verify an exactString
Am I doing something wrong here or missing something? Or is something not working the way it should?
On the consumer I also tried using
PactSpecVersion.V3
instead ofPactSpecVersion.V4
but got the same result.The entire source code of the consumer & provider is available: