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

Feature: a way replace inner function in spy mocking #6234

Open
CyanChanges opened this issue Dec 4, 2024 · 8 comments
Open

Feature: a way replace inner function in spy mocking #6234

CyanChanges opened this issue Dec 4, 2024 · 8 comments
Labels
suggestion a suggestion yet to be agreed testing

Comments

@CyanChanges
Copy link

Is your feature request related to a problem? Please describe.

In an event emitter, I want to replace the handler to throw an error, to verify if the emitter can handle error

Describe the solution you'd like

Like node:test, you can replace the mock implementation with

import { mock } from 'node:test'
mocked_fn = mock.fn()
mocked_fn.mock.mockImplementation(()=>{ /* new logic */ })

Describe alternatives you've considered

make the fn in spy(fn) a wrapper, which call an external function variable.

@kt3k kt3k added testing suggestion a suggestion yet to be agreed labels Dec 5, 2024
@IgorM867
Copy link
Contributor

IgorM867 commented Dec 5, 2024

How exactly would this look like in terms of std? What do you think of something like that?

Deno.test("test", () => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  function throws() {
    throw new Error("error");
  }

  const fn = spy(addOne);

  assertEquals(fn(), 1);
  fn.mockImplementation(addTwo);

  assertEquals(fn(), 3);
  assertEquals(fn(), 5);

  fn.mockImplementation(throws as () => number);
  assertThrows(fn, "error");
});

@CyanChanges
Copy link
Author

CyanChanges commented Dec 5, 2024

How exactly would this look like in terms of std? What do you think of something like that?

Deno.test("test", () => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  function throws() {
    throw new Error("error");
  }

  const fn = spy(addOne);

  assertEquals(fn(), 1);
  fn.mockImplementation(addTwo);

  assertEquals(fn(), 3);
  assertEquals(fn(), 5);

  fn.mockImplementation(throws as () => number);
  assertThrows(fn, "error");
});

LGTM
I think could also support a setter, like:

fn.implementation = ()=>{ ... }

@kt3k
Copy link
Member

kt3k commented Dec 6, 2024

spy function already exposes (readonly) .original property. Maybe making it writable property is the easiest way to implement this.

cc @KyleJune What do you think of this kind of addition?

@CyanChanges
Copy link
Author

CyanChanges commented Dec 6, 2024

spy function already exposes (readonly) .original property. Maybe making it writable property is the easiest way to implement this.

cc @KyleJune What do you think of this kind of addition?

Yes, I checked the code. Just add a setter here.

std/testing/mock.ts

Lines 653 to 656 in 2ad1564

original: {
enumerable: true,
value: original,
},

@KyleJune
Copy link
Contributor

KyleJune commented Dec 6, 2024

spy function already exposes (readonly) .original property. Maybe making it writable property is the easiest way to implement this.
cc @KyleJune What do you think of this kind of addition?

Yes, I checked the code. Just add a setter here.

std/testing/mock.ts

Lines 653 to 656 in 2ad1564

original: {
enumerable: true,
value: original,
},

That original property you linked is on the methodSpy but there is a similar one on functionSpy. The purpose of it is to be able to access the original value of the property. Making it writable and giving the user access to override it would basically turn spys into stubs. It would also make the property confusing since the name implies it would give you the original function being spied on while it actually would be giving you the stub function.

I don't think original should be made writable. If you want to make it possible to turn a spy into a stub or to replace the replacement function of a stub, I would start with changing it so that the fake property on stubs is writable and updating the stub code so that it will call that property instead of the fake const. That would make it possible to swap the fake version of the function with a new fake that you would like the stub to use going forward.

std/testing/mock.ts

Lines 1106 to 1109 in 2ad1564

fake: {
enumerable: true,
value: fake,
},

Then for making it possible to convert a spy, I'd suggest adding a fake property to spies that defaults to undefined. Then if the user sets it, make the spy call through to the fake instead of the original if the fake property is set. That would also make it possible to detect if a spy has been converted into a stub.

Oh also instead of just making it a writable, I'd actually change it so that the fake variable is declared with let instead of const. Then the fake property descriptor would have a getter that returns the vale and a setter that overwrites it. That way we could have the setter throw an error if the user tries to set it to anything other than undefined or a function.

I don't like the idea of there being 2 different ways to change the implementation used behind the mock. I'd suggest choosing to either make fake property a getter/setter, or add a function like replaceStub. Personally, I'd prefer the fake property getter/setter approach.

Edit: Also worth noting if this change is made, I think stub could be refactored to just call through to spy, then setting the fake property to be the stub function before returning it. That would remove some code duplication.

@kt3k
Copy link
Member

kt3k commented Dec 8, 2024

@KyleJune I agree with you. Let's not make original writable, but add .fake to spy and make it writable.

So the API would probably look like:

const fn = spy(addOne);
assertEquals(fn(), 1);
fn.fake = addTwo;
assertEquals(fn(), 3);
assertEquals(fn(), 5);
fn.fake = throws;
assertThrows(fn);

@timreichen
Copy link
Contributor

How about replacement or substitute instead of fake as a name?

@KyleJune
Copy link
Contributor

KyleJune commented Dec 8, 2024

How about replacement or substitute instead of fake as a name?

I think fake would be better because stub already has a fake property and it would be a breaking change to rename it vs. changing it to be writable via a setter. And it would be confusing if the property name was different for spy and stub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
suggestion a suggestion yet to be agreed testing
Projects
None yet
Development

No branches or pull requests

5 participants