Articles

Testando Workflow para Componentes Web

Quando você envia algo para ser usado por outros, você assume a responsabilidade de entregar um código seguro e estável. Uma maneira de resolver isso é testando seu código.

Não importa quão pequeno – não importa quão simples seja seu projeto, o ideal é que sempre haja testes.

Sim, eu sei que a realidade bate forte e haverá muitos casos em que isso não acontecerá – mas você deve sempre se esforçar para ter testes

Disclaimer

Neste tutorial, vamos fazer uma versão simples de um elemento de entrada. Ao final dele, você ganhará habilidades e conhecimento para colocar ferramentas de teste em open-wc para praticar; e construir um componente de entrada sólido, acessível e bem testado.

Aviso

Este é um tutorial detalhado mostrando algumas armadilhas e casos difíceis quando se trabalha com componentes web. Isto é definitivamente para usuários mais avançados. Você deve ter um conhecimento básico sobre LitElement e JSDoc Types. Ter uma idéia do que Mocha, Chai BDD, Karma é pode ajudar um pouco também.

Estamos pensando em postar uma versão mais fácil de digerir, então se isso é algo que você gostaria de ver – deixe-nos saber nos comentários.

Se você quiser jogar junto – todo o código está no github.

Vamos Começar!

Executar na sua consola

$ npm init @open-wc# Results in this flow✔ What would you like to do today? › Scaffold a new project✔ What would you like to scaffold? › Web Component# Select with space! "Testing" => just enter will move one with no selection✔ What would you like to add? › Testing✔ Would you like to scaffold examples files for? › Testing✔ What is the tag name of your application/web component? … a11y-input✔ Do you want to write this file structure to disk? › YesWriting..... done✔ Do you want to install dependencies? › No

Para mais detalhes por favor veja https://open-wc.org/testing/.

Delete src/A11yInput.js

Modificar src/a11y-input.js a:

e test/a11y-input.test.js a:

Nossos testes até agora consistem em uma única característica (a propriedade label) e uma única asserção (expect). Estamos usando karma e a sintaxe BDD do chai, então agrupamos conjuntos de testes (it) sob as características ou APIs com as quais eles se relacionam (describe).

Vejamos se tudo funciona corretamente, executando: npm run test.

Fantástico – tal como esperado (🥁), temos um teste falhado 🙂

Passemos para o modo de relógio, que irá executar os testes continuamente sempre que você fizer alterações no seu código.

npm run test:watch

O seguinte código foi adicionado no vídeo acima a src/a11y-input.js:

static get properties() { return { label: { type: String }, };}constructor() { super(); this.label = '';}

Até agora, tudo bem? Ainda estás connosco? Ótimo! Vamos aumentar um pouco o jogo…

Adicionando um Teste para o DOM das Sombras

Vamos adicionar uma afirmação para testar o conteúdo da raiz da sombra do nosso elemento.

Se queremos ter a certeza de que o nosso elemento se comporta/desaparece da mesma forma, devemos certificar-nos de que a sua estrutura de dom se mantém igual.
Então vamos comparar o verdadeiro dom das sombras com o que queremos que ele seja.

Como esperado, obtemos:

Então vamos implementar isso em nosso elemento.

render() { return html` <slot name="label"></slot> <slot name="input"></slot> `;}

Interessando, o teste deve ser verde… mas não é 🤔 Vamos dar uma olhada.

Você deve ter notado aqueles comentários vazios estranhos <!----> tags. São marcadores que lit-html usa para lembrar onde estão as partes dinâmicas, para que possa ser atualizado eficientemente. Para testar, no entanto, isto pode ser um pouco irritante para lidar com.

Se usarmos innerHTML para comparar o DOM, teríamos de confiar na simples igualdade de strings. Nessas circunstâncias, teríamos que combinar exatamente o espaço em branco do DOM gerado, comentários, etc; em outras palavras: terá que ser uma combinação perfeita. Realmente tudo o que precisamos testar é que os elementos que queremos renderizar são renderizados. Queremos testar o conteúdo semântico da raiz da sombra.

Então vamos experimentar 💪

// old:expect(el.shadowRoot.innerHTML).to.equal(`...`);// new:expect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);

Bam 🎉

a11y input ✔ has by default an empty string as a label ✔ has a static shadowDom

Como funciona o shadowDom.to.equal()?

  1. Obtém o innerHTML da raiz da sombra
  2. Para-a (na verdade, o navegador analisa-o – não é necessária nenhuma biblioteca)
  3. Normaliza-o (potencialmente cada tag/propriedade na sua própria linha)
  4. Passa e normaliza a string HTML esperada
  5. Passa ambas as strings DOM normalizadas para a função de comparação padrão da chai
  6. Em caso de falha, grupos, e exibe quaisquer diferenças de forma clara

Se quiser saber mais, consulte a documentação do semantic-dom-diff.

Testando o DOM “Light”

Podemos fazer exatamente a mesma coisa com o DOM light. (O DOM que será fornecido pelo nosso usuário ou pelos nossos padrões, ou seja, o elemento children).

E vamos implementá-lo.

Então nós testamos nossa luz e sombra dom 💪 e nossos testes correm limpos 🎉

Note: Usar o DOM API em um ciclo de vida de um elemento iluminado é um anti-padrão, porém para permitir a11y pode ser um caso de uso real – de qualquer forma é ótimo para fins ilustrativos

Usando Nosso Elemento em um App

Então agora que temos uma entrada básica a11y vamos usá-la – e testá-la – em nossa aplicação.

Again we start with a skeleton src/my-app.js

E o nosso teste em test/my-app.test.js;

Executar o teste => falha e depois adicionamos a implementação a src/a11y-input.js

render() { return html` <h1>My Filter App</h1> <a11y-input></a11y-input> `;}

Mas oh não! Isso agora deve ser verde…

O que está a acontecer?
Você se lembra que tivemos um teste específico para garantir o dom da luz de um11y-input?
Então mesmo que os usuários só coloquem <a11y-input></a11y-input> no seu código – o que realmente sai é

<a11y-input> <label slot="label"></label> <input slot="input"></a11y-input>

e.g. a11y-input está realmente criando nós dentro da sua my-app dom da sombra. Pretérito! Para o nosso exemplo aqui dizemos que é isso que queremos.
Então como podemos ainda testá-lo?

Felizmente .shadowDom tem outro ás na manga; permite-nos ignorar partes do dom.

expect(el).shadowDom.to.equal(` <h1>My Filter App</h1> <a11y-input></a11y-input>`, { ignoreChildren: });

Podemos até especificar também as seguintes propriedades:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalmente ou para tags específicas)

Para mais detalhes por favor veja semantic-dom-diff.

Teste de instantâneos

Se você tem muitas árvores grandes de cúpula escrevendo/mantendo todas aquelas esperanças escritas manualmente vai ser difícil.
Para ajudar você com que há instantâneos semi/automáticos.

Então se mudarmos o nosso código

// fromexpect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);// toexpect(el).shadowDom.to.equalSnapshot();

Se executarmos agora npm run test irá criar um ficheiro __snapshots__/a11y input.md e preenchê-lo com algo como isto

# `a11y input`#### `has a static shadowDom```html<slot name="label"></slot><slot name="input"></slot>``

O que escrevemos antes à mão pode agora ser auto-gerado no init ou forçosamente via npm run test:update-snapshots.

Se o ficheiro __snapshots__/a11y input.md já existir, irá compará-lo com o output e você receberá erros se o seu output html for alterado.

Para mais detalhes por favor veja semantic-dom-diff.

Acho que agora é o suficiente sobre comparar árvores dom…
Está na hora de uma mudança 🤗

Cobertura de código

Outra métrica útil que obtemos quando testamos com a configuração open-wc é cobertura de código.
Então o que significa e como podemos obtê-lo? A cobertura de código é uma medida de quanto do nosso código é verificado pelos testes. Se houver uma linha, instrução, função ou ramo (por exemplo if/else instrução) que nossos testes não cobrem nossa pontuação de cobertura será afetada.
Um simples npm run test é tudo o que precisamos e você terá o seguinte:

=============================== Coverage summary ===============================Statements : 100% ( 15/15 )Branches : 100% ( 0/0 )Functions : 100% ( 5/5 )Lines : 100% ( 15/15 )================================================================================

O que significa que 100% das instruções, ramos, funções e linhas do nosso código são cobertos por testes. Muito legal!

Então vamos por outro caminho e acrescente o código a src/a11y-input.js antes de adicionar um teste. Digamos que queremos aceder ao valor do nosso input directamente através do nosso elemento personalizado e sempre que o seu valor for ‘cat’ queremos registar algo.

É um resultado muito diferente

A nossa cobertura é muito mais baixa do que antes. Nosso comando de teste falha, mesmo que todos os testes sejam executados com sucesso.
Isto porque por padrão a configuração open-wc’s define um limite de 90% para cobertura de código.

Se queremos melhorar a cobertura precisamos adicionar testes – então vamos fazer isso

uh oh 😱 queríamos melhorar a cobertura, mas agora precisamos corrigir um bug real primeiro 😞

Isso foi inesperado… à primeira vista, eu não sei o que isso significa… melhor verificar alguns nós reais e inspecioná-los no navegador.

Depuração no Navegador

Quando executamos nosso teste com relógio, o karma configura um ambiente de navegador persistente para executar testes.

  • Tenha certeza que você começou com npm run test:watch
  • visita http://localhost:9876/debug.html

Você deve ver algo como isto

Você pode clicar no botão de jogo circulado para executar apenas um teste individual.

Então vamos abrir as ferramentas de desenvolvimento do cromo (F12) e colocar um depurador no código de teste.

Dang… o erro acontece mesmo antes desse ponto…
Erros “fatais” como este são um pouco mais difíceis pois não são testes falhados mas uma espécie de derretimento completo do seu componente completo.

Ok, vamos colocar algum código no setter diretamente.

set value(newValue) { debugger;

Alright, isso funcionou para que a nossa consola cromada escrevemos console.log(this)> vejamos o que temos aqui

<a11y-input> #shadow-root (open)</a11y-input>

>

Ahhh aí a temos – a dom-sombra ainda não é renderizada quando o setter é chamado.
Então vamos ser seguros e adicionar uma verificação antes de

set value(newValue) { if (newValue === 'cat') { console.log('We like cats too :)'); } if (this.inputEl) { this.inputEl.value = newValue; }}

O erro fatal desapareceu 🎉
Mas agora temos um teste de falha 😭

✖ can set/get the input value directly via the custom elementAssertionError: expected '' to equal 'foo'

Pode ser necessária uma mudança de tática 🤔
Podemos adicioná-lo como uma propriedade separada value e sincronizar quando necessário.

E estamos finalmente de volta ao negócio! 🎉

>

ok bug corrigido – podemos voltar à cobertura? obrigado 🙏

Voltar à cobertura

Com este teste adicionado fizemos alguns progressos.

No entanto ainda não estamos totalmente lá – a questão é porquê?

Para descobrir abrir coverage/index.html no seu browser. Não é necessário um servidor web, basta abrir o ficheiro no seu browser – num mac pode fazer isso a partir da linha de comandos com open coverage/index.html

Verá algo como isto

Após clicar em a11y-input.js obterá uma informação linha a linha com que frequência foram executados.
Assim podemos ver imediatamente quais as linhas que ainda não foram executadas pelos nossos testes.

Então vamos adicionar um teste para isso

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 75% ( 3/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Com isso, estamos de volta aos 100% nas declarações mas ainda temos algo em falta nos ramos.
Vejamos porquê?

Este E significa else path not taken.
Então, sempre que a função update é chamada há sempre uma propriedade value nas propriedades alteradas.

Temos também labelPortanto é uma boa ideia testá-la. 👍

boom 100% 💪 ganhamos 🥇

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 100% ( 4/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Mas espere, nós nem terminamos o teste acima – o código ainda é

 // somehow check that console.log was called

Como é que temos 100% de cobertura de teste?

Vamos primeiro tentar entender como funciona a cobertura do código 🤔
A forma como a cobertura do código é medida é aplicando uma forma de instrumentation. Em resumo, antes do nosso código ser executado ele é alterado (instrumented) e comporta-se algo como isto:

Nota: Esta é uma versão super simplificada para fins ilustrativos.

Basicamente, o seu código fica repleto de muitas bandeiras. Baseado em quais bandeiras são acionadas uma estatística é criada.

Então 100% de cobertura de teste significa que cada linha que você tem no seu código foi executada pelo menos uma vez após todos os seus testes terem terminado. Isso não significa que você testou tudo, ou se seus testes fazem as asserções corretas.

Então, mesmo que já tenhamos 100% de cobertura de código, ainda assim vamos melhorar nosso teste de log.

Você deve, portanto, ver a cobertura de código como uma ferramenta que só lhe dá orientação e ajuda na localização de alguns testes que faltam, ao invés de uma dura garantia de qualidade do código.

Espiando no Código

Se você quiser verificar com que frequência ou com que parâmetros uma função é chamada, isso é chamado de espionagem.
open-wc recomenda o venerável pacote sinon, que fornece muitas ferramentas para espionagem e outras tarefas relacionadas.

npm i -D sinon

Então você cria um espião em um objeto específico e então você pode verificar com que freqüência ele é chamado.

Uh oh… o teste falha:

AssertionError: expected 0 to equal 1

Messing with global objects like console might have side effects so let’s better refactor using a dedicated log function.

This result in no global object in our test code – sweet 🤗

No entanto, ainda temos o mesmo erro. Vamos depurar… boohoo aparentemente update não está sincronizado – uma suposição errada que eu fiz 🙈 Estou dizendo que suposições são perigosas com bastante frequência – ainda assim eu caio nela de vez em quando 😢.

Então o que podemos fazer? Infelizmente não parece haver uma api pública para fazer algumas ações de sincronização acionadas por uma atualização de propriedade.
Vamos criar um problema para ela https://github.com/Polymer/lit-element/issues/643.

Por enquanto, aparentemente, a única maneira é confiar em uma api privada. 🙈
Tambem, precisamos mover a sincronização de valores para updated para que ela seja executada após cada renderização de dom.

e aqui está o teste actualizado para o registo

aaa, isso foi um pouco mais difícil do que o esperado mas nós fizemo-lo 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Executando Testes Sem o Karma Framework

O Karma framework é poderoso e rico em características, mas às vezes podemos querer pare-down o nosso regimento de testes. O bom de tudo o que propusemos até agora é que usamos apenas módulos padrão do navegador sem necessidade de transposição, com a única exceção de especificadores de módulos nus.
Então, apenas criando um test/index.html.

e abrindo-o via owc-dev-server em cromo, ele funcionará perfeitamente.
Temos tudo funcionando sem webpack ou karma – doce 🤗

Faz a Coisa do Cross-Browser

Sentimo-nos agora bastante confortáveis com a nossa componente web. É testado e coberto; há apenas mais um passo – queremos ter a certeza que corre e é testado em todos os browsers.

Então vamos executá-lo

npm run test:bsSUMMARY:✔ 42 tests completedTOTAL: 42 SUCCESS

Yeah, isso funciona muito bem! 🤗

Se houver testes falhados, ele os emitirá no resumo com o browser específico onde falhou.

Se você precisar depurar um browser em particular:

Tal como se você quiser ajustar o browser que é testado você pode ajustar o seu karma.bs.config.js.

Por exemplo, se você quiser adicionar o Firefox ESR à sua lista.

ou talvez você queira testar apenas 2 navegadores específicos?

Nota: Isto usa as estratégias de fusão do webpack substitua.

Quick Recap

  • Testar é importante para cada projeto. Certifique-se de escrever o máximo que puder.
  • Tente manter a sua cobertura de código alta, mas lembre-se que não é uma garantia mágica, por isso nem sempre precisa ser 100%.
  • Debugar no navegador via npm run test:watch. Para navegadores antigos, use npm run test:legacy.watch.

O que se segue?

  • Executar os testes no seu CI (funciona perfeitamente bem em conjunto com o browserstack). Veja nossas recomendações em automatizar.

Siga-nos no Twitter, ou siga-me no meu Twitter pessoal.
Não deixe de verificar nossas outras ferramentas e recomendações em open-wc.org.

Pascal e Benny por seu feedback e por ajudar a transformar meus rabiscos em uma história que possa ser seguida.