Jest Contract Matching
This challenge focuses on implementing a robust contract matching mechanism within Jest tests. You will create a utility function that asserts whether a given object conforms to a predefined contract (schema), which is particularly useful for verifying API responses, configuration files, or data structures in a reliable and maintainable way.
Problem Description
You need to develop a Jest custom matcher, named toMatchContract, that checks if an actual received object adheres to a specified contract (schema). The contract should define the expected structure, data types, and optionally, specific values or patterns for properties within an object. This matcher will help ensure that data exchanged between different parts of an application or between services remains consistent.
Key Requirements:
- The
toMatchContractmatcher should accept acontractobject as an argument. - The
contractobject will define the expected structure and types. - The matcher should recursively traverse nested objects and arrays.
- It should support basic type checking (e.g.,
string,number,boolean,object,array). - It should allow specifying exact values for properties.
- It should handle optional properties gracefully.
- The matcher should provide clear and informative error messages when the contract is not met, indicating the specific property and reason for failure.
Expected Behavior:
- If the
actualobject matches thecontract, the assertion passes. - If the
actualobject does not match thecontract, the assertion fails with a descriptive error message.
Edge Cases to Consider:
- Empty objects or arrays in both
actualandcontract. nullorundefinedvalues.- Arrays where the contract specifies an array of a certain type, but the actual might contain different types or lengths.
- Deeply nested structures.
- Properties present in
actualbut not defined in thecontract(should they be allowed or disallowed? For this challenge, assume they are allowed unless explicitly disallowed by a specific rule not yet defined, but focus on matching what is defined).
Examples
Example 1:
Input:
const contract = {
name: 'string',
age: 'number',
isActive: 'boolean',
address: {
street: 'string',
city: 'string',
},
};
const actual = {
name: 'Alice',
age: 30,
isActive: true,
address: {
street: '123 Main St',
city: 'Anytown',
},
};
Output:
Assertion expect(actual).toMatchContract(contract) passes.
Explanation: The actual object perfectly matches the structure and types defined in the contract.
Example 2:
Input:
const contract = {
id: 'number',
tags: ['string'], // Represents an array of strings
metadata: {
timestamp: 'number',
source: 'string',
},
};
const actual = {
id: 101,
tags: ['jest', 'testing', 'typescript'],
metadata: {
timestamp: 1678886400,
source: 'api',
},
extraInfo: 'This is extra' // This property is allowed as per general understanding
};
Output:
Assertion expect(actual).toMatchContract(contract) passes.
Explanation: The actual object matches the contract, including the array of strings for tags and the nested metadata object. Extra properties in actual are ignored by default.
Example 3: (Failure Case)
Input:
const contract = {
productName: 'string',
price: 'number',
available: 'boolean',
};
const actual = {
productName: 'Laptop',
price: 1200,
available: 'yes', // Type mismatch: expected boolean, got string
};
Output:
Assertion expect(actual).toMatchContract(contract) fails with an error like:
"Expected property 'available' to be of type 'boolean', but received 'string' with value 'yes'."
Explanation: The available property in actual is a string ('yes'), but the contract expects a boolean.
Example 4: (Nested Failure Case)
Input:
const contract = {
user: {
firstName: 'string',
lastName: 'string',
profile: {
email: 'string',
phone: 'string',
},
},
};
const actual = {
user: {
firstName: 'John',
lastName: 'Doe',
profile: {
email: 'john.doe@example.com',
phone: 1234567890, // Type mismatch: expected string, got number
},
},
};
Output:
Assertion expect(actual).toMatchContract(contract) fails with an error like:
"Expected property 'user.profile.phone' to be of type 'string', but received 'number' with value '1234567890'."
Explanation: There's a type mismatch for the phone property within the deeply nested profile object.
Example 5: (Optional Property Handling - Assuming contracts define optionality implicitly for now)
Input:
const contract = {
userId: 'number',
username: 'string',
// 'email' property is not in the contract, so it's implicitly not required by the contract for this challenge.
};
const actual = {
userId: 456,
username: 'johndoe',
email: 'john.doe@example.com', // This property is extra but the contract is fulfilled
};
Output:
Assertion expect(actual).toMatchContract(contract) passes.
Explanation: The actual object has all required properties from the contract and their correct types. The extra email property is ignored.
Constraints
- The
contractcan be nested up to 10 levels deep. - The
actualobject can also be nested up to 10 levels deep. - The total number of properties to check in any given object/contract should not exceed 50.
- Input
actualandcontractwill always be valid JavaScript objects or primitives for type checking. - The solution should be implemented using TypeScript.
Notes
- Consider how to represent types in your contract. A common approach is to use string literals like
'string','number','boolean'. For arrays, you might use a convention like['string']to denote an array of strings. - Think about how to handle the recursive nature of the matching.
- The error reporting is crucial for debugging, so make sure it's informative.
- For this challenge, we will assume that properties present in the
actualobject but not in thecontractare allowed and do not cause a failure. The focus is on validating the properties defined in the contract. - You'll need to extend Jest's
expecttype definitions to include your custom matcher.