DEV Community

Júnior Mendes
Júnior Mendes

Posted on

Testando lançamento de exceções com Jest

Testar se uma função trata corretamente erros é algo comum em testes unitários. Quando esse tratamento ocorre por meio de exceções, é fácil se perder frente à quantidade de possibilidades - além do fato de não ser tão trivial quanto parece.

Antes de mais nada, neste tutorial eu assumo que:

  • Você está familiarizado com async/await;
  • Você já utilizou o jest e sabe o contexto de termos como describe e it;
  • Você sabe lidar com exceções.

O problema

Imagine as seguintes funções:

// funcs.js
const foo = () => {
  throw new Error('Something wrong');
}

const goo = async () => {
  throw new Error('Something wrong - async'); 
}

const boo = (n) => {
  if (n < 0) {
    throw new Error('Something wrong - params');
  }
}

const doo = async (n) => {
  if (n < 0) {
    throw new Error('Something wrong - async + params');
  }
}

export default { foo, goo, boo, doo };
Enter fullscreen mode Exit fullscreen mode

Como seria possível testar se a exceção é lançada corretamente? Você pode pensar em algo assim para a função foo, por exemplo:

// funcs.test.js
import { foo } from './funcs.js'

test('Should throw exception', () => {
  try {
    foo();
  } catch (e) {
    expect(e.message).toBe('Something wrong');
  }
});

Enter fullscreen mode Exit fullscreen mode

Contudo esse teste não seria muito eficiente, uma vez que ele poderia passar quando nenhuma exceção está sendo lançada. Além disso, o Jest nos possibilita fazer algo muito mais simples, sem a necessidade de escrever uma estrutura tão grande.

Funções Síncronas sem parâmetros (foo)

Esse é o caso mais simples. Basta passar a função como parâmetro para o expect e utilizar o método toThrow (ou similares). Nesse caso, teríamos algo assim:

import { foo } from './funcs.js'

test('Should throw exception', () => {
  expect(foo).toThrow();
});
Enter fullscreen mode Exit fullscreen mode

Caso queira verificar o tipo de erro, você pode utilizar o toThrowError:

expect(foo).toThrowError(new Error('Something wrong'));

Funções Síncronas com parâmetros (boo)

Aqui temos uma pequena diferença. Vamos chamar a função com um dado que deve gerar exceções dentro de uma função anônima e passar esta para o expect:

import { boo } from './funcs.js'

test('When n < 0, expect to throw Error ', () => {
  expect(() => boo(-1)).toThrow();
});
Enter fullscreen mode Exit fullscreen mode

Funções assíncronas (goo, doo)

Aqui teremos duas mudanças. A primeira é que a função passada para o test/it deve ser assíncrona. A outra é que vamos chamar a função diretamente dentro do expect e testar se a Promise será rejeitada e retornará um erro:

import { goo, doo } from './funcs.js'

test('Expect to throw Error ', async () => {
  await expect(goo()).rejects.toThrow();
});

test('When n < 0, expect to throw Error ', async () => {
  await expect(doo(-1)).rejects.toThrow();
});
Enter fullscreen mode Exit fullscreen mode

Nas funções assíncronas podemos realizar chamadas diretamente pelo fato delas não lançarem a exceção diretamente, mas retornarem Promises rejeitadas. Caso o mesmo seja feito para funções síncronas, o teste será interrompido e não funcionará corretamente.

Top comments (4)

Collapse
 
fabimendes profile image
Fabi Mendes • Edited

Júnior, estou iniciando nos testes e talvez você possa me ajudar... Tenho uma função de login com try/catch que recebe um email e um password, se o e-mail e senha não batem ela exibe um alert na tela. Montei o teste para essa função, passando os parâmetros que o backend aceita, mas o teste quebra no catch. Vou colá-los aqui:

Função login:

"async function Login(loginData) {
try {
const response = await api.post('/user/login', loginData);
setUser(response.data);
localStorage.setItem('@App:user', JSON.stringify(response.data));
localStorage.setItem('@App:token', response.data.id);
api.defaults.headers.Authorization = Bearer ${response.data.id};
} catch(error) {
alert(Usuário ou senha inválidos!);
}
}"

Teste:

"describe('Login function', () => {
it('Set the user to logged', () => {
const TestComponent = () => {
const { Login, logged } = useAuth();
const loginData = {email:'teste@front.com', password:'teste123'};
return (
<>

{logged.toString()}

</>
);
}
render();
expect(screen.getByTestId('status')).toHaveTextContent('false')
fireEvent.click(screen.getByTestId('button'))
expect(screen.getByTestId('status')).toHaveTextContent('true')
})
});"

Sabe me dizer o que está errado? Agradeço demais o help, é muito difícil achar conteúdo sobre testes, principalmente para funções com providers..rsrs

Collapse
 
dotmendes profile image
Júnior Mendes

Oi Fabi, tudo bem? Percebi que nesse trecho que tu me mandou, não tem nenhum mock pra API do Axios. Isso foi apenas por simplicidade ou de fato está batendo no backend durante o teste? Talvez sso poderia fazer com que a chamada pra API sempre retorne um erro.

De toda forma, o intuito seria testar apenas a lógica da função Login? Penso que poderia ser mais simples, se for o caso, testar ela diretamente, sem usar esse TestComponent.

Collapse
 
fabimendes profile image
Fabi Mendes

Então, fiz um teste com mock pra testar se a função Login está sendo chamada ao clicar no botão, esta parte está ok.
Mas eu queria testar neste caso o estado do usuário, se após os dados serem validados o status dele passa para logged=true. Fiz a função enviado os dados para bater no backend(api) para testar essa validação... Tenho pouca experiência com testes, talvez eu esteja testando algo indevido também, rsrsrs
Este era um passo de um teste que me enviaram onde pediam testes de snapshot com jest...não consegui visualizar como o snapshot poderia se encaixar neste contexto, então tentei fazer assim...rs

Collapse
 
ricardomorato profile image
Ricardo Morato Rocha

Ótimo conteúdo Júnior! Ansioso para mais!