Skip to content
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

test: refactor expectsError #14058

Closed
wants to merge 1 commit into from

Conversation

BridgeAR
Copy link
Member

@BridgeAR BridgeAR commented Jul 3, 2017

So far common.expectsError only tested type, message and code instead of all properties.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

test

@nodejs-github-bot nodejs-github-bot added the test Issues and PRs related to the tests. label Jul 3, 2017
@tniessen
Copy link
Member

tniessen commented Jul 3, 2017

Where do we use this behavior?

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 3, 2017

@tniessen every error thrown by assert have a couple more properties and currently they are tested only for assert.fail. In general I think it makes sense to test for everything that is requested for and not only for some of these properties.

@DavidCai1111
Copy link
Member

assert.strictEqual(error.message, message);
}
} else {
assert.strictEqual(error[key], options[key]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this needs a custom message The ${key} field of ${error} was ${error[key]} and not {options[key]} as expected (or a little bit shorter)
Otherwise we get an AssertionError: a !== b thrown from deep within the harness.
So the same applies for L716 and the message strings.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assert.strictEqual(error.message, message);
}
Object.keys(options).forEach((key) => {
if (key === 'type') {
Copy link
Contributor

@refack refack Jul 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a switch look better?
New code looks real pretty 💅

assert(error instanceof type,
`${error} is not the expected type ${type}`);
}
} else if (key === 'message') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep the if/else change to (key === 'message' && options.message instanceof RegExp) and let the string case fall to the else

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm (not strongly) -1 on this change. I don't think every single property should match the options.

It complicates the code a little and I'm not sure this is a good idea.

@lpinca
Copy link
Member

lpinca commented Jul 5, 2017

Well I guess the point is to make it flexible. Instead of forcing at max code, type, and message it optionally allows to match other properties. I find it useful.

@benjamingr
Copy link
Member

I understand the point and appreciate the effort - but I think it makes the code more confusing for no good reason at the moment - and I can see the pitfalls of tests failing after an object is passed and a property is added unintentionally.

If it was called .expects and not .expectsError or just deepEqual I would understand.

@lpinca
Copy link
Member

lpinca commented Jul 5, 2017

I can see the pitfalls of tests failing after an object is passed and a property is added unintentionally.

It will fail also as is with unintentionally added properties (code or type) to the input object :)

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 5, 2017

@benjamingr I ran into this with another PR as I wanted to test additional properties than just code, type and message and I expected a wrong value in one of the properties and didn't realize that at first as the test passed even though it shouldn't have.

You have to explicitly list every property that you want to test for and if you do not want to test any besides e.g. code and type it'll work as expected:

const err = new Error('foobar')
err.code = 'RANDOM'
err.additional = true
err.foobar = 'is ignored'

// Tests only code
common.expectsError({
  code: 'RANDOM'
})(err)

// Tests other properties
common.expectsError({
  code: 'RANDOM',
  additional: true
  type: Error
})(err)

Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@refack
Copy link
Contributor

refack commented Jul 5, 2017

I understand the point and appreciate the effort - but I think it makes the code more confusing for no good reason at the moment - and I can see the pitfalls of tests failing after an object is passed and a property is added unintentionally.

As I see it the crux of the issue is that the new NodeErrors are "internal" and we can't easily generate them in test code, so we need a helper to generate mock Errors. So as I see it this helper needs to generate/validate that the expected Errors are stable since they are API, so the unintentionally case is a pro, not a con.

The main problem with the current code is the destructuring of the formal agrument: exports.expectsError = function expectsError({code, type, message}) { that silently swallows extra properties...
So we either go with this PR — or validate that the passed object contains only {code, type, message}

P.S. @benjamingr IMHO the new version of the code is almost as succinct as the code before... and fixes the problem of unuseful messages.. so⚖️

Ref: #13686

@refack
Copy link
Contributor

refack commented Jul 5, 2017

Also relevant #13937 if message change are semver-major so should changes to all other properties of thrown Errors

const actual = error[key];
const expected = options[key];
if (key === 'type') {
if (expected !== undefined) {
Copy link
Contributor

@refack refack Jul 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it was called .expects and not .expectsError or just deepEqual I would understand.

That made me think, remove the if and that will make options.type mandatory.
Also add assert(expected instanceof Error) if (!(expected instanceof Error)) throw new TypeError('`options.type` must inherit from `Error`');

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does not work as the class is passed to the function and not the instance. The simplest check I could though of is: expected !== Error && !Error.isPrototypeOf(expected)

@refack
Copy link
Contributor

refack commented Jul 5, 2017

@BridgeAR I don't want to mix 🍎s and 🍊s, but we should actually add a validation that code is passed (re:#13686 (comment)).. so maybe that should go into a new PR?
Also we still need to convince @benjamingr this is a good and important improvement, and need to get consensus that expectsError is only to be used to check for NodeErrors (or at least Error's with an assinged code).

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 9, 2017

Rebased due to conflicts and I updated the documentation as I missed that.

@benjamingr would you mind having another look?

@benjamingr
Copy link
Member

benjamingr commented Jul 9, 2017

I'm fine with asserting that it's an instance of an error - but I'm not sure about testing all properties.

I'm concerned about cases where a "dirty" error object would be passed and it would verify incorrect things by mistake. I'm not sure I see the value this adds to be honest.

The method is already a little footgunny for verifying things by convention - going over all object properties and treating some of them differently doesn't sound like good design to me.

@BridgeAR
Copy link
Member Author

BridgeAR commented Jul 9, 2017

I'm not sure I understand what you mean with the "dirty" error object. Do you mean the common.expectsError settings argument or the error passed to the returned function?

As you can see there is no change needed for any test and the function will only test properties explicitly asked for in the settings object. How could that verify incorrect things by mistake?

For me the current situation was a minor footgun by not testing properties that I wanted it to test for.

@benjamingr
Copy link
Member

I'm not sure I understand what you mean with the "dirty" error object. Do you mean the common.expectsError settings argument or the error passed to the returned function?

I mean someone passing a TypeError that has a property added to it by mistake or something similar.

@refack
Copy link
Contributor

refack commented Jul 9, 2017

The method is already a little footgunny for verifying things by convention - going over all object properties and treating some of them differently doesn't sound like good design to me.

Ack. Probably the best thing would be to add --expose-internals to all tests and validate against the real deal + Have 1 test file that tests the proper construction of all the errors.
Currently as I see it there is as issue that the NodeErrors have other properties that we do not assert the stability of, and thay are in effect part of the public API that we don't tests for regressions.
Maybe a good compromise would be not to use the keys of expected but to iterate on a const set — for Error, TypeError, and RangeError it's [message, name, code] and for AssertionError there's also [generatedMessage, name, code, actual, expected, operator] and that stackStartFunction is not in .stack.

@refack refack self-assigned this Jul 9, 2017
* `type` [<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)
expected error must be an instance of `type`
expected error must be an instance of `type` and should be [subclassed by
the `internal/errors` module](https://github.com/nodejs/node/blob/master/doc/guides/using-internal-errors.md#api).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This additional bit subclassed by the 'internal/errors' module doesn't really make sense. The thing must be an instance of type and must be an Error subclass. Whether it is a subclass of one of the internal/errors.js classes is somewhat inconsequential.


The expected error should be [subclassed by the `internal/errors` module](https://github.com/nodejs/node/blob/master/doc/guides/using-internal-errors.md#api).
* return [<Function>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)
If `fn` is provided, it will be passed to `assert.throws` as first argument
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a blank line after the * return line... and it should be * Returns:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The * Returns: would be inconsistent with all other entries in this file. Shall I change all other entries as well?

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New version not using Object.keys looks good to me

@BridgeAR
Copy link
Member Author

I completely reworked the code as suggested by @refack
IMHO this is not as nice as it was before but I hope this is a good compromise.

@refack
Copy link
Contributor

refack commented Jul 11, 2017

New version not using Object.keys looks good to me

I completely reworked the code as suggested by @refack
IMHO this is not as nice as it was before but I hope this is a good compromise.

I had this whole spiel about flattening error and settings then using deepStrinctEqual, but that should be a new PR...


  1. use the code here to generate a mock Error that could be compared
  2. mock should have:
    1. const keys = [name, message];
    2. If (settings.Type.name === 'AssertionError') keys.push(...['generatedMessage', 'actual', 'expected', 'operator']);
    3. const mockError = keys.reduce((s,k) => Object.assign(k, {[k]: settings[k]}), {});
  3. error could be flattened with const flatActual = keys.reduce((s,k) => Object.assign(k, {[k]: error[k]}), {});
  4. const name = `${settings.type.name} [${settings.code]}]`; - https://github.com/nodejs/node/blob/master/lib/internal/errors.js#L34
  5. two properties will need special treatment (but IMHO that should go into assert somehow):
    1. mockError.stack = (s) => s.includes(settings.stackExcludes) === false;
    2. mockError.message = (s) => (settings.message instanceof RegEx ? settings.message.test(s) : settings.message === s;
  6. We don't have to compare Type because it's embedded in name, just that error instanceof Error
  7. finally: assert.deepStrinctEqual(flatActual, flatExpected);

@BridgeAR
Copy link
Member Author

Rebased. @refack could this land or do you want me to still change something?

@refack
Copy link
Contributor

refack commented Jul 19, 2017

Didn't see that the CR was dismissed.
Pre-land CI: https://ci.nodejs.org/job/node-test-commit/11255/

refack pushed a commit to refack/node that referenced this pull request Jul 19, 2017
PR-URL: nodejs#14058
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gibson Fahnestock <gibfahn@gmail.com>
@refack
Copy link
Contributor

refack commented Jul 19, 2017

Landed in 2a621d4

@refack refack closed this Jul 19, 2017
@refack
Copy link
Contributor

refack commented Jul 19, 2017

Extra sanity run on master: https://ci.nodejs.org/job/node-test-commit-linuxone/7431/

@MylesBorins
Copy link
Contributor

Should this be backported to v6.x-staging? If yes please follow the guide and raise a backport PR, if not let me know or add the dont-land-on label.

@BridgeAR
Copy link
Member Author

This function does not exist in v.6 so backporting is obsolete.

@refack refack removed their assignment Oct 12, 2018
@BridgeAR BridgeAR deleted the refactor-expects-error branch April 1, 2019 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
test Issues and PRs related to the tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.