Hone logo
Hone
Problems

Implementing a Custom Jest Matcher: toHaveProperty

Jest's built-in matchers provide powerful tools for asserting conditions in your tests. One such useful matcher is toHaveProperty, which checks if an object has a specific property, optionally asserting its value. This challenge asks you to implement a custom Jest matcher that replicates this functionality. Understanding how matchers are built will deepen your knowledge of Jest and testing best practices.

Problem Description

You need to create a custom Jest matcher named toHaveProperty. This matcher should be able to:

  1. Check for property existence: Given an object and a property key (string or symbol), it should assert that the object possesses that property.
  2. Check for property existence and value: Given an object, a property key, and an expected value, it should assert that the object possesses the property AND that its value matches the expected value.
  3. Handle nested properties: The matcher should support checking for nested properties using dot notation (e.g., 'user.address.city').

Key Requirements:

  • The matcher should return an object with pass (boolean) and message (function) properties, as expected by Jest's custom matcher API.
  • The message function should generate clear and informative error messages for both passing and failing assertions.
  • Consider edge cases such as null or undefined objects, non-existent nested properties, and properties with undefined values.

Expected Behavior:

  • expect(obj).toHaveProperty('prop') should pass if obj has a property named 'prop'.
  • expect(obj).toHaveProperty('prop', value) should pass if obj has a property named 'prop' and obj.prop strictly equals value.
  • expect(obj).toHaveProperty('user.address.city') should pass if obj has a nested property user.address.city.
  • expect(obj).toHaveProperty('user.address.city', 'New York') should pass if the nested property user.address.city exists and its value is 'New York'.

Examples

Example 1:

const obj = {
  name: 'Hone',
  age: 30,
  address: {
    street: '123 Main St',
    city: 'Metropolis'
  }
};

// Assertion 1: Check for property existence
expect(obj).toHaveProperty('name');
// Expected: pass

// Assertion 2: Check for property existence and value
expect(obj).toHaveProperty('age', 30);
// Expected: pass

// Assertion 3: Check for non-existent property
expect(obj).not.toHaveProperty('email');
// Expected: pass

Explanation: The object obj clearly has a name property and an age property with the value 30. It does not have an email property.

Example 2:

const obj = {
  user: {
    profile: {
      firstName: 'Alice',
      lastName: 'Smith'
    }
  }
};

// Assertion 1: Check for nested property existence
expect(obj).toHaveProperty('user.profile.firstName');
// Expected: pass

// Assertion 2: Check for nested property existence and value
expect(obj).toHaveProperty('user.profile.lastName', 'Smith');
// Expected: pass

// Assertion 3: Check for incorrect nested value
expect(obj).toHaveProperty('user.profile.firstName', 'Bob');
// Expected: fail (message should indicate value mismatch)

Explanation: The object has the nested property user.profile.firstName and user.profile.lastName with the expected values. The assertion for firstName with value 'Bob' fails because the actual value is 'Alice'.

Example 3: Edge Cases

const obj1 = null;
const obj2 = { prop: undefined };
const obj3 = { nested: { prop: null } };

// Assertion 1: Null object
expect(obj1).not.toHaveProperty('someProp');
// Expected: pass (message should indicate null object)

// Assertion 2: Property with undefined value
expect(obj2).toHaveProperty('prop', undefined);
// Expected: pass

// Assertion 3: Nested property with null value
expect(obj3).toHaveProperty('nested.prop', null);
// Expected: pass

// Assertion 4: Non-existent nested property
expect(obj3).not.toHaveProperty('nested.otherProp');
// Expected: pass

Explanation: These examples demonstrate how the matcher should handle null objects, properties that explicitly hold undefined or null values, and non-existent nested properties.

Constraints

  • The implementation should be written in TypeScript.
  • The solution must adhere to Jest's custom matcher API signature.
  • The matcher should correctly handle property keys that are strings or Symbols.
  • Performance should be reasonable for typical object sizes; no extreme optimizations are required, but avoid O(n^2) or worse complexity for property access.
  • The solution should be runnable within a Jest testing environment.

Notes

  • You will need to register your custom matcher using Jest's expect.extend function.
  • Consider how you will parse the dot notation for nested properties. A simple split by '.' should suffice for most cases.
  • When checking for nested properties, ensure that intermediate properties in the path are not null or undefined before attempting to access subsequent properties.
  • The message function should provide clear, actionable feedback to the user. For example, if a property doesn't exist, state that. If the value doesn't match, show both the expected and received values.
  • You can leverage Object.prototype.hasOwnProperty.call for robust property checking, especially to distinguish between own properties and inherited properties if that becomes a requirement, although for this challenge, a direct property access check (in operator or obj[key] !== undefined) might be sufficient for existence. For value checking, direct access is necessary.
Loading editor...
typescript