Tester React Query

--- vues
Banner image for Tester React Query

Les questions concernant les tests reviennent souvent avec React Query, je vais donc essayer de répondre à certaines d'entre elles ici. Je pense qu'une des raisons est que tester des composants "intelligents" (également appelés container components) n'est pas la chose la plus simple à faire. Avec l'émergence des hooks, cette séparation a été largement abandonnée. Il est maintenant recommandé d'utiliser les hooks directement là où vous en avez besoin plutôt que de faire une séparation arbitraire et de faire descendre les props.

Je pense que c'est généralement une très bonne amélioration pour la colocation et la lisibilité du code, mais nous avons maintenant plus de composants qui consomment des dépendances en dehors des simples "props".

Ils peuvent utiliser useContext, useSelector, ou useQuery.

Ces composants ne sont techniquement plus purs, car leur appel dans différents environnements conduit à des résultats différents. Lors des tests, vous devez soigneusement configurer ces environnements pour que tout fonctionne correctement.

Simuler les requêtes réseau#

Étant donné que React Query est une bibliothèque de gestion d'état asynchrone côté serveur, vos composants feront probablement des requêtes vers un backend. Lors des tests, ce backend n'est pas disponible pour fournir des données, et même s'il l'était, vous ne voulez probablement pas que vos tests en dépendent.

Il existe de nombreux articles sur la façon de simuler des données avec Jest. Vous pouvez simuler votre client API si vous en avez un. Vous pouvez simuler fetch ou axios directement. Je ne peux que confirmer ce que Kent C. Dodds a écrit dans son article Stop mocking fetch :

Utilisez mock service worker par @ApiMocking

Il peut être votre source unique de vérité pour la simulation de vos APIs :

  • fonctionne avec Node pour les tests
  • supporte REST et GraphQL
  • possède un addon Storybook pour écrire des stories pour vos composants qui utilisent useQuery
  • fonctionne dans le navigateur pour le développement, et vous verrez toujours les requêtes sortantes dans les devtools du navigateur
  • fonctionne avec Cypress, similaire aux fixtures

Maintenant que notre couche réseau est prise en charge, nous pouvons parler des spécificités de React Query à surveiller :

QueryClientProvider#

Chaque fois que vous utilisez React Query, vous avez besoin d'un QueryClientProvider et d'un queryClient - un conteneur qui détient le QueryCache. Le cache conservera à son tour les données de vos requêtes.

Je préfère donner à chaque test son propre QueryClientProvider et créer un nouveau QueryClient pour chaque test. De cette façon, les tests sont complètement isolés les uns des autres. Une approche différente pourrait être de vider le cache après chaque test, mais je préfère garder l'état partagé entre les tests aussi minimal que possible. Sinon, vous pourriez obtenir des résultats inattendus et instables si vous exécutez vos tests en parallèle.

Pour les hooks personnalisés Si vous testez des hooks personnalisés, vous utilisez probablement react-hooks-testing-library. C'est la solution la plus simple pour tester les hooks. Avec cette bibliothèque, nous pouvons envelopper notre hook dans un wrapper, qui est un composant React pour envelopper le composant de test lors du rendu. Je pense que c'est l'endroit parfait pour créer le QueryClient, car il sera exécuté une fois par test :

wrapper
const createWrapper = () => {
  // ✅ crée un nouveau QueryClient pour chaque test
  const queryClient = new QueryClient()
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test('mon premier test', async () => {
  const { result } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper(),
  })
})

Pour les composants#

Si vous voulez tester un composant qui utilise un hook useQuery, vous devez également envelopper ce composant dans QueryClientProvider. Un petit wrapper autour de render de react-testing-library semble être un bon choix. Regardez comment React Query le fait en interne pour leurs tests.

Désactiver les tentatives#

C'est l'un des pièges les plus courants avec React Query et les tests : la bibliothèque est configurée par défaut pour faire trois tentatives avec un délai exponentiel, ce qui signifie que vos tests risquent d'expirer si vous voulez tester une requête erronée. La façon la plus simple de désactiver les tentatives est, encore une fois, via le QueryClientProvider. Étendons l'exemple ci-dessus :

no-retries
const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        // ✅ désactive les tentatives
        retry: false,
      },
    },
  })

  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test("mon premier test", async () => {
  const { result } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper()
  })
})

Cela définira les valeurs par défaut pour toutes les requêtes dans l'arbre des composants à "pas de tentatives". Il est important de savoir que cela ne fonctionnera que si votre useQuery n'a pas de tentatives explicitement définies. Si vous avez une requête qui veut 5 tentatives, cela aura toujours la priorité, car les valeurs par défaut ne sont prises qu'en dernier recours.

setQueryDefaults#

Le meilleur conseil que je puisse vous donner pour ce problème est : Ne définissez pas ces options directement sur useQuery. Essayez d'utiliser et de remplacer les valeurs par défaut autant que possible, et si vous avez vraiment besoin de changer quelque chose pour des requêtes spécifiques, utilisez queryClient.setQueryDefaults.

Par exemple, au lieu de définir retry sur useQuery :

not-on-useQuery
const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  // 🚨 vous ne pouvez pas remplacer ce paramètre pour les tests !
  const queryInfo = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    retry: 5,
  })
}

Définissez-le comme ceci :

setQueryDefaults
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2,
    },
  },
})

// ✅ seuls les todos feront 5 tentatives
queryClient.setQueryDefaults(['todos'], { retry: 5 })

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

Ici, toutes les requêtes feront deux tentatives, seuls les todos feront cinq tentatives, et j'ai toujours la possibilité de désactiver cela pour toutes les requêtes dans mes tests 🙌.

ReactQueryConfigProvider#

Bien sûr, cela ne fonctionne que pour les clés de requête connues. Parfois, vous voulez vraiment définir certaines configurations sur une partie de votre arbre de composants. Dans la v2, React Query avait un ReactQueryConfigProvider pour ce cas d'utilisation précis. Vous pouvez obtenir le même résultat dans v3 avec quelques lignes de code :

ReactQueryConfigProvider
const ReactQueryConfigProvider = ({ children, defaultOptions }) => {
  const client = useQueryClient()
  const [newClient] = React.useState(
    () =>
      new QueryClient({
        queryCache: client.getQueryCache(),
        muationCache: client.getMutationCache(),
        defaultOptions,
      })
  )

  return (
    <QueryClientProvider client={newClient}>
      {children}
    </QueryClientProvider>
  )
}

Vous pouvez voir cela en action dans cet exemple codesandbox.

Toujours attendre la requête#

Comme React Query est asynchrone par nature, lors de l'exécution du hook, vous n'obtiendrez pas immédiatement un résultat. Il sera généralement en état de chargement et sans données à vérifier. Les utilitaires asynchrones de react-hooks-testing-library offrent de nombreuses façons de résoudre ce problème. Pour le cas le plus simple, nous pouvons simplement attendre que la requête passe à l'état de succès :

waitFor
const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  })
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

test("mon premier test", async () => {
  const { result, waitFor } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper()
  })

  // ✅ attendre que la requête passe à l'état de succès
  await waitFor(() => result.current.isSuccess)

  expect(result.current.data).toBeDefined()
}
Update

@testing-library/react v13.1.0 a également un nouveau renderHook que vous pouvez utiliser. Cependant, il ne renvoie pas son propre utilitaire waitFor, vous devrez donc utiliser celui que vous pouvez importer depuis @testing-library/react à la place. L'API est un peu différente, car elle ne permet pas de renvoyer un booléen, mais attend une Promise à la place. Cela signifie que nous devons adapter légèrement notre code :

new-render-hook
import { waitFor, renderHook } from '@testing-library/react'

test("mon premier test", async () => {
  const { result } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper()
  })

  // ✅ renvoyer une Promise via expect à waitFor
  await waitFor(() => expect(result.current.isSuccess).toBe(true))

  expect(result.current.data).toBeDefined()
}

Tout mettre ensemble#

J'ai créé un dépôt rapide où tout cela s'assemble parfaitement : mock-service-worker, react-testing-library et le wrapper mentionné. Il contient quatre tests - des tests basiques d'échec et de succès pour les hooks personnalisés et les composants. Jetez un œil ici : https://github.com/TkDodo/testing-react-query