-
-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
undefined object keys not obvious #252
Comments
nvm, will close till i have a reproduction |
it('should support undefined values', () => {
const expectation = new StrongExpectation('bar', [{}], { value: undefined });
expect(expectation.matches([{key: undefined}])).toBeTruthy();
}); this upcoming flag in typescript 4.4 explains the issue more: |
this is tough because one isn't right or wrong, the current behavior of those two arguments not matching is technically correct, and while most of the time, i think a user would expect this not to be the case, for someone who does have code that depends on the existence of keys, comparing objects with undefined values removed could break things for someone else. the out of curiosity, I created a test to see what straight jest does, and this passes: it('test', () => {
expect({}).toEqual({ key: undefined });
}); so given that the jest utils AND jest expect behavior seems to line up, we'll need to either a) adjust our isEqual implementation to something that matches the jest behavior, or use a non-jest print method to mimick the lodash isEqual implementation |
another interesting thing, it('test', () => {
expect({}).toMatchObject({ key: undefined });
}); |
@parisholley there are 2 issues described here:
const fn = mock<(x: any) => boolean>();
when(fn({})).thenReturn(true);
expect(instance(fn)({ key: undefined })).toBeTruthy(); Error: Didn't expect mock({"key": undefined}) to be called.
Remaining unmet expectations:
- when(mock({})).thenReturn(true).between(1, 1)
const fn = mock<(x: any) => boolean>();
when(fn(It.isObject({}))).thenReturn(true);
expect(instance(fn)({ key: undefined })).toBeTruthy();
|
@NiGhTTraX grrr... sorry, i still have my circular dependency patch w/r/t to |
@parisholley AFAICT only jest ignores Given that jest is the outlier here, I'd keep the default behavior as is and introduce a matcher that ignores |
@NiGhTTraX that sandbox isn't comparing apples to oranges... given that strong-mock is using lodash isEqual (which is deep, not shallow), the sandbox should reflect deep matching behavior. also note the test behavior will change if you make both values the same (eg: {} vs {})) const expected = {nested: {}};
const actual = {nested: {key: undefined}}; // shallow (fails)
assert.equal(actual, expected);
// deep (fails)
assert.deepEquall(actual, expected); // strict equals
chai.expect(actual).to.equal(expected);
// deep (fails)
chai.expect(actual).to.deep.include(expected); // strict
expect(spy.calledWith(expected)).toBeTruthy();
// deep (passes)
expect(spy.calledWith(sinon.match(expected))).toBeTruthy(); there is no consistency as to how any of the libs behave, some deal with undefined keys, others do not. though if we consider jest an outlier, then what is consistent, is that no library deep equals by default (strong mock does). if we were to look at it from the angle of, excluding pure assertion libraries and only focusing on spy implementations, jest and sinon are consistent (they support undefined values when deep matching). given that strong-mock isn't intended to be an assertion/test library, i believe following their behavior would make more sense. |
sinon's All object comparisons have to be deep otherwise you would just compare references. Comparing While It seems that the jest team wishes for strict equal to be the default jestjs/jest#8588 (comment), but they can't change it for risk of breaking everything jestjs/jest#6032 (comment). The TS article you linked reinforces the need of treating an |
correct, also agree that treating undefined should be default when doing object comparisons, however my disagreement is that deep comparison is the API default of strong mock. IMO, mocking assertions should not care about the interior mutability of an object unless the users wants that behavior. my general experience across strictly-typed languages (eg: java + mockito), the default behavior of all equals() methods is effectively strict unless the user wants to override that behavior. the pitch (as i understand it) of strong mock is "strict mocking", which is a reflection of both verification behavior and explicitness, allowing "lose" matches by default is the opposite and counter-intuitive (and traversing proxies could also be problematic)/ if i pass a stateful object to a mocked method as an argument, similar to the jest team's preferences, it should be strict equal unless you state otherwise. while this project is still in its infancy, i'd say now is the time to rip off that bandaid :p |
@parisholley I'm not sure I understand your last comment, could you please clarify some things?
Treating how?
How does this relate to undefined keys?
Correct, in Java/Mockito comparisons would be done by reference. However, using
What do you mean by interior mutability?
What do you mean by lose? The way I see it, treating a missing key and a key with an It seems like we're discussing 2 separate things now — comparing missing keys vs "undefined" keys, and comparing non primitives in general. Can we please limit this issue to the undefined keys topic, and open another issue to discuss the deep comparison topic? Also, can you please clarify exactly what you expect to happen for both of them? |
i meant that if there was an explicit opt-in object comparison matcher, etc
it relates in that, if a non-strict equals API is intended to make the library "more practical"/"easier to use", then this "javascript-friendly" approach should also assume that most cases will not care about undefined keys (json serialization, http parameters, etc).
IMO, as they should. my point is that for a "strict" mocking library, they should fail, implicit object comparisons are always bound to have edge cases (eg: if the object is a proxy being traversed unexpectedly) and wouldn't be intuitive at first glance. this library is making a baseline assumption that "objects" are the most common arguments outside of primitives, but in a more robust application (eg: integration tests), it is the opposite. perhaps a quicker solution/thought to this point is, using something like
class StatefulObject {
private state = 1;
mutate(){
this.state += 1;
}
}
const obj = new StatefulObject();
when(mock.call(obj)); i recalled having an issue where internal state had changed during a test and caused an issue, but i'm not able to reproduce it (may have been a byproduct of some of our earlier discussions/bugs). digging through the library, it doesn't appear to "snapshot" data anywhere, so it may not show up in practice. but my point was, i should be able to call
sorry typo, "loose" = non-strict equals.
I agree. to recap my sentiment... non-strict equals is IMO, loose.. and therefor, comparing two objects with undefined keys, is also intuitively loose. having deep equals but strict key matching (which isn't the same as partial matching as with
to an extent. the potential solution to either are intertwined so i think it is easier to talk about it together. to sum up what I said above, i feel like these are some potential solutions: Breaking API
Breaking Matcher
Magic Path
|
Take the following example: type Foo = { bar: string };
type Cb = (x: Foo) => void;
const foo = (cb: Cb) => {
cb({ bar: "baz" });
};
const cb = mock<Cb>();
when(cb({ bar: "baz" })).thenReturn();
foo(instance(cb)); The above test will not pass using a
The assumption is that for the majority of the cases where you have to deal with objects you won't expect the exact same object that you passed to your code under test e.g.: const o = { foo: "bar" };
when(cb(o)).thenReturn();
instance(cb)(o); // pass the same object here I consider the above pattern rare, and would introduce an
The examples you mentioned are indeed compelling. To solve both issues discussed here, and to offer more flexibility to the user, I'll
|
i would say it is ONLY common, if the argument is a primitive object. if that argument is a class instance (or any native object type provided by the javascript engine (browser or node)), i don't think anyone could make a strong case that a matcher other than i would bet you could take the
perhaps it would help to share the cases you speak of? while understandably, it sucks for tests to be verbose, the reward is in preventing future regressions that pass tests and type checks. while you would hope that experienced engineers are writing code and can understand when to add strict equals checks, the library is putting risk in developers knowing best practices. here are some examples of how an inexperienced engineer can cause problems: Service Orchestration Original Implementation function delegator(){
const result1 = this.serviceA.invoke();
const result2 = this.serviceB.invoke(result1);
} Regression const symbol = Symbol('foo');
function delegator(){
const result1 = this.serviceA.invoke();
const mutated = {...result1, [symbol]: 'bar'};
const result2 = this.serviceB.invoke(mutated);
} Service B may actually iterate the object in a way that recognizes symbols, but the engineer may not know that. The lodash Transactions Original Implementation function delegator(tx:Transaction){
this.serviceA.invoke(tx);
this.serviceB.invoke(tx);
} Regression function delegator(tx:Transaction){
this.serviceA.invoke(tx);
const newTx = this.newTransaction();
this.serviceB.invoke(newTx);
} Because the Derived Arguments I think this is the case you believe to be the most common, where the delegation method is generating objects on your behalf and you want to assert those arguments. There isn't really any regression potential in this implementation. And depending on the project, you may be right, this is the most common, but there is no production risk. function delegator(){
const internalObject = {...};
this.thirdPartyApi.invoke(internalObject);
}
that would be great :) on my end, i would definitely take advantage of setting the default matcher to either way, thanks for considering my arguments |
It does compare symbol keys, tests added in 2029128.
I see your point here, though I think in practice a class like that will have some enumerable key that will be unique per instance. However, it's entirely conceivable that it wouldn't, leaving it up to the user to opt in to a stricter matcher. With the latest release this should be easy. @parisholley please check out release 7.3.0 and let me know if it's flexible enough for you. I cherry picked your changes from #257 and released them under a new |
bah your right, i missed his comment of the patch here: lodash/lodash#2840 @NiGhTTraX implementation wise, looks good :) |
got it do what I need :) fyi, this is the custom matcher I am using as a hybrid approach i mentioned before const isClassInstance = (arg) =>
typeof arg === 'object' &&
!Array.isArray(arg) &&
arg !== null &&
arg.constructor.name !== 'Object' &&
typeof arg !== 'string' &&
typeof arg !== 'number';
const matcher = (expected) =>
strong.It.matches(
(arg) => {
if (isClassInstance(expected)) {
if (expected.constructor.name === 'Big') {
if (!arg || arg.constructor.name !== 'Big') {
return false;
}
return expected.eq(arg);
}
return strong.It.is(expected).matches();
}
return strong.It.deepEquals(expected).matches(arg);
},
{
toJSON: () => {
if (isClassInstance(expected)) {
return strong.It.is(expected).toJSON();
}
return strong.It.deepEquals(expected).toJSON();
},
}
);
strong.setDefaults({ matcher }); |
if you mock with an argument of
{}
and the value{foo: undefined}
is used as an value, when the exception is throwing comparing the expected mock argument with actual value, it will not show{foo: undefined}
, just{}
, making it hard to know what the problem is right awayThe text was updated successfully, but these errors were encountered: