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:
- Check for property existence: Given an object and a property key (string or symbol), it should assert that the object possesses that property.
- 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.
- 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) andmessage(function) properties, as expected by Jest's custom matcher API. - The
messagefunction should generate clear and informative error messages for both passing and failing assertions. - Consider edge cases such as
nullorundefinedobjects, non-existent nested properties, and properties withundefinedvalues.
Expected Behavior:
expect(obj).toHaveProperty('prop')should pass ifobjhas a property named'prop'.expect(obj).toHaveProperty('prop', value)should pass ifobjhas a property named'prop'andobj.propstrictly equalsvalue.expect(obj).toHaveProperty('user.address.city')should pass ifobjhas a nested propertyuser.address.city.expect(obj).toHaveProperty('user.address.city', 'New York')should pass if the nested propertyuser.address.cityexists 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.extendfunction. - 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
nullorundefinedbefore attempting to access subsequent properties. - The
messagefunction 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.callfor 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 (inoperator orobj[key] !== undefined) might be sufficient for existence. For value checking, direct access is necessary.