Search code examples
javascriptmockingprototypees6-proxy

Mocking an async builder pattern API with proxy


I'm trying to mock knex for testing. Everything seems to work but attaching a proxy to an array as prototype seems to remove the iteratability of arrays.

Here is the mock function. Following works fine with objects.

const mock = (data) => {
    const orgData = structuredClone(data)

    Object.setPrototypeOf(
        data,
        new Proxy(
            {},
            {
                get(_, prop) {
                    if (prop === 'then') {
                        return orgData
                    }

                    if (typeof prop === 'symbol') {
                        return orgData
                    }

                    return () => mock(orgData)
                },
            }
        )
    )

    return data
}
const mm = mock([
    {
        name: 'user',
    },
])

const run = async () => {
    const res = await mm.select('*').where('name', 'user')
    const [first] = res
    //             ^^^^^
    // TypeError: res is not iterable
    //     at run (.../test.js:35:18)
}
run()

When there is await present before the chain, proxy get is called with then as the value of prop. And that's the last call. That returns [ { name: 'user' } ] correctly. However the value that's assigned to the res variable is looks like this { '0': { name: 'user' } }. I'm wondering if it's possible to assign a prototype to an array without making it an object.


Solution

  • I'm the same guy who posted this question. The resolution was not to set the prototype at all and instead, directly proxy the promise itself.

    const mock = (data) => {
          return new Proxy(Promise.resolve(data), {
                get(target, prop) {
                      if (prop in target) {
                            const promise = Promise.resolve(target)
                            // NOTE: when `then` is called independently, this will be global so it
                            // wouldn't be resolved correctly. So here binding the context to the
                            // function to make it work
                            return promise.then.bind(promise)
                      }
    
                      return () => mock(target)
                },
          })
    }
    
    const mm = mock([
          {
                name: 'user',
          },
    ])
    
    const run = async () => {
          const res = await await mm.select('*').where('name', 'user')
          console.log(res)
          const [first] = res
          console.log(first)
    }
    
    run()
    

    Output:

    :!node test.js
    [ { name: 'user' } ]
    { name: 'user' }