Registering functions
During verification, ContractCase needs to be able to invoke the real
implementation of each function named in the contract. You provide these with
registerFunction, called on the verifier before running the verification.
The first argument is the function name from the contract (the functionName
the consumer used when defining the
interaction), and
the second is the implementation to invoke:
- Typescript
- Java
verifyContract(
{
providerName: 'function execution',
},
(verifier) => {
verifier.registerFunction('zeroArgs', () => {});
verifier.registerFunction('concatenate', (a, b) => `${a}${b}`);
},
);
ContractVerifier verifier = new ContractVerifier(
ContractCaseConfigBuilder.aContractCaseConfig()
.providerName("Java Function Implementer Example")
.build());
verifier.registerFunction("NoArgFunction", () -> null);
verifier.registerFunction(
"PageNumbers",
convertJsonIntegerArg((Integer num) -> num + " pages"));
verifier.runVerification(
ContractCaseConfigBuilder.aContractCaseConfig().build());
If the contract contains an interaction for a function that hasn't been registered, ContractCase fails the verification with a configuration error telling you which function name it was looking for.
Argument and return value serialisation
Arguments and return values are JSON-serialised when they cross the boundary
between ContractCase and your code. In dynamically typed languages like typescript,
this is handled for you. In Java, registered functions receive each argument as a JSON
string and must return a JSON string, so you may need a small adapter around
the function under test to parse the arguments and serialise the result. For
example, the convertJsonIntegerArg adapter used above is:
- Typescript
- Java
// With Typescript/Javascript, arguments and return values
// are marshalled for you, so no adapter is needed
// Registered functions receive each argument as a JSON string, and
// must return a JSON string. An adapter like this parses the arguments
// and serialises the result of the real function under test.
private static @NotNull <R> InvokableFunction1<?>
convertJsonIntegerArg(Function<Integer, R> functionUnderTest) {
return (String a) -> {
try {
var arg1 = mapper.readValue(a, Integer.class);
return mapper.writeValueAsString(functionUnderTest.apply(arg1));
} catch (JsonProcessingException e) {
throw new RuntimeException("Unable to parse argument");
}
};
}
Functions that throw
If the interaction was defined with a throwing variant (see defining function
call interactions),
ContractCase invokes your registered function and expects it to throw. The
class name of the thrown error is compared with the errorClassName in the
contract.
If the interaction also describes errorInternals, the thrown error is serialised
and its content is compared with the errorInternals matcher:
- In Typescript, the error internals are the error's own enumerable properties
(excluding the standard
name,message,stackandcause). - In Java, the exception is serialised with Jackson, so the error internals contain
any public getters on the exception class (the standard
Throwableproperties such as the stack trace, cause and message are excluded). If your exception has properties that can't be serialised, exclude them with Jackson annotations such as@JsonIgnore.
State handlers
If any interactions in the contract have states, you provide state handlers in exactly the same way as for HTTP server verification. This is often how you set up the data that a registered function needs to return the expected values.