Pesquisa de site

javascript - Jest toHaveBeenNthCalledWith retorna o valor da variável conforme está definido atualmente, em vez do valor no momento da chamada


Estou trabalhando em uma aplicação NestJS usando um conjunto bastante padrão de plugins NestJS (coisas como @nestjs/axios e @nestjs/testing.

Eu tenho uma função relativamente simples que deve paginar através de solicitações de API e adicionar os valores resultantes a um array.

async syncRecordList(limit: number = 1000) {
        const url = 'https://my.test.url/records';
        const headers = {
            Authorization: 'Bearer token-value',
        };
        const params = {
            limit, // the per page limit
            offset: 0, // the offset for pagination
        };
        const itemIds: number[] = [];
        let more: boolean = true;
        while (more) {
            const response = await this.httpService.axiosRef.get(url, {
                headers,
                params,
            });
            response.data.items.forEach((item) => {
                itemIds.push(item.id);
            });
            params.offset += limit;
            more = response.data.hasMore;
        }
        return itemIds;
    }

Para testar esta função simples, eu tenho o seguinte teste:

describe('syncRecordList', () => {
        it('should call multiple times if there is pagination', async () => {
            const response0 = {
                data: {
                    items: [{ id: 1 }, { id: 2 }],
                    hasMore: true,
                },
            };

            const response1 = {
                data: {
                    items: [{ id: 3 }, { id: 4 }],
                    hasMore: false,
                },
            };

            const callExpectWith0 = [
                'https://my.test.url/records',
                {
                    headers: {
                        Authorization: 'Bearer token-value',
                    },
                    params: {
                        limit: 2,
                        offset: 0
                    },
                },
            ]

            const callExpectWith1 = [
                'https://my.test.url/records',
                {
                    headers: {
                        Authorization: 'Bearer token-value',
                    },
                    params: {
                        limit: 2,
                        offset: 2
                    },
                },
            ]

            const httpSpy = jest.spyOn(service.httpService.axiosRef, 'get')
                .mockResolvedValueOnce(response0)
                .mockResolvedValueOnce(response1);

            await service.syncRecordList(2);

            expect(httpSpy).toHaveBeenCalledTimes(2);
            expect(httpSpy).toHaveBeenNthCalledWith(1, ...callExpectWith0);
            expect(httpSpy).toHaveBeenNthCalledWith(2, ...callExpectWith1);
        });
    });

Como você pode ver no código, eu espero que a primeira chamada use os parâmetros de limit: 2, offset: 0, e que a segunda chamada use os parâmetros de limit: 2, offset: 2. O código é projetado de forma que o valor dos parâmetros seja alterado a cada chamada do loop para modificar o offset. Isso é feito antes do loop terminar, significando que na segunda vez que algo acontece, antes do valor de more ser definido como falso, incrementaremos o valor do offset dos parâmetros em antecipação a outro loop.

Minha expectativa é que isso passasse. Que no histórico de chamadas dos mocks os parâmetros mostrassem dois valores diferentes. O que eu realmente vejo, no entanto, é uma falha.

expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected)

    n: 1
    Expected: "https://my.test.url/records", {"headers": {"Authorization": "Bearer token-value"}, "params": {"limit": 2, "offset": 0}}
    Received
    ->     1
              "https://my.test.url/records",
            @@ -2,8 +2,8 @@
                "headers": Object {
                  "Authorization": "Bearer token-value",
                },
                "params": Object {
                  "limit": 2,
            -     "offset": 0,
            +     "offset": 4,
                },
              },
           2: "https://my.test.url/records", {"headers": {"Authorization": "Bearer token-value"}, "params": {"limit": 2, "offset": 4}}

O resultado de console.log(JSON.stringify(httpSpy.mock.calls)); é [["https://123456789c.suitetalk.api.netsuite.com/services/rest/record/v1/assemblyItem",{"headers":{"Authorization":"Bearer token-value"},"params":{"limit":2,"offset":4}}],["https://123456789c.suitetalk.api.netsuite.com/services/rest/record/v1/assemblyItem",{"headers":{"Authorization":"Bearer token-value"},"params":{"limit":2,"offset":4}}]] mostrando que ele considera que foi chamado duas vezes com um offset de quatro.

Mas não foi chamado com um deslocamento de quatro, o valor de params no final do teste tem um deslocamento de quatro.

Se eu mudar o código ligeiramente, para usar uma cópia de params em vez de params, funciona:

    async syncRecordList(type: NetSuiteRecordTypes, integrationId: string, limit: number = 1000) {
        // ... Snipped 
        const params = {
            limit, // the per page limit
            offset: 0, // the offset for pagination
        };
        const itemIds: number[] = [];
        let more: boolean = true;
        while (more) {
            // This was done so that testing would pass. The params object was being modified and
            // causing the tests to fail as it seems to always refer to the variable used, not what what
            // was actually called with.
            const paramsToUse = { ...params };
            const response = await this.httpService.axiosRef.get(url, {
                headers,
                params: paramsToUse,
            });
            response.data.items.forEach((item) => {
                itemIds.push(item.id);
            });
            params.offset += limit;
            more = response.data.hasMore;
        }
        return itemIds;
    }

Neste caso, meus testes passam, pois a função é chamada com o que eu esperava.

No fim, minha pergunta é: existe uma maneira de o Jest chamado com 'can' realmente usar o que foi passado no momento da chamada, e não o valor da variável depois que o teste termina?

Resposta #1

Esta é de fato uma limitação do Jest. Comentários sobre os problemas 434 e 4715 do GitHub do Jest afirmam que copiar o valor em vez de usar a referência seria muito caro e afetaria severamente o desempenho do Jest.

Com base nos comentários à minha pergunta original, parece que eu tinha algumas opções.

  1. Modifique meu código para que os argumentos no loop sejam sempre diferentes, em vez de referências.
  2. Modifique meu teste para usar uma asserção diferente
  3. Implemente uma biblioteca diferente para alterar esse teste

Na pergunta original eu já estava fazendo a opção 1, mas eu refinei isso com base nos comentários à pergunta. Acabei com algo assim no meu método:

async syncRecordList(limit: number = 1000) {
        const url = 'https://my.test.url/records';
        const headers = { Authorization: 'Bearer token-value' };
        const itemIds: number[] = [];
        let offset: number = 0;
        let more: boolean = true;
        while (more) {
            const response = await axios.get(url, {
                headers,
                params: { limit, offset },
            });
            response.data.items.forEach((item) => {
                itemIds.push(item.id);
            });
            offset += limit;
            more = response.data.hasMore;
        }
        return itemIds;
    }