jest-extended
                                
                                 jest-extended copied to clipboard
                                
                                    jest-extended copied to clipboard
                            
                            
                            
                        feature: toContainEqual+toMatchObject
Based on issue: https://github.com/facebook/jest/issues/6588
🚀 Feature Proposal
Find an object in an array that matches like an object.
Motivation
There may be times where you need to match a partial object against something in a returned list.
Example
I am writing tests for an ORM that saves an object, but it adds extra metadata to the objects when they are later retrieved. The order of the list is not deterministic. Therefor I want to match the fetched object list from the DB and test if the origin object I saved matches any of the objects in the list returned from the query.
const list = [ 
  { foo: "bar" , color: "blue"},
  { foo: "bazz" , color: "red"},
]
expect(list).toContainMatch( { color: "blue" } ); // true
expect(list).toContainMatch( { color: "red" } ); // true
expect(list).toContainMatch( { foo: "bar" , color: "blue"} ); // true
expect(list).toContainMatch( { color: "green"} ); // false
I like this, i can see a use case for it,
i've wrote this for context, as I feel the jest matchers names arn't really that clear, and your suggestion would simplify the logic for checking if some element appears in an array at any point. however is toContainMatch clear language? would something like expect(Array).toMatchAtleastOnce(expected) that can take any input for the expected?
describe('test examples', () => {
  it('toContain', () => {
    const example = [
      'some',
      'thing'
    ]
    expect(example).toContain('thing')
  })
  it('toContainEqual', () => {
    const example = [
      {
        some: 'thing'
      }
    ]
    expect(example).toContainEqual({some: 'thing'})
  })
  it('toMatch', () => {
    const example = 'some grapefruit'
    expect(example).toMatch(/grapefruit/)
    expect(example).toMatch(new RegExp('grapefruit'))
  })
  it('toMatchObject', () => {
    const example = {
      some: 'thing',
      addtional: 'thing'
    }
    const expected = {
      some: 'thing'
    }
    expect(example).toMatchObject(expected)
  })
  it('toContainMathch', () => {
    const example = [
      {
        some: 'thing',
        addition: 'otherThing'
      },
      {
        foo: 'bar',
        bar: 'foo'
      }
    ]
    expect(example).toEqual(
      expect.arrayContaining([expect.objectContaining({foo: 'bar'})])
    )
  })
})
So for example, these are the behavious i would expect from such a matcher:
describe('test use case toMatchAtleastOnce', () => {
  it('toMatchAtleastOnce Strings', () => {
    const example = [
      'some',
      'thing'
    ]
    expect(example).toMatchAtleastOnce('thing')
  })
  it('toMatchAtleastOnce Objects', () => {
    const example = [
      {
        addivional: 'value',
        some: 'thing'
      },
      {
        addivional: 'value'
      }
    ]
    expect(example).toMatchAtleastOnce({some: 'thing'})
  })
  it('toMatchAtleastOnce Arrays', () => {
    const example = [
      [
        'some',
        'thing'
      ],
      [
        'additional',
        'entry'
      ]
    ]
    expect(example).toMatchAtleastOnce('thing')
  })
  it('toMatchAtleastOnce Numbers', () => {
    const example = [
      1,
      2,
      3
    ]
    expect(example).toMatchAtleastOnce(2)
  })
})
I don't know how we would deal with this use case though, objects in nested arrays,
  it('toMatchAtleastOnce Objects', () => {
    const example = [
      [
        {
          addivional: 'value',
          some: 'thing'
        },
      ],
      [
        {
          addivional: 'value'
        }
      ]
    ]
    expect(example).toMatchAtleastOnce({some: 'thing'})
  })
from .toMatchAtleastOnce() i can see a case being drawn for a whole raft of matchers:
- toMatchAtleastOnce()
- toMatchExactlyOnce()
- toMatchAtleastTimes()
- toMatchExactlyTimes() / Uses strict equality
- toMatchAtPosition(val, position)
- toMatchFirstElement(val) --> first element of array
- toMatchLastElement(val) --> last elemtent of array / Partially matchs array elements, such as having {some: 'thing'} in a larger object
- toPartiallyMatchAtPosition(val, position)
- toPartiallyMatchFirstElement(val)
- toPartiallyMatchLastElement(val)
I dont know how this would be recieved as i'm not sure if these things exist or not yet.
Perhaps I'm biased here, but I see lists as fundamental primites worthy of having robust testing functions as you mentioned above. I'm using Jest correctly to write tests for a new DB I'm developing, and as you can expect, many of the APIs return lists that need to be verified using a variety of ways, like what @benjaminkay93 mentioned above.
Thats the same use case for me, a step beyond this would be schema testing, maybe we could wrap a an ajv schema testing into a matcher aswell, i would love to be able to do say:
expect(someObject).toMatchSchemaDraft7(someSchema);
Hey guys sorry to take so long to chime in on this!
I'm open to adding a partial matching assertion for lists 👍
Around the naming conventions, I would say that it would be better to be closer to the current naming style for Array's with .toInclude as the prefix.
Off the top of my head I think I would suggest something like:
.toIncludePartial or .toIncludePartialMatch
The way I see it is you can only perform a partial match on:
- Object
- Array
- Set
- Map
- String
and not on: boolean, number, Date, Symbol, null, undefined
test('matches partially object', () => {
  const listObjs = [ 
    { foo: "bar" , color: "blue"},
    { foo: "bazz" , color: "red"},
  ]
  expect(listObjs).toIncludePartial({ color: "blue" }); // true
})
test('matches partially string', () => {
  const listStrings = ["blue", "red"]
  expect(listStrings).toIncludePartial("bl"); // true
})
test('matches partially array', () => {
  const listArrays = [ 
    ['bar', 'blue'],
    ['bazz', 'red']
  ]
   expect(listArrays).toIncludePartial(["blue"]); // true
})
test('matches partially map', () => {
  const listMaps = [ 
    new Map([['foo', 'bar'], ['color', 'blue']]),
    new Map([['foo', 'bazz'], ['color', 'red']])
  ]
  expect(listMaps).toIncludePartial(new Map([['color', 'blue']]); // true
})
test('matches partially set', () => {
  const listSet = [ 
    new Set(['bar', 'blue']),
    new Set(['bazz', 'red'])
  ]
  expect(listSet).toIncludePartial(new Set('blue')); // true
})
My question is how deep does the partial match go and how do you specify the structure?
What if you had something like: List<List<Map<String, Set<Object>>>
const list = [
  [
    new Map([
      [
        'key1',
        new Set([
          { foo: 'bar', color: 'blue' },
          { foo: 'bazz', color: 'red' }
        ])
      ],
      ['key2', new Set()]
    ])
  ]
]
// should this work? i.e. traversing multiple data types
expect(list).toIncludePartial({ color: 'blue'}) 
// or do you have to specify the types?
expect(list).toIncludePartial([new Map([['key1', new Set([{ color: 'blue'}]) ]])]) 
@mattphillips For how deep the matches go for mixed collection types, I don't have a strong opinion on. I'd probably opt for expect(list).toIncludePartial({ color: 'blue'}) since we can assume I'm not trying to validate against types but rather my intent it to just check "hey, is this value matching something in this collection?".
As @benjaminkay93 mentioned, it would be useful to be also able to specify how many times the value gets matched. Does this value match to something in the collection 'only once', 'at least once, 'twice', etc?
expect(list).toIncludePartial({ color: 'blue'}, [1]) // match exactly once
expect(list).toIncludePartial({ color: 'blue'}, [0,1]) // match exactly zero or one
expect(list).toIncludePartial({ color: 'blue'}, [1,-1]) // match one or more times (default)
expect(list).toIncludePartial({ color: 'blue'})  // same as above
maybe we give the option to include a predicate, then people can write their own thing to check these things with a passed in function?
@benjaminkay93 we already have .toSatisfyAll which takes a predicate :smile:
@jadbox I think .toIncludePartial(value) and .toIncludePartialTimes(value, times) is probably sufficient.
@mattphillips actually, ya... .toIncludePartialTimes(value, times) would be sufficient.
@mattphillips any update on toIncludePartial and toIncludePartialTimes? Looking at the docs today (it's been some time), I don't see any additions to aid this use-case. toIncludeAnyMembers isn't quite close enough as it cannot match partial objects within the collection.
I have been facing the same issue and have come up with the following solution:
import matchers from 'expect/build/matchers'; // The matchers used internally by Jest
expect.extend({
  /**
   * Deep-match an array of objects against another object. (= Basically, run
   * expect().toMatchObject() in a loop and declare the test to have passed if
   * any object matches the expected one.)
   */
  toContainObjectMatching(received, expected: Record<string, unknown>) {
    let matchFound = false;
    if (Array.isArray(received)) {
      matchFound = 
        received.map((obj) => {
          const result = matchers.toMatchObject.call(this, obj, expected);
          // For some reason TSC doesn't fully recognize result as object of type
          // ExpectationResult
          return (result as any).pass;
        }).reduce((a, b) => a || b, false);
    }
    const prettyPrinted = {
      received: this.utils.printReceived(received),
      expected: this.utils.printExpected(expected),
    };
    return {
      message: () =>
        this.isNot
          ? `expected ${prettyPrinted.received} to be list containing no object matching ${prettyPrinted.expected}`
          : `expected ${prettyPrinted.received} to be list containing at least one object matching ${prettyPrinted.expected}`,
      pass: matchFound,
    };
  },
});
declare global {
  namespace jest {
    interface Matchers<R> {
      toContainObjectMatching(expected: Record<string, unknown>): R;
    }
  }
}
Usage: Add the above file to the setupFilesAfterEnv setting in jest.config.ts. Then in your tests do
expect([
  { foo: 'foo' },
  { bar: 'bar' },
]).toContainObjectMatching({
  foo: 'foo'
});