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

assert: add new callTracker.callsWith function #43133

Closed

Conversation

ErickWendel
Copy link
Member

@ErickWendel ErickWendel commented May 17, 2022

This PR adds a new callTracker.callsWith function and its docs

@nodejs-github-bot nodejs-github-bot added assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run. labels May 17, 2022
@ErickWendel ErickWendel force-pushed the assert/add-new-callsWith-func branch from 406b2a0 to 8290594 Compare May 17, 2022 22:32
@ErickWendel ErickWendel marked this pull request as draft May 18, 2022 00:30
@ErickWendel ErickWendel force-pushed the assert/add-new-callsWith-func branch 2 times, most recently from cca51af to afcbd38 Compare May 18, 2022 17:53
this also adds the replaceme tag on the docs
@ErickWendel ErickWendel force-pushed the assert/add-new-callsWith-func branch from afcbd38 to 4d56b1c Compare May 18, 2022 17:56
@ErickWendel ErickWendel marked this pull request as ready for review May 18, 2022 17:58
@ErickWendel
Copy link
Member Author

ErickWendel commented Jun 6, 2022

is it ready to go?

@RafaelGSS RafaelGSS added the request-ci Add this label to start a Jenkins CI on a PR. label Jun 6, 2022
@RafaelGSS
Copy link
Member

cc: @nodejs/assert

@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jun 6, 2022
@nodejs-github-bot
Copy link
Collaborator

@@ -322,6 +322,88 @@ function func() {}
const callsfunc = tracker.calls(func);
```

### `tracker.callsWith([fn],withArgs[, exact])`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### `tracker.callsWith([fn],withArgs[, exact])`
### `tracker.callsWith([fn], withArgs[, exact])`

it is very strange to have an optional argument before a required one. can that be reevaluated?

The wrapper function is expected to be called exactly `exact` times and
to be called exactly with `withArgs` arguments.

The `withArgs` will compare whether the function arguments are deep-strict equal
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
The `withArgs` will compare whether the function arguments are deep-strict equal
The `withArgs` will compare whether the function arguments are deep-strict equal.

[`tracker.verify()`][] is called, then [`tracker.verify()`][] will throw an
error.

```mjs
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
```mjs
```js

i don't think mjs is a syntax highlighting mode, this is just JS

if (context.actual === context.exact) {

// Only spy args if requested
if (context.expectedArgs.length)
Copy link
Member

Choose a reason for hiding this comment

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

is this checking if it's truthy, or if it's > 1? a user doesn't have to pass in a number, does it?

Comment on lines +108 to +112
details: ArrayPrototypePush([], {
message,
operator: 'callsWith',
stack: genericNodeError()
})
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
details: ArrayPrototypePush([], {
message,
operator: 'callsWith',
stack: genericNodeError()
})
details: [{
message,
operator: 'callsWith',
stack: genericNodeError()
}]

@tniessen
Copy link
Member

tniessen commented Jun 6, 2022

I don't know if this is a good idea.

  • This is a very specific test that is simple to implement in userland:

    tracker.calls((...args) => assert.deepStrictEqual(args, [1, 2, 3]));
  • As this is written, the user-defined function still cannot rely on the arguments, and error reporting is not ideal:

    function testBuffer(buffer) {
      buffer.slice(0, 10);
    }
    
    const ct = new assert.CallTracker();
    const fn = ct.callsWith(testBuffer, [Buffer.from([1, 2, 3])]);
    // This throws from within testBuffer:
    fn(null);
    // This is never reached, so callsWith() never produces a visible error:
    ct.verify();
  • This is not particularly extendible. What if I want to additionally verify the value of this? What if I want to ignore a specific argument? What if I only want to check the typeof of an argument? What if I want reference equality (instead of deep equality) for a specific argument?

@ErickWendel
Copy link
Member Author

I don't know if this is a good idea.

* This is a very specific test that is simple to implement in userland:
  ```js
  tracker.calls((...args) => assert.deepStrictEqual(args, [1, 2, 3]));
  ```

* As this is written, the user-defined function still cannot rely on the arguments, and error reporting is not ideal:
  ```js
  function testBuffer(buffer) {
    buffer.slice(0, 10);
  }
  
  const ct = new assert.CallTracker();
  const fn = ct.callsWith(testBuffer, [Buffer.from([1, 2, 3])]);
  // This throws from within testBuffer:
  fn(null);
  // This is never reached, so callsWith() never produces a visible error:
  ct.verify();
  ```

* This is not particularly extendible. What if I want to additionally verify the value of `this`? What if I want to ignore a specific argument? What if I only want to check the `typeof` of an argument? What if I want reference equality (instead of deep equality) for a specific argument?

Just a thing, I didn't like either the design of this API so I opened this Issue to make it extendable. I got a green signal to go forward on this implementation so I believe I'd mark this PR as draft until I finish the new design in the next few weeks.

Well, I agree this can be done on the userland however I was wondering to implement something like Sinon Spies does to avoid verbosity and replicated code.

This example you pointed out I believe is something expected. Even using only .calls when an error occurs the .verify results are omitted.

{
  function testBuffer(buffer) {
    buffer.slice(0, 10);
  }
  
  const ct = new assert.CallTracker();
  const fn = ct.calls(testBuffer);
  // This throws from within testBuffer:
  fn(null);
  // This is never reached, so callsWith() never produces a visible error:
  ct.verify();
}

About those questions:

What if I want to additionally verify the value of this? What if I want to ignore a specific argument?

I'm not sure if this is the responsibility of .callsWith. Not sure if Sinon does it as well. I think it's a good idea but I'd put it as an additional option to check / ignore argumeents

What if I only want to check the typeof of an argument?

I would add an option flag to check it but it wasn't planned for the .callsWith

What if I want reference equality (instead of deep equality) for a specific argument?

This is the current behavior. see

const containsExpectArgs = isDeepStrictEqual(context.currentFnArgs, context.expectedArgs);

@ErickWendel
Copy link
Member Author

ErickWendel commented Jun 7, 2022

I'm rolling back this PR to Draft as the #43341 will change the function's signature. As soon as it's landed I'll came back here

@ErickWendel ErickWendel marked this pull request as draft June 7, 2022 13:46
@tniessen
Copy link
Member

tniessen commented Jun 7, 2022

Well, I agree this can be done on the userland however I was wondering to implement something like Sinon Spies does to avoid verbosity and replicated code.

Sinon Spies are a good example of a userland implementation. That being said, Sinon's withArgs() is much more powerful than what is being proposed here, so this proposal is not going to replace Sinon Spies.

This example you pointed out I believe is something expected. Even using only .calls when an error occurs the .verify results are omitted.

Yes, but .calls() makes no guarantees about the arguments, so the callee cannot rely on the arguments or their types.

My understanding is that the entire purpose of callsWith() is to verify the arguments, but if the callee still cannot rely on the arguments despite explicitly using callsWith() to verify the arguments and their types, then this is not particularly helpful.

What if I want to additionally verify the value of this? What if I want to ignore a specific argument?

I'm not sure if this is the responsibility of .callsWith. Not sure if Sinon does it as well. I think it's a good idea but I'd put it as an additional option to check / ignore argumeents

Sinon has spy.alwaysCalledOn(), for example.

What if I only want to check the typeof of an argument?

I would add an option flag to check it but it wasn't planned for the .callsWith

If every additional feature will require additional options, the complexity of this API will explode rapidly.

What if I want reference equality (instead of deep equality) for a specific argument?

This is the current behavior. see

const containsExpectArgs = isDeepStrictEqual(context.currentFnArgs, context.expectedArgs);

I don't understand; I think isDeepStrictEqual() checks for deep equality, not for reference equality.

@cjihrig
Copy link
Contributor

cjihrig commented Jun 7, 2022

@ErickWendel it's totally up to you, but you may want to take a look at cjihrig@e484301. It's proof of concept work for adding mocking to Node's test runner. It probably overlaps with this a bit.

@ErickWendel
Copy link
Member Author

@ErickWendel it's totally up to you, but you may want to take a look at cjihrig@e484301. It's proof of concept work for adding mocking to Node's test runner. It probably overlaps with this a bit.

Uh I liked it! I'm in vacation right now. As soon as I'm back I'll take a look at it

@MoLow
Copy link
Member

MoLow commented Jul 1, 2022

@ErickWendel I think a much more powerful API can be an API exposing an array of calls and their params, this way you can decide what assersions to perform on the arguments,
if for example some of the arguments are not important or the calling order is not important etc.

I thought of something like this

const tracked = callTracker.track(() => {});
assert.strictEqual(tracked.calls.length, 0);
tracked('bar');
assert.strictEqual(tracked.calls.length, 1);
assert.strictEqual(tracked.calls[0], { arguments: ['bar'] });

WDYT?

@ErickWendel
Copy link
Member Author

@ErickWendel I think a much more powerful API can be an API exposing an array of calls and their params, this way you can decide what assersions to perform on the arguments, if for example some of the arguments are not important or the calling order is not important etc.

I thought of something like this

const tracked = callTracker.track(() => {});
assert.strictEqual(tracked.calls.length, 0);
tracked('bar');
assert.strictEqual(tracked.calls.length, 1);
assert.strictEqual(tracked.calls[0], { arguments: ['bar'] });

WDYT?

wooww I loved the idea. I think Sinon does the same thing right?

@MoLow
Copy link
Member

MoLow commented Jul 5, 2022

@ErickWendel I think a much more powerful API can be an API exposing an array of calls and their params, this way you can decide what assersions to perform on the arguments, if for example some of the arguments are not important or the calling order is not important etc.
I thought of something like this

const tracked = callTracker.track(() => {});
assert.strictEqual(tracked.calls.length, 0);
tracked('bar');
assert.strictEqual(tracked.calls.length, 1);
assert.strictEqual(tracked.calls[0], { arguments: ['bar'] });

WDYT?

wooww I loved the idea. I think Sinon does the same thing right?

Cool I implemented some POC MoLow@ecd3ff7

@ErickWendel
Copy link
Member Author

ErickWendel commented Aug 8, 2022

closing this PR as it has another one in progress with the PoC MoLow@ecd3ff7

@ErickWendel ErickWendel closed this Aug 8, 2022
@MoLow
Copy link
Member

MoLow commented Aug 8, 2022

@ErickWendel can you link to the other PR?

@ErickWendel
Copy link
Member Author

@ErickWendel can you link to the other PR?

just linked your PoC and realized it's not a PR, is it?

@MoLow
Copy link
Member

MoLow commented Aug 8, 2022

just linked your PoC and realized it's not a PR, is it?

Ah, I did not want to open a PR before you told me you are ok with it. I will rebase my branch and open a PR

@ErickWendel
Copy link
Member Author

just linked your PoC and realized it's not a PR, is it?

Ah, I did not want to open a PR before you told me you are ok with it. I will rebase my branch and open a PR

oh perfect! Sorry for the mess 😂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants