SkillByAIOpen interactive version →

Lesson 5 / 25

Choosing the Right Matcher

toBe, toEqual, toThrow, toMatchObject and asymmetric matchers.

Identity, structure and partial matches

toBe uses Object.is, so it suits primitives and checking that two references are the same object. toEqual compares recursively by value and ignores properties that are undefined; toStrictEqual also checks undefined properties, sparse arrays and class types. toMatchObject passes if the received object contains the expected subset. toThrow needs a function to call: expect(() => fn()).toThrow('message'). Asymmetric matchers like expect.any(String), expect.stringContaining, expect.objectContaining and expect.arrayContaining can sit inside other expectations to ignore values you do not care about, such as generated ids or timestamps. Use toBeCloseTo for floating-point maths and .not to negate.

Matchers side by side

Each line passes.

const user = { id: 'u_81f2', name: 'Asha', roles: ['admin', 'editor'], createdAt: new Date() };

expect(2 + 2).toBe(4);
expect({ a: 1 }).toEqual({ a: 1 });            // same shape, different objects
expect({ a: 1 }).not.toBe({ a: 1 });           // ...but not the same reference
expect({ a: 1, b: undefined }).toEqual({ a: 1 });
expect({ a: 1, b: undefined }).not.toStrictEqual({ a: 1 });

expect(user).toMatchObject({ name: 'Asha' });   // subset is enough
expect(user).toEqual({
  id: expect.stringMatching(/^u_/),
  name: 'Asha',
  roles: expect.arrayContaining(['admin']),
  createdAt: expect.any(Date),
});

expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(() => JSON.parse('{oops')).toThrow(SyntaxError);

Assert on what matters, loosen the rest

Over-specific assertions (exact timestamps, full objects with dozens of fields) break on harmless changes. Pin the fields the behaviour depends on and use asymmetric matchers for the rest.

Quick check: Why does expect(fn()).toThrow() usually fail to catch the error?

  • toThrow only works with async code
  • fn() runs and throws before expect receives anything; toThrow needs a function to call
  • toThrow only accepts strings
  • Errors are always swallowed by the runner
Answer

fn() runs and throws before expect receives anything; toThrow needs a function to call — Wrap the call: expect(() => fn()).toThrow().