Hone logo
Hone
Problems

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 toMatchContract matcher should accept a contract object as an argument.
  • The contract object 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 actual object matches the contract, the assertion passes.
  • If the actual object does not match the contract, the assertion fails with a descriptive error message.

Edge Cases to Consider:

  • Empty objects or arrays in both actual and contract.
  • null or undefined values.
  • 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 actual but not defined in the contract (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 contract can be nested up to 10 levels deep.
  • The actual object 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 actual and contract will 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 actual object but not in the contract are 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 expect type definitions to include your custom matcher.
Loading editor...
typescript