test-utils icon indicating copy to clipboard operation
test-utils copied to clipboard

Changing registerEndpoint response

Open tomsdob opened this issue 2 years ago • 8 comments

Hello!

I would like to test a component which renders different child components based on the response of a single API endpoint. Is there any way of changing the response of an endpoint registered by registerEndpoint or perhaps removing the endpoint once it is not needed any more in a test?

Here is a code example:

// @vitest-environment nuxt
import { describe, expect, it } from 'vitest';
import { registerEndpoint } from 'vitest-environment-nuxt/utils';

describe('Test 123', async () => {
  registerEndpoint('/test', () => ({
    test: 123,
  }));

  const { data } = await useFetch('/test');

  it('should return 123', () => {
    expect(data.value).toStrictEqual({ test: 123 });
  });
});

describe('Test 321', async () => {
  registerEndpoint('/test', () => ({
    test: 321,
  }));

  const { data } = await useFetch('/test');

  // Still returns { test: 123 }
  it('should return 321', () => {
    expect(data.value).toStrictEqual({ test: 321 });
  });
});

tomsdob avatar Jun 19 '23 07:06 tomsdob

While fiddling around with the registerEndpoint source code I came upon a solution of some kind which solved my problem and might serve as an idea worth adding:

function registerEndpoint(url: string, handler: EventHandler) {
  // @ts-expect-error private property
  if (!window.__app) return;
  // @ts-expect-error private property
  const existingEndpoint = window.__app.stack.find((item) => item.route === `/_${url}`);
  if (existingEndpoint) {
    // @ts-expect-error private property
    window.__app.stack[window.__app.stack.indexOf(existingEndpoint)].handler = handler;
  } else {
    // @ts-expect-error private property
    window.__app.use('/_' + url, defineEventHandler(handler));
    // @ts-expect-error private property
    window.__registry.add(url);
  }
}

tomsdob avatar Jun 19 '23 07:06 tomsdob

Either do it in different describe blocks or you can also just make the function you give a vitest.fn mock function.

so like

const api = vitest.fn().mockResolvedValueOnce({
      result: [{id: 1}],
    });

registerEndpoint("/api/fake-call", api);

I am not sure if vitest's parallel test execution adds problems for this. Workaround for that would still be different test suites/describe blocks perhaps. But i'm not sure if it's an issue in the first place.

rubennaatje avatar Sep 07 '23 09:09 rubennaatje

Either do it in different describe blocks or you can also just make the function you give a vitest.fn mock function.

so like

const api = vitest.fn().mockResolvedValueOnce({
      result: [{id: 1}],
    });

registerEndpoint("/api/fake-call", api);

I am not sure if vitest's parallel test execution adds problems for this. Workaround for that would still be different test suites/describe blocks perhaps. But i'm not sure if it's an issue in the first place.

It doesn't behave as you hope. Unfortunately it only uses the first registerEndpoint in the file despite how you setup blocks.

I've personally turned to splitting it up into specific files instead. So e.g.:

  • ComponentName.generic.nuxt.spec.ts (doesn't care about response)
  • ComponentName.empty.nuxt.spec.ts (has en empty response)
  • ComponentName.populated.nuxt.spec.ts (has a populated response)

It would be neater to have it all in one file, though, but at least I get to test it now.

mathiasrando avatar Sep 14 '23 19:09 mathiasrando

I think I am facing the same problem. I'm running a series of tests and being able to unregister an endpoint or at least modify its event handler would be awesome!

JulienChampagnol avatar Jan 08 '24 16:01 JulienChampagnol

got that using vi.stubGlobal

describe("...", () => {
	it("1", () => {
		vi.stubGlobal("$fetch", () => MOCKED_RESPONSE);
	});
	it("2", () => {
		vi.stubGlobal("$fetch", () => MOCKED_RESPONSE_2);
	});
	it("3 (error)", async () => {
		vi.stubGlobal("$fetch", () => {
			throw new Error();
		});
	});
})

tumoxep avatar Mar 22 '24 01:03 tumoxep

Extending rubennaatjes idea, this is what worked for me:

const getFakeCall = vi.fn();

registerEndpoint('/api/fake-call', getFakeCall);

describe('fake-call', () => {
  test('test 1', async () => {
    getFakeCall.mockImplementation(() => ({ test: 123 }));
    // ...
  });

  test('test 2', async () => {
    getFakeCall.mockImplementation(() => ({ test: 321 }));
    // ...
  });
})

miracoly avatar Mar 22 '24 19:03 miracoly

to anyone coming here to know how to setup different registerEndpointresponses on the same test file, here's the way:

const mocks = vi.hoisted(() => {
  return {
    fetchProjects: vi.fn(),
  };
});

registerEndpoint("/api/v1/projects/", mocks.fetchProjects);

And then inside tests:

describe("Given composable X", () => {
  describe("When situation 1", () => {
    it("Then...", () => {
      mocks.fetchProjects.mockReturnValue(1);
      // Test with response 1
    });
  });
  describe("When situation 2", () => {
    it("Then...", () => {
      mocks.fetchProjects.mockReturnValue(2);
      // Teset with response 2
    });
  });
});

bbm16 avatar Apr 24 '24 13:04 bbm16

There is also an issue with the order of registerEndpoint. Let's say I have two endpoints /endpoint1 and /endpoint2

registerEndpoint('/endpoint1', resp1)
registerEndpoint('/endpoint2', resp2)

and in my tested code is:

$fetch('/endpoint2')
$fetch('/endpoint1')

I get the resp2 from /endpoint1...

tballak avatar May 31 '24 06:05 tballak

I checked the h3 framework documentation and figured out why the second registerEndpoint doesn't seem to work. Also checked the code about how h3 registerEndpoint is implemented. This is because of the way it registers the eventHandlers. Check docs here: https://h3.unjs.io/guide/app#registering-event-handlers:~:text=In%20this%20example,never%20be%20called. To allow middleware capabilities, it allows the stacking of multiple routes with the same path, however, only if the handler doesn't return the response. If it does, subsequent handlers in the stack will never be called. In other words, a request will never have 2 responses.

The solution is to make sure the first time we register, we don't return the response but instead, make it return dynamically using vi.mockReturnValue().

For example:

// @vitest-environment nuxt
import { describe, expect, it } from 'vitest';
import { registerEndpoint } from 'vitest-environment-nuxt/utils';

describe('Test 123', async () => {
  const endpoint = vi.fn()
  registerEndpoint('/test', endpoint);
  beforeAll(() => {
    vi.restoreAllMocks() // prevents any previous' test mock conflict
  })
  afterAll(() => {
    vi.restoreAllMocks() // prevents any subsequent' test mock conflict
  })

  endpoint.mockReturnValue({
    test: 123,
  })
  const { data } = await useFetch('/test');

  it('should return 123', () => {
    expect(data.value).toStrictEqual({ test: 123 });
  });
});

describe('Test 321', async () => {
  const endpoint = vi.fn()
  registerEndpoint('/test', endpoint);
  beforeAll(() => {
    vi.restoreAllMocks() // prevents any previous' test mock conflict
  })
  afterAll(() => {
    vi.restoreAllMocks() // prevents any subsequent' test mock conflict
  })

  endpoint.mockReturnValue({
    test: 321,
  })
  const { data } = await useFetch('/test');

  // Still returns { test: 123 }
  it('should return 321', () => {
    expect(data.value).toStrictEqual({ test: 321 });
  });
});

A lot of room for refactoring, but I hope that helps.

kaitoqueiroz avatar Jan 31 '25 13:01 kaitoqueiroz