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()?
- Obtém o
innerHTML
da raiz da sombra - Para-a (na verdade, o navegador analisa-o – não é necessária nenhuma biblioteca)
- Normaliza-o (potencialmente cada tag/propriedade na sua própria linha)
- Passa e normaliza a string HTML esperada
- Passa ambas as strings DOM normalizadas para a função de comparação padrão da chai
- 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 label
Portanto é 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, usenpm 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.