Criando e testando um contexto com React,Typescript, Jest e React-Testing-Library
Motivação
Estava no meio de um projeto pessoal que com certeza vai ser um dos maiores e que me deu mais trabalho. Nesse projeto, precisei criar um context para guardar os itens que o usuário colocaria no carrinho. Criar um contexto muita gente já sabe, por isso aqui vou mostrar como criar e testar um, que é um a mais que pode facilitar muito sua vida no futuro.
Tipando o contexto
Nesse ponto já presumo que você já tem todo um ambiente de desenvolvimento configurado e o projeto em andamento (em React, claro).
Primeiro, crie uma pasta nomeada 'hooks' dentro 'src' e outra pasta dentro dela com o nome do contexto que você vai criar e um aquivo 'index.tsx', o meu ficou assim:
depois disso podemos começar a tipagem.
Para criarmos um contexto e usa-lo como um hook, vamos precisar das seguintes importações no nosso arquivo 'index.tsx' que acabamos de criar.
import React, { createContext, useContext } from 'react';
Se você estiver usando Typescript (o que eu espero que sim por que facilita muito a vida) precisaremos criar dois tipos:
- O tipo das propriedades que o nosso PROVIDER vai receber;
- O tipo das propriedades que ficarão armazenadas no nosso contexto, ou seja, as informações que desejamos compartilhar entre os componentes;
Primeiro vamos ao tipo do provider que é o mais simples, o seu deve se parece com algo assim:
type ShoppingListProviderProps = {
children: React.ReactNode;
};
o meu contexto se chama "ShoppingList", substitua no seu projeto pelo nome do seu contexto para fazer sentido. A propriedade não muda, nosso provider recebe um 'children', ou seja, todos os componentes que tiverem nesse children terão acesso as informações do nosso contexto.
O nosso proximo tipo é o dos dados que o nosso contexto vai armazenar/compartilhar, o meu se parece com algo assim:
export type ListItem = {
id: string;
name: string;
quantity: number;
};
export type ShoppingListContextData = {
items: ListItem[];
onAdd: (id: string, name: string, quantity: number) => void;
onIncrease: (id: string) => void;
onRemove: (id: string) => void;
onDecrease: (id: string) => void;
isInList: (id: string) => boolean;
};
meu contexto tem um dado 'items' (onde ficará armazenado os itens que meu usuário colocar no carrinho), onAdd(responsável por adicionar os itens ao meu carrinho), onIncrease(aumenta a quatidade de certo item no meu carrinho), onDecrease(diminui a quantidade de certo item no meu carrinho) e inList(confere se um item pertecence ou não ao meu carrinho). Lembrando que essa tipagem faz sentido para as propriedades do MEU CONTEXTO, tipe as propriedades de acordo com o seu. Agora que já temos a nossa tipagem, vamos criar o contexto.
Criando o contexto
Agora vamos criar o nosso contexto usando as nossas importações, ficará algo assim:
const ShoppingListContext = createContext<ShoppingListContextData>(
ShoppingListContextDefaultValues
);
repare que eu tipo as propriedades com o 'type' que criamos no bloco anterior e que coloco como valor inicial um 'ShoppingListContextDefaultValues' que se parece com isso:
const ShoppingListContextDefaultValues = {
items: [],
onAdd: () => null,
onIncrease: () => null,
onRemove: () => null,
onDecrease: () => null,
isInList: () => false
};
apenas coloco valores padrões que não vão fazer diferença na prática, faço isso por que o hook createContext pede valores iniciais. Se você nao quiser criar esse objeto, pode fazer assim também:
const ShoppingListContext = createContext(
{} as ShoppingListContextData
);
com isso, já criamos o nosso contexto e agora devemos criar as propriedades que ele possui.
Criando as propriedades do contexto
Quando criamos um contexto em React, ele tem uma propriedade chamada 'Provider' que pode ser chamada como um component react, algo assim:
<ShoppingListContext.Provider value={{ }}>
{children}
</ShoppingListContext.Provider>
como já disse antes, todos os componentes que estiverem dentro de 'children' vão poder acessar as propriedades, o 'value' que devemos passar são os valores/propriedades em sí. Por isso, vamos começar a criar as nossas propriedades.
Como eu armazeno itens no meu contexto, vou precisar do hook useState que vou importar no meu arquivo ficando assim:
import React, { createContext, useContext, useState } from 'react';
As minhas propriedades vão ficar assim:
items
const [items, setItems] = useState<ListItem[]>([]);
onAdd
const onAdd = (id: string, name: string, quantity: number) => {
setItems([...items, { id, name, quantity }]);
};
onRemove
const onRemove = (id: string) => {
const filteredItems = items.filter((item) => item.id !== id);
setItems(filteredItems);
};
onIncrease
const onIncrease = (id: string) => {
const itemsArray = [...items];
for (let i = 0; i < itemsArray.length; i++) {
const item = itemsArray[i];
if (item.id == id) {
item.quantity++;
break;
}
}
setItems(itemsArray);
};
onDecrease
const onDecrease = (id: string) => {
const itemsArray = [...items];
for (let i = 0; i < itemsArray.length; i++) {
const item = itemsArray[i];
if (item.id == id) {
item.quantity--;
break;
}
}
setItems(itemsArray);
};
isInList
const isInList = (id: string) => {
return items.some((item) => item.id === id);
};
não vou entrar muito em detalhes de como cada função funciona, mas o importante é saber o que cada uma faz que já descrevi anteriormente.
Depois de criar as propriedades, precisamos criar um componente de provider que vai conter as nossa propriedades e no final retornanar um ShoppingList.Provider com o nosso children e as propriedades como no parâmetro value como já mostrado. Fazemos isso para guardar tudo que está relacionado ao contexto dentro do seu arquivo. No final teremos algo assim:
const ShoppingListProvider = ({ children }: ShoppingListProviderProps) => {
const [items, setItems] = useState<ListItem[]>([]);
const onAdd = (id: string, name: string, quantity: number) => {
setItems([...items, { id, name, quantity }]);
};
const onRemove = (id: string) => {
const filteredItems = items.filter((item) => item.id !== id);
setItems(filteredItems);
};
const onIncrease = (id: string) => {
const itemsArray = [...items];
for (let i = 0; i < itemsArray.length; i++) {
const item = itemsArray[i];
if (item.id == id) {
item.quantity++;
break;
}
}
setItems(itemsArray);
};
const onDecrease = (id: string) => {
const itemsArray = [...items];
for (let i = 0; i < itemsArray.length; i++) {
const item = itemsArray[i];
if (item.id == id) {
item.quantity--;
break;
}
}
setItems(itemsArray);
};
const isInList = (id: string) => {
return items.some((item) => item.id === id);
};
return (
<ShoppingListContext.Provider
value={{ items, onAdd, onDecrease, onRemove, onIncrease, isInList }}
>
{children}
</ShoppingListContext.Provider>
);
};
esse nosso componente será responsavel por ser o nosso provider. Esse component 'ShoppingListProvider' nós podemos importa-lo em qualquer lugar do nosso arquivo para definir quais componentes vão poder acessar essas propriedades. Antes disso, vamos criar um hook customizado que ficará assim:
const useShoppingList = () => useContext(ShoppingListContext);
esse hook é apenas para facilitar a utlização do nosso contexto, apenas uma função que retorna o nosso contexto.
Um exemplo de comos podemos utilizar esse 'ShoppingListProvider' é assim:
import { ShoppingListProvider } from 'hooks/use-shoppinglist';
export default function Home() {
return (
<ShoppingListProvider>
<ComponentesDaHome />
</ShoppingListProvider>
);
}
Esse componente 'Home' como o nome já diz é o meu componte da página Home, ao envolver o retorno dele como o meu provider, TODOS os componentes que estiverem dentro dele podem acessar as propriedades, pelo nosso hook personalizado 'useShoppingList' desse jeito:
const {
isInList,
items,
onAdd,
onDecrease,
onIncrease,
onRemove
} = useShoppingList();
e claro, precisamos importa-lo no nosso arquivo. Agora que já temos um contexto totalmente funcional, precisamos testa-lo que é o que vamos fazer no próximo bloco.
Testando o nosso contexto
Talvez você não esteja acostumado com testes em cotextos/hooks (como eu não estava), mas por ser uma parte importantíssima do nosso projeto ele pode e DEVE ser testado. Como praticamente todo os projetos React já vem com o jest e a react testing library instalados (que é o que vamos usar) vou presumir que você já tem isso configurado, se não tiver vá ate o link da testing library para configurar seu ambiente e ao do jest. Com isso podemos começar.
Instalando pacote
Como vamos testar um hook, precisamos de um pacote extra da testing library que fará isso para gente, por isso instale o pacote testing-library/react-hooks:
npm install --save-dev @testing-library/react-hooks
ou
yarn add @testing-library/react-hooks -D
após isso, crie um arquivo de teste dentro do diretório do seu contexto.
No nosso arquivo de teste vamos precisar importar o nosso provider, nosso hook personalizado, a função 'renderHook' do pacote que acabamos de instalar e uma função helper do proprio react para testes , ficará assim:
import {
useShoppingList,
ShoppingListProvider,
ShoppingListProviderProps
} from '.';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-dom/test-utils
após isso já podemos abrir o nosso bloco describe para testes e iniciar. Para testar o nosso hook, precisamos ver o que necessita de testes (no meu caso são as 5 funções) e aí sim começar. Com isso, vou testar primeiro a função de adicionar um item que deve ficar assim:
describe('useShoppinglist', () => {
it('should add one item to the list', () => {
const wrapper = ({ children }: ShoppingListProviderProps) => (
<ShoppingListProvider>{children}</ShoppingListProvider>
);
const { result } = renderHook(() => useShoppingList(), {
wrapper
});
act(() => {
result.current.onAdd('1', 'cake', 5);
});
expect(result.current.items).toStrictEqual([
{ id: '1', name: 'cake', quantity: 5 }
]);
});
});
antes de mais nada precisamos criar um 'wrapper' para definir onde o nosso hook será renderizado, faço isso nas primeiras linhas do bloco 'it' apenas criando uma função e retornando o nosso provider com um children. Após isso, precisamos usar a função 'renderHook' para fazer o nosso hook ficar utilizavel dentro do nosso teste. Essa função recebe como primeiro parâmentro uma função que retorna o nosso hook e como segundo um objeto que deve ter dentro dele o nosso 'wrapper' que por sua vez TEM que ter o provider englobando o retorno.
Esse hook nos retorna um atributo 'result' que é onde está todas as nossas propriedades do contexto. Para testarmos a função de 'onAdd' bastar chamar result.current.onAdd('1', 'cake', 5) (dentro do 'result' nós temos esse atributo 'current' que se refere ano nosso hook, tanto que podemos acessar todas as nossas propriedades a partir de 'current'). Usamos essa função 'act' importada de 'react-dom/test-utils' por ser recomendada pela própria documentação do react quando fazemos mudança de estados em testes, leia mais aqui.
Após isso tudo, podemos de fato testar se a nossa função 'onAdd' realmente funcionou chamando a função 'expect', que recebe o que queremos analisar (no caso os 'items' do nosso contexto) e o que queremos fazer, nesse caso eu quero que ele seja estritamente igual a um array que contém um objeto com as propriedades exatamente iguais as que eu adicionei. Não vou testar as outras quatro funções pois vocês já imaginam como dever ser os testes e meu objetivo era mostrar como testas hooks, e não funções específicas do meu contexto.
Conclusão
Após essa gigante dissertação, aprendemos a como criar um contexto com um hook personalizado no React e ainda mais testar tudo isso, o que é de extrema importância. Agora que você já saber fazer ambos, não perca tempo e adicione os testes nos seus hooks personalizados de imediato !!!!
Até a proxima