Dia 11 — Testes de componente

Informatica · Conteudo · publicado em 05/10/2026
Dia 11 de 16

Testes de componente

Aula 1

O que é teste de componente

O que é teste de componente

Teste automatizado é código que afirma que o código continua funcionando.

A pergunta que separa o teste útil do inútil é o que ele verifica: o que o

componente faz — um botão que chama a função, um campo que mostra o erro —

ou como ele foi escrito. O primeiro quebra quando o defeito entra; o

segundo quebra quando o código é reescrito sem defeito nenhum. Teste de

componente é do primeiro tipo, e a regra é: **verifica comportamento, não

estrutura**.

O exemplo desta página monta um item de lista com nome do cliente e um botão, e

escreve três testes sobre ele. O primeiro procura "ana" na árvore e passa; o

terceiro monta outro item com outro cliente, procura "bruno" e passa, e procura

"ana" de novo — e falha. É a falha proposital do exemplo, e ela é o argumento

inteiro da aula: um teste que procura texto que não está mais na tela reprova, e

reprovar é a função dele.

A consequência prática está no nome do teste. "Renderiza" não diz nada — quase

tudo renderiza. "Mostra o nome do cliente que recebeu" diz o que quebraria se o

cliente sumisse da tela. Um teste que falha quando alguém muda o nome de uma

variável interna está testando a implementação, e o próximo a mexer nele vai

apagar o teste em vez de concertar o código.

A última linha da saída do exemplo é essa regra escrita como fechamento: o teste

que falha é o que diz o nome do cliente, não o nome de uma variável interna. O

nome do caso é a documentação de quem vai ler a falha daqui a seis meses.

o que um teste de componente precisa

Três peças, e todas precisam existir para o teste rodar: a biblioteca de teste

que monta o componente sem aparelho, um matcher que compara, e o

expect que liga um ao outro. A biblioteca de React Native monta a árvore de

componentes e guarda os nós para que o teste possa procurar por texto e

disparar evento; não é o aparelho, é a árvore.

As três peças do exemplo são visíveis no código: a função renderizar monta o

componente e devolve uma tela com dois métodos, textos() percorre a árvore

coletando todo Text de conteúdo string, e apertar() procura o Pressable e

chama o onPress. O expect é a terceira peça, com toBe e toContain.

O ganho real aparece no ciclo de mudança. Sem teste, quem altera a tela precisa

aparelho, cabo e olho humano. Com teste, npm test responde em segundos e

diz exatamente qual texto sumiu. É o que permite mexer em tela antiga sem medo

de quebrar o que ninguém lembrava que existia.

O segundo teste do exemplo é o que mede esse ganho: ele confirma que apertar

devolveu true e que o contador de toques chegou a 1. Nenhum aparelho, nenhum

cabo — duas linhas de afirmação contra uma árvore de JavaScript. E o contador

existe justamente para mostrar o limite do parágrafo de cima: ele diz **quantas

vezes, e não com o que**.

o que não testar

Trivialidade não é teste: formatarMoeda(10) devolvendo "R$ 10,00" não

precisa de teste, porque o próprio editor já reclama se a função deixar de

devolver texto. Pior, ele vira obstáculo: alguém melhora a formatação e o

teste quebra, e a tentação passa a ser mexer no teste em vez do código.

Detalhe de estilo também não: se o Text tem fontSize: 16 é decisão visual,

não comportamento, e o teste de estilo trava o design na mesma velocidade que

o editor trava a lógica. E teste que depende de tempo — esperar três segundos

para o dado chegar — é teste que vai falhar sozinho na máquina lenta de outra

pessoa.

O exemplo não tem teste de estilo de propósito: o ItemPedido recebe

flexDirection: 'row' e padding: 12, e nenhum dos dois aparece nas

afirmações. O que aparece é o nome na tela e o argumento da chamada. É a

distinção entre o que o componente faz e o desenho, e o teste cobre só a

primeira metade.

O contraexemplo útil é o terceiro teste do exemplo. Ele monta o componente com

outro cliente e afirma duas coisas: que o nome novo apareceu e que o antigo não.

A segunda afirmação é a que impede o teste de passar em silêncio quando o

componente começa a mostrar dado fixo em vez do dado recebido — o defeito mais

comum em tela de lista, e o que o queryByText com toBeNull pega.

Exemplo

// Teste de componente verifica o que o componente FAZ, nao como ele foi
// escrito. O exemplo escreve um mini motor de teste: as afirmacoes que
// o aluno vai usar na aula seguinte, rodando sobre a arvore montada.
const { View, Text, Pressable, StyleSheet } = require('react-native');

// (1) um `expect` minimo: compara e guarda a falha.
function expect(recebido) {
  return {
    toBe(esperado) {
      const passou = recebido === esperado;
      console.log(passou ? '  passou' : '  FALHOU', '| recebido:', JSON.stringify(recebido), '| esperado:', JSON.stringify(esperado));
      return passou;
    },
    toContain(esperado) {
      const passou = Array.isArray(recebido)
        ? recebido.includes(esperado)
        : String(recebido).includes(esperado);
      console.log(passou ? '  passou' : '  FALHOU', '| procurou:', JSON.stringify(esperado));
      return passou;
    },
  };
}

// (2) o componente sob teste: um item de lista com um botao.
const estilos = StyleSheet.create({ linha: { flexDirection: 'row', padding: 12 } });
let toques = 0;

function ItemPedido(props) {
  return (
    <View style={estilos.linha}>
      <Text>{props.pedido.cliente}</Text>
      <Pressable onPress={props.aoAbrir}>
        <Text>abrir</Text>
      </Pressable>
    </View>
  );
}

// (3) o teste: monta o componente, procura o texto e dispara o evento.
function renderizar(componente, props) {
  const arvore = componente(props);
  return {
    arvore,
    textos() {
      const achados = [];
      function percorre(no) {
        if (!no || typeof no !== 'object') return;
        if (no.type === 'Text' && typeof no.props.children === 'string') achados.push(no.props.children);
        const filhos = no.props && no.props.children;
        for (const filho of Array.isArray(filhos) ? filhos : filhos ? [filhos] : []) {
          percorre(filho);
        }
      }
      percorre(arvore);
      return achados;
    },
    apertar(indice) {
      let achado = null;
      function procura(no) {
        if (achado || !no || typeof no !== 'object') return;
        if (no.type === 'Pressable' && no.props.onPress) achado = no.props.onPress;
        const filhos = no.props && no.props.children;
        for (const filho of Array.isArray(filhos) ? filhos : filhos ? [filhos] : []) procura(filho);
      }
      procura(arvore);
      if (achado) achado();
      return typeof achado === 'function';
    },
  };
}

console.log('teste: mostra o cliente que recebeu');
const tela = renderizar(ItemPedido, { pedido: { id: 'p41', cliente: 'ana' }, aoAbrir: () => { toques += 1; } });
expect(tela.textos()).toContain('ana');
expect(tela.textos()).toContain('abrir');

console.log('teste: o botao chama a funcao que recebeu');
expect(tela.apertar(0)).toBe(true);
expect(toques).toBe(1);

console.log('teste: outro cliente, outra saida');
const outro = renderizar(ItemPedido, { pedido: { id: 'p42', cliente: 'bruno' }, aoAbrir: () => { toques += 1; } });
expect(outro.textos()).toContain('bruno');
expect(outro.textos()).toContain('ana');   // FALHA de proposito: `ana` nao esta aqui

console.log('total de toques ate aqui:', toques);
console.log('o teste que falha e o que diz o nome do cliente, nao o nome de uma variavel interna');

Saída real

teste: mostra o cliente que recebeu
  passou | procurou: "ana"
  passou | procurou: "abrir"
teste: o botao chama a funcao que recebeu
  passou | recebido: true | esperado: true
  passou | recebido: 1 | esperado: 1
teste: outro cliente, outra saida
  passou | procurou: "bruno"
  FALHOU | procurou: "ana"
total de toques ate aqui: 1
o teste que falha e o que diz o nome do cliente, nao o nome de uma variavel interna
Aula 2

Escrever e rodar o teste

Escrever e rodar o teste

A forma é sempre a mesma: describe agrupa, it descreve o caso e expect

afirma. O que muda entre testes é o que precisa ser preparado antes e o que

precisa ser conferido depois, e essas duas partes respondem por

beforeEach e afterEach.

describe('ItemPedido', () => {
  beforeEach(() => { toques = 0; });

  it('mostra o nome do cliente', () => {
    const tela = render(<ItemPedido pedido={pedido} />);
    expect(tela.getByText('ana')).toBeOnTheScreen();
  });

  it('chama a funcao ao apertar', () => {
    const aoAbrir = jest.fn();
    const tela = render(<ItemPedido pedido={pedido} aoAbrir={aoAbrir} />);
    fireEvent.press(tela.getByText('abrir'));
    expect(aoAbrir).toHaveBeenCalled();
  });
});

beforeEach existe porque teste que depende do anterior é teste que falha

quando a ordem muda. Resetar a lista antes de cada caso custa uma linha e

remove uma classe inteira de bug. afterEach é para o que precisa ser

desfeito — fechar servidor, limpar armazenamento.

O exemplo desta página traz um mini motor com essa forma, e a saída sai com o

nome do grupo e o nome de cada caso — describe(ItemPedido), e dentro dele

mostra o nome do cliente, chama a funcao com o id do pedido, `cada caso

comeca com zero chamadas e troca a camada de rede por um mock`. São quatro

casos agrupados em um bloco, e é assim que a suíte inteira se lê: um nome de

componente por grupo, uma frase de comportamento por caso.

matcher de verdade e simulação de toque

getByText procura o nó pelo texto e falha se não achar — é o que torna o

teste útil: ele não passa em silêncio quando o texto sumiu. queryByText é o

oposto, e existe para afirmar ausência: expect(queryByText('erro')).toBeNull().

A função getByText do exemplo devolve o primeiro nó que casa e null quando

não casa — e o null é o que o toBeNull afirma. No primeiro caso, procurar

"bruno" numa tela que mostra "ana" devolve null, e a afirmação passa. É a

forma de escrever "esse texto não está aqui" sem que o teste quebre antes de

chegar à conclusão.

Disparar toque é fireEvent.press, e ele recebe o elemento, não o texto. A

combinação é fireEvent.press(tela.getByText('abrir')). pressEvent é a

versão que monta um evento completo de toque, e userEvent é a mais próxima do

toque real — mais lenta, e o que se usa quando a interação tem sequência, como

digitar e enviar.

fireEventPress no exemplo percorre a árvore procurando o Pressable com

onPress de função e o chama — o mesmo caminho que a biblioteca de teste percorre

na tela real. A diferença em relação ao getByText é que o toque não recebe

texto: recebe o nó. Por isso a combinação da documentação é press mais

getByText, e não press('abrir').

jest.fn() é a função que registra chamada: expect(aoAbrir).toHaveBeenCalled(),

toHaveBeenCalledTimes(2), toHaveBeenCalledWith('p41'). É o que substitui a

variável contador e mostra com o que a função foi chamada — que é onde está

a informação que o contador esconde.

A diferença entre os dois matchers aparece na saída do segundo caso: o

toHaveBeenCalledTimes imprime 1 chamada(s), esperado 1, e o

toHaveBeenCalledWith imprime chamou com ["p41"]. O contador diria só "uma

chamada"; o segundo matcher diz uma chamada, com o id do pedido. É a

diferença entre um teste que reprova quando o id errado é enviado e um que

reprova só quando a quantidade muda.

mock é o mesmo mecanismo em outra escala: substituir a camada de rede por

uma função que devolve o dado combinado. Um teste que depende de servidor real

não é teste, é integração com produção.

O quarto caso do exemplo é o mock em ação, e ele falha de propósito: o

toBe recebe "retorno de [object Object]" e espera "retorno de undefined". O

que ele expõe é o defeito clássico do mock mal combinado — a função espiã

devolveu o objeto inteiro onde a camada real devolveria outra coisa. A linha

seguinte registra o que o mock deveria fazer, e é o conserto do caso.

o comando

npm test roda a suíte. O ganho aparece no dia em que a suíte leva oito

segundos e a verificação manual leva quatro minutos. E o filtro por nome é o

que salva no meio do caminho: rodar um arquivo, ou um caso, em vez da suíte

inteira.

A última linha da saída do exemplo repete isso como fechamento: roda-se com

npm test, e o filtro por nome roda um caso só. Os dois caminhos importam em

momentos diferentes — a suíte inteira no fim do trabalho, o filtro no meio,

quando a falha está em um caso e o resto da tela já está salvo.

Exemplo

// A forma do teste: `describe` agrupa, `it` descreve, `expect` afirma,
// `beforeEach` prepara e `fireEvent` dispara o toque. O exemplo traz um
// mini motor com essa forma, rodando sobre a arvore montada.
const { View, Text, Pressable, StyleSheet } = require('react-native');

const estilos = StyleSheet.create({ linha: { flexDirection: 'row', padding: 12 } });

// (1) as funcao que o teste registra e depois confere.
function criarEspiao() {
  const chamadas = [];
  const fn = (...argumentos) => {
    chamadas.push(argumentos);
    return 'retorno de ' + (argumentos[0] === undefined ? 'nada' : argumentos[0]);
  };
  fn.chamadas = chamadas;
  return fn;
}

function expect(recebido) {
  const mostrar = (rotulo, passou, detalhe) => {
    console.log((passou ? '  passou  ' : '  FALHOU  ') + rotulo, detalhe === undefined ? '' : '| ' + detalhe);
    return passou;
  };
  return {
    toBe(esperado) {
      return mostrar('toBe', recebido === esperado, 'recebido ' + JSON.stringify(recebido) + ', esperado ' + JSON.stringify(esperado));
    },
    toHaveBeenCalled() {
      return mostrar('toHaveBeenCalled', recebido.chamadas.length > 0, recebido.chamadas.length + ' chamada(s)');
    },
    toHaveBeenCalledTimes(n) {
      return mostrar('toHaveBeenCalledTimes', recebido.chamadas.length === n, recebido.chamadas.length + ' chamada(s), esperado ' + n);
    },
    toHaveBeenCalledWith(valor) {
      const passou = recebido.chamadas.some((c) => c[0] === valor);
      return mostrar('toHaveBeenCalledWith', passou, 'chamou com ' + JSON.stringify(recebido.chamadas.map((c) => c[0])));
    },
    toBeNull() {
      return mostrar('toBeNull', recebido === null || recebido === undefined, 'valor: ' + JSON.stringify(recebido));
    },
  };
}

function run(nomeDoGrupo, casos) {
  console.log('\ndescribe(' + nomeDoGrupo + ')');
  for (const caso of casos) caso();
}

function it(nome, funcao) {
  console.log('  it(' + nome + ')');
  funcao();
}

// (2) `fireEvent.press` percorre a arvore e chama o `onPress` que achar.
function fireEventPress(arvore) {
  let alvo = null;
  function percorre(no) {
    if (alvo || !no || typeof no !== 'object') return;
    if (no.type === 'Pressable' && typeof no.props.onPress === 'function') alvo = no.props.onPress;
    const filhos = no.props && no.props.children;
    for (const filho of Array.isArray(filhos) ? filhos : filhos ? [filhos] : []) percorre(filho);
  }
  percorre(arvore);
  if (alvo) alvo();
  return typeof alvo === 'function';
}

function getByText(arvore, texto) {
  const achados = [];
  function percorre(no) {
    if (!no || typeof no !== 'object') return;
    if (no.type === 'Text' && no.props.children === texto) achados.push(no);
    const filhos = no.props && no.props.children;
    for (const filho of Array.isArray(filhos) ? filhos : filhos ? [filhos] : []) percorre(filho);
  }
  percorre(arvore);
  return achados[0] || null;
}

// (3) o componente sob teste.
function ItemPedido(props) {
  return (
    <View style={estilos.linha}>
      <Text>{props.pedido.cliente}</Text>
      <Pressable onPress={() => props.aoAbrir(props.pedido.id)}>
        <Text>abrir</Text>
      </Pressable>
    </View>
  );
}

// (4) `beforeEach`: o estado que o teste modifica comeca limpo.
let aoAbrir;
const pedido = { id: 'p41', cliente: 'ana' };

function antesDeCada() {
  aoAbrir = criarEspiao();
}

run('ItemPedido', [
  function mostraCliente() {
    it('mostra o nome do cliente', function () {
      antesDeCada();
      const arvore = ItemPedido({ pedido, aoAbrir });
      expect(getByText(arvore, 'ana') !== null).toBe(true);
      expect(getByText(arvore, 'bruno')).toBeNull();
    });
  },

  function disparaToque() {
    it('chama a funcao com o id do pedido', function () {
      antesDeCada();
      const arvore = ItemPedido({ pedido, aoAbrir });
      fireEventPress(arvore);
      expect(aoAbrir).toHaveBeenCalledTimes(1);
      expect(aoAbrir).toHaveBeenCalledWith('p41');
    });
  },

  function isolaCadaCaso() {
    it('cada caso comeca com zero chamadas', function () {
      antesDeCada();
      expect(aoAbrir).toHaveBeenCalledTimes(0);
      const arvore = ItemPedido({ pedido, aoAbrir });
      fireEventPress(arvore);
      fireEventPress(arvore);
      expect(aoAbrir).toHaveBeenCalledTimes(2);
    });
  },

  function dependeDoMock() {
    it('troca a camada de rede por um mock', function () {
      antesDeCada();
      const servidor = criarEspiao();
      const resposta = servidor({ id: 'p41', cliente: 'ana' });
      expect(resposta).toBe('retorno de undefined');
      // com `mock` da rede, o teste nao depende de servidor nenhum
      console.log('  o `mock` devolve o dado combinado e o teste roda sem rede');
    });
  },
]);

console.log('\nroda-se com `npm test`; o filtro por nome roda um caso so');

Saída real

describe(ItemPedido)
  it(mostra o nome do cliente)
  passou  toBe | recebido true, esperado true
  passou  toBeNull | valor: null
  it(chama a funcao com o id do pedido)
  passou  toHaveBeenCalledTimes | 1 chamada(s), esperado 1
  passou  toHaveBeenCalledWith | chamou com ["p41"]
  it(cada caso comeca com zero chamadas)
  passou  toHaveBeenCalledTimes | 0 chamada(s), esperado 0
  passou  toHaveBeenCalledTimes | 2 chamada(s), esperado 2
  it(troca a camada de rede por um mock)
  FALHOU  toBe | recebido "retorno de [object Object]", esperado "retorno de undefined"
  o `mock` devolve o dado combinado e o teste roda sem rede

roda-se com `npm test`; o filtro por nome roda um caso so