1

I have this helper function in my app that tells me the changes of newData when compared to oldData.

How can I refactor my getChanges function to make the test below pass? I thought I may need to make this function recursive since it executes itself from within itself, but I am not totally sure how to implement that.

It looks like this:

getChanges helper function:

export function getChanges(oldData: Record<string, any>, newData: Record<string, any>): any {
  
  return Object.entries(newData).reduce((changes, [key, newVal]) => {

    if (JSON.stringify(oldData[key]) === JSON.stringify(newVal)) return changes
    changes[key] = newVal
    return changes

  }, {} as any)
}

During my actual tests I use ava's deepEqual to help make the comparison. For whatever reason though, one of the tests that I am running does not pass.

index.ts test 1 passes

import test from 'ava'
import { getChanges } from '../src/comparisonHelpers.js'

test('getChanges - flat', (t) => {
  const a = getChanges({}, {})
  const b = {}
  t.deepEqual(a, b)

  t.deepEqual(getChanges({ a: 1 }, { a: 1 }), {})
  t.deepEqual(getChanges({ a: 1 }, {}), {})
  t.deepEqual(getChanges({}, { a: 1 }), { a: 1 })

  const oldData = { a: 1, b: 1, c: 1 }
  const newData = { x: 1, a: 1, b: 2 }
  const result = getChanges(oldData, newData)
  const expect = { x: 1, b: 2 }
  t.deepEqual(result, expect)
})

index.ts test 2 does not pass

import test from 'ava'
import { getChanges } from '../src/comparisonHelpers.js'

test('getChanges - nested difference', (t) => {
  const oldData = { nested: { a: 1, b: 1, c: 1 } }
  const newData = { nested: { x: 1, a: 1, b: 2 } }
  const res = getChanges(oldData, newData)
  t.deepEqual(res, { nested: { x: 1, b: 2 } })
})

Basically, I expect nothing to be returned if the test passes, but this test returns this object upon failure:

{
      nested: {
  -     a: 1,
        b: 2,
        x: 1,
      },
    }

What am I doing wrong here that is stopping this test from passing?

Cheers!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
LovelyAndy
  • 841
  • 8
  • 22
  • Where does your expected value come from? Shouldn't it contain something like `c: undefined`? – Scott Sauyet Mar 31 '21 at 15:31
  • I think you would benefit from [this Q&A](https://stackoverflow.com/a/33233053/633183). Let me know if you have any questions. – Mulan Mar 31 '21 at 15:43
  • @ScottSauyet Thank you for the reply! Right now these tests are totally isolated to the files they are in. Right now the expected value is the second value in the getChanges function (see my added 'test 1'). Test 1 breaks things down a bit better as it's just looking for the difference btween the two. I hope the edit makes more sense than my explanation – LovelyAndy Mar 31 '21 at 15:44
  • @Thankyou: I was *looking* for that answer. Didn't find it and just wrote a first pass at my own. Still too ugly to share, though. – Scott Sauyet Mar 31 '21 at 15:52
  • @LovelyAndy: I was trying to figure out why there is no `c` expected in your output? It's in the first value and not the second. I would expect something like `{nested: {x: 1, b: 2, c: undefined}}` or similar. Removing the `c` is certainly a change that should be noticed, no? But do look at the suggestion from Thankyou for a more robust style diff. – Scott Sauyet Mar 31 '21 at 15:55
  • @ScottSauyet Firstly, thank you for that Q&A. I am still fairly new to programming, especially testing, so this type of dense information is great! Second, you are totally right; wow. I'm really not sure why that doesn't get displayed in the failure output.... – LovelyAndy Mar 31 '21 at 15:57
  • The point is, who wrote the test case? It seems to be expecting something odd. Is this a test case from an assignment, or a real test for your own work that you wrote yourself? What would you expect to happen when there is a property in `oldObject` that is not in `newObject`? I was suggesting that you might want to include something like `{... prop: undefined, ...}` in the output in that case, but it's your call. (By the way, that Q + A was not supplied by me but by its author, user Thankyou. It also offers a much more robust diff format than supplied here.) – Scott Sauyet Mar 31 '21 at 16:07
  • @ScottSauyet I've been working on an app with a friend of mine and this was a test ticket he gave me. He wrote the tests to begin with and it's on me to figure out why this test isn't working and how to fix it. I am not familar with any testing outside of cypress.io and don't even know typescript yet. The expected result from a failed test is to print what has changed, in the case of the second test, it would show you x, b and c, ideally. The prop: undefined is a good idea. Recussion was a suggestion as a possible solution from my friend. – LovelyAndy Mar 31 '21 at 16:14
  • @Thankyou My mistake. Thank you for the Q&A! – LovelyAndy Mar 31 '21 at 16:14

1 Answers1

2

Here is an extremely rough first pass at such a function (here named diff rather than getChanges):

const isObject = (o) => 
  Object (o) === o
const isEmptyObject = (o) => 
  isObject(o) && Object .keys (o) .length == 0

const diff = (a, b) => 
  Object .fromEntries (
    [... (new Set ([...Object .keys (a), ...Object.keys(b)]))].flatMap (
      (k) =>
        k in a
          ? k in b
            ? isObject (a [k])
              ? isObject (b [k])
                ? [[k, diff (a [k], b [k])]]  // <--- recursive call here
                : [[k, b [k]]]
              : a[k] == b [k]
                ? []
                : [[k, b [k]]]
            : [[k, undefined]]
          : [[k, b [k]]]
    ) .filter (([k, v]) => !isEmptyObject(v))
  )


const oldData = {nested: { a: 1, b: 1, c: 1 }, foo: {x: 3, y: 5}, bar: {x: 1}, qux: {x: 6}}
const newData = {nested: { x: 1, a: 1, b: 2 }, foo: {x: 4, y: 5}, bar: {x: 1}, corge: {x: 6}}

console .log (diff (oldData, newData))
.as-console-wrapper {max-height: 100% !important; top: 0}

This is very unsophisticated and there are inputs for which it will not work, most notably those which intentionally include an undefined value. It would also by design include the undefined value for keys missing from the new data. But it would be easy to not include them: Just change [[k, undefined]] to [] in the function and that would pass your test cases, I believe.

Note that the answer Thank you (User) suggested uses a much nicer diff format than this: for all keys which have changed, it includes left and right properties to give you the value, skipping those which simply don't exist. This would let you unambiguously replay or revert the diff. With the format here, that won't always work.

There are also in the flow here too many copies of the same output. I'm guessing with some thought, we might be able to reduce the cases involved.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Thank you very much Scott. This is quite a bit more than I initially expected, but honestly helps a ton. I appreciate your time and hope I can get this to work! Thanks again! – LovelyAndy Mar 31 '21 at 16:34
  • nice break down of the shortcomings of this approach. it's a nice answer nonetheless. i haven't been able to test it, but it looks like it would convert arrays to objects too. – Mulan Mar 31 '21 at 19:45
  • 1
    @Thankyou: I haven't tested that either, but I did mean to mention that not explicitly handling arrays was another limitation. – Scott Sauyet Mar 31 '21 at 19:47