如何对 apollo 链接进行单元测试

Eri*_*cDS 2 unit-testing mocking typescript reactjs apollo-client

我目前有点困惑如何在我的 React 应用程序中测试一些 apollo 链接,因为官方文档似乎只提供有关如何在组件连接到提供程序后测试组件的建议。

目前我有 2 个链接:一个用于注入授权令牌,另一个用于在服务器返回响应后刷新它;我想要做的是要么单独测试它们,要么测试客户端(将在这些链接和一个简单的基础上构建HttpLink)在满足条件时执行它们的逻辑。

以下是他们的实现:

// InjectToken.ts
import { setContext } from '@apollo/client/link/context';

import { getToken } from '../../authentication';

const authenticationLink = setContext(async (_, { headers }) => {

  // Fetches the token from the local storage
  const token = await getToken();

  if (token) {
    return {
      headers: {
        ...headers,
        Authorization: `Bearer ${token}`
      }
    }
  }

  return { headers };
});

export default authenticationLink;
Run Code Online (Sandbox Code Playgroud)
// RefreshToken.ts
import { ApolloLink } from '@apollo/client';

import { refreshToken } from '../../authentication';

const resetTokenLink = new ApolloLink(
  (operation, forward) => forward(operation).map(response => {
    const context = operation.getContext();
    refreshToken(context);
    return response;
  })
);

export default resetTokenLink;
Run Code Online (Sandbox Code Playgroud)

我虽然关于使用apollo或钩子MockProvider之一通过客户端发出带有模拟响应的“假”请求,但似乎这个模拟提供者实际上在解析假数据之前模拟了客户端,因此它不在桌面上我。useQueryuseMutation

我考虑的第二个选项是遵循本指南,它基本上将您的链接与您调用expect方法的自定义“断言”链接连接起来。

虽然很有希望,但该实现并不适合我开箱即用,因为测试没有等待调用execute完成(没有断言设法执行),所以我做了一些更改以将其包装在如下承诺中:

// mockAssertForLink.ts

// This performs the mock request
async function mockExecuteRequest(link: ApolloLink): Promise<void> {
  return new Promise<void>((resolve): void => {
    const lastLink = new ApolloLink(() => {
      resolve();
      return null;
    })
    execute(ApolloLink.from([link, lastLink]), { query: MockQuery}).subscribe((): void => {
      // Not required for our tests, subscribe merely fires the request
    });
  })
}

// This exposes the assertionCallback after the promise fulfills, and reports the operation object.
export default async function mockAssertForLink(
  link: ApolloLink,
  assertionCallback: (operation: Operation) => void
): Promise<void> {
  return mockExecuteRequest(ApolloLink.from([
    link,
    new ApolloLink((operation, forward) => {
      assertionCallback(operation);
      return forward(operation);
    })
  ]))
}
Run Code Online (Sandbox Code Playgroud)

通过此实现,我基本上为我想要执行测试的每个链接创建两个额外的链接:

  • 公开断言回调,我可以在其中检查操作的上下文
  • 一个实际调用的Promise.resolve(),这将防止我的异步测试在执行时陷入困境

我的测试是mockAssertForLink这样使用的:

// InjectToken.test.ts
it('correctly injects authorization header', async () => {
  mocked(getToken).mockResolvedValue(mockToken);
  await mockAssertForLink(authenticationLink, operation => {
    expect(operation.getContext().headers.Authorization).toBe(`Bearer ${mockToken}`)
  });
});

// RefreshToken.ts
it('correctly refreshes the token', async () => {
  await mockAssertForLink(resetTokenLink, () => {
    expect(refreshToken).toHaveBeenCalledTimes(1);
  });
});

Run Code Online (Sandbox Code Playgroud)

这适用于第一个链接,我只是注入一个标头,但在第二个链接上,断言总是失败,仔细观察后,似乎我在方法中定义的内容map从未被调用。

现在,我不确定这是否是进行此类测试的正确方法,因为有关该主题的文档有点缺乏。我想知道的是:

  • 这种测试 apollo 的方法实际上可行吗,或者有更好的方法来测试我的客户端配置吗?
  • 如果我继续使用这种方法,有没有办法可以强制map在链接上调用该方法?

任何帮助将不胜感激。

wan*_*tok 5

我有同样的一般性问题,并决定在链接执行工具中的适当时间解决承诺的相同答案。

您没有看到map接到电话有以下几个原因:

  • map在可观察的结果上调用,并且您不会从终止链接返回结果,因此map永远不会在被测试的链接上调用。
  • 如果您确实在终止链接中返回了结果,则可以调用当前执行断言的位置。 map您需要将断言延迟到响应处理逻辑运行之后。

可以将您的工具简化为添加单个终止链接,然后将您的承诺解析逻辑移至订阅调用中。请参阅下面的示例:

import { ApolloLink, execute, FetchResult, gql, GraphQLRequest, Observable, Operation } from '@apollo/client';

const MockQuery = gql`
  query {
    thing
  }
`;

interface LinkResult<T> {
  operation: Operation;
  result: FetchResult<T>;
}

async function executeLink<T = any, U = any>(
  linkToTest: ApolloLink,
  request: GraphQLRequest = { query: MockQuery },
  responseToReturn: FetchResult<U> = { data: null }
) {
  const linkResult = {} as LinkResult<T>;

  return new Promise<LinkResult<T>>((resolve, reject) => {
    const terminatingLink = new ApolloLink((operation) => {
      linkResult.operation = operation;
      return Observable.of(responseToReturn);
    });

    execute(ApolloLink.from([linkToTest, terminatingLink]), request).subscribe(
      (result) => {
        linkResult.result = result as FetchResult<T>;
      },
      (error) => {
        reject(error);
      },
      () => {
        resolve(linkResult);
      }
    );
  });
}

it('calls refreshToken', async () => {
  const refreshToken = jest.fn();
  const resetTokenLink = new ApolloLink((operation, forward) => {
    operation.variables.test = 'hi';

    return forward(operation)
      .map((response) => {
        refreshToken(operation.getContext());
        return response;
      })
      .map((response) => {
        (response.context ??= {}).addedSomething = true;
        return response;
      });
  });

  const { operation, result } = await executeLink(resetTokenLink);

  expect(refreshToken).toHaveBeenCalled();
  expect(operation.variables.test).toBe('hi');
  expect(result.context?.addedSomething).toBe(true);
});
Run Code Online (Sandbox Code Playgroud)

我只是捕获承诺中的操作和结果值,而不是插入断言逻辑以在链接内运行。您当然可以创建其他自定义链接以在链接链中的特定点插入断言,但在最后断言结果似乎更好。