No meu
artigo anterior, falei sobre a experiência de usar o mecanismo
Gemini para desenvolver testes visuais, ou melhor, testes de regressão visual. Esses testes verificam se algo “mudou” na interface do usuário após as próximas alterações, comparando as capturas de tela atuais com as de referência corrigidas anteriormente. Desde então, muita coisa mudou em nossas abordagens para escrever testes visuais, incluindo o mecanismo usado. Agora usamos
Hermione , mas neste artigo vou falar não apenas e não tanto sobre Hermione, mas sobre os problemas que se acumularam desde então e como resolvê-los, o que, entre outras coisas, levou à transição para um novo mecanismo.
Em primeiro lugar, embora os testes funcionassem e com bastante sucesso, não tínhamos um entendimento claro do que era coberto pelos testes e do que não era. Havia, é claro, alguma idéia do grau de cobertura, mas não a medimos quantitativamente. Em segundo lugar, a composição dos testes aumentou com o tempo e testes diferentes frequentemente testavam a mesma coisa, porque em capturas de tela diferentes, alguma parte coincidia com a mesma parte, mas em uma captura de tela diferente. Como resultado, mesmo pequenas alterações no CSS podem sobrecarregar muitos testes ao mesmo tempo e exigir a atualização de um grande número de padrões. Em terceiro lugar, um tema sombrio apareceu em nosso produto e, para cobri-lo de alguma forma com testes, alguns testes foram selecionados seletivamente para usar um tema sombrio, o que também não adicionava clareza ao problema ao determinar o grau de cobertura.
Otimização de desempenho
Estranhamente, começamos com desempenho otimizado. Eu vou explicar o porquê. Nossos testes visuais são baseados no
Storybook . Cada história do livro de histórias não é um componente único, mas um "bloco" inteiro (por exemplo, uma grade com uma lista de entidades, um cartão de entidade, diálogo ou mesmo o aplicativo como um todo). Para exibir este bloco, você precisa “bombear” a história com dados, não apenas os dados exibidos para o usuário, mas também o estado dos componentes usados dentro do bloco. Essas informações são armazenadas junto com o código-fonte na forma de arquivos json que contêm uma representação serializada do estado do aplicativo (repositório redux). Sim, esses dados são, para dizer o mínimo, redundantes, mas simplificam bastante a criação de testes. Para criar um novo teste, basta abrir o cartão, a lista ou o diálogo desejado no aplicativo, tirar uma foto do estado atual do aplicativo e serializá-lo em um arquivo. Em seguida, adicionamos uma nova história e testes que fazem capturas de tela dessa história (todas em poucas linhas de código).
Essa abordagem inevitavelmente aumenta o tamanho do pacote. O grau de duplicação de dados nele apenas "passa". Ao executar testes, o mecanismo gemini executa cada suíte de testes em uma sessão separada do navegador. Cada sessão carrega o pacote novamente e o tamanho do pacote nesse esquema está longe do último valor.
Para reduzir o tempo de execução do teste, reduzimos o número de suítes de teste aumentando o número de testes neles. Assim, um conjunto de testes pode afetar várias histórias de uma só vez. Nesse esquema, praticamente perdemos a capacidade de “rastrear” apenas uma determinada área da tela devido ao fato de o Gemini permitir que você defina a área de captura de tela apenas para o conjunto de testes como um todo (embora a API permita que você faça isso antes de cada captura de tela, mas, na prática, não funciona).
A incapacidade de limitar a área da captura de tela nos testes levou à duplicação de informações visuais nas imagens de referência. Embora não houvesse muitos testes, esse problema não parecia significativo. Sim, e a interface do usuário não mudou com muita frequência. Mas isso não poderia durar para sempre - uma reformulação apareceu no horizonte.
Olhando para o futuro, direi que em Hermione uma área de captura de tela pode ser definida para cada cena e, à primeira vista, mudar para um novo mecanismo resolveria todos os problemas. Mas ainda teríamos que “esmagar” grandes suítes de teste. O fato é que os testes visuais não são inerentemente estáveis (isso pode ser devido a várias razões, por exemplo, com defasagens na rede, usando animações ou com "clima em Marte") e é muito difícil fazer sem tentativas automáticas. Gêmeos e Hermione realizam novas tentativas para a suíte de testes como um todo, e quanto mais espessa for a suíte de testes, menor a probabilidade de concluir com êxito durante as tentativas, como na próxima execução, os testes que foram concluídos anteriormente com êxito podem cair. Para conjuntos de testes espessos, tivemos que implementar um esquema de repetição alternativo incorporado ao mecanismo Gemini e realmente não queremos fazer isso novamente ao mudar para um novo mecanismo.
Portanto, para acelerar o carregamento do conjunto de testes, dividimos o pacote monolítico em partes, alocando cada instantâneo do estado do aplicativo em uma "peça" separada, carregada "sob demanda" para cada matéria separadamente. O código de criação da história agora se parece com isso:
Para criar uma história, o componente StoryProvider é usado (seu código será fornecido abaixo). Os instantâneos são carregados usando a função de
importação dinâmica . Histórias diferentes diferem entre si apenas em imagens de estados. Para um tema sombrio, sua própria história é gerada, usando o mesmo instantâneo da história para um tema claro. No contexto de um livro de histórias, fica assim:
O componente StoryProvider aceita um retorno de chamada para carregar uma captura instantânea na qual a função import () é chamada. A função import () funciona de forma assíncrona, portanto você não pode capturar uma captura de tela imediatamente após carregar a história - corremos o risco de remover o vazio. Para capturar o momento do final do download, o provedor renderiza o elemento DOM do marcador sinalizando o mecanismo de teste durante todo o tempo do download, que deve ser adiado com a captura de tela:
Além disso, para reduzir o tamanho do pacote, desative a adição de mapas de origem ao pacote. Mas, para não perder a capacidade de depurar a história (você nunca sabe o que), fazemos isso sob a condição:
.storybook / webpack.config.js O
script build-storybook do npm run compila um livro de histórias estático sem o mapa de origem na pasta estática do livro de histórias. É usado ao executar testes. E o
script de livro de histórias npm run é usado para desenvolver e depurar histórias de teste.
Eliminação da duplicação de informações visuais
Como eu disse acima, o Gemini permite definir seletores de área de captura de tela para o conjunto de testes como um todo, o que significa que, para resolver completamente o problema de duplicar informações visuais nas capturas de tela, teríamos que criar nosso próprio conjunto de testes para cada captura de tela. Mesmo levando em consideração a otimização do carregamento da história, ela não parecia muito otimista em termos de velocidade e pensamos em mudar o mecanismo de teste.
Na verdade, por que Hermione? Atualmente, o repositório Gemini está marcado como obsoleto e, mais cedo ou mais tarde, tivemos que "mudar" para algum lugar. A estrutura do arquivo de configuração Hermione é idêntica à estrutura do arquivo de configuração Gemini e pudemos reutilizar essa configuração. Os plugins Gêmeos e Hermione também são comuns. Além disso, pudemos reutilizar a infraestrutura de teste - máquinas virtuais e implantamos a grade de selênio.
Ao contrário de Gêmeos, Hermione não está posicionada como uma ferramenta apenas para testes de regressão de layout. Seus recursos de manipulação de navegador são muito mais amplos e limitados apenas pelos recursos do
Webdriver IO . Em combinação com o
mocha, esse mecanismo é conveniente para usar mais em testes funcionais (simulando ações do usuário) do que em testes de layout. Para teste de regressão do layout, Hermione fornece apenas o método assertView (), que compara uma captura de tela de uma página do navegador com uma referência. A captura de tela pode ser limitada à área especificada usando seletores de css.
Para o nosso caso, o teste para cada história individual ficaria assim:
O método waitForVisible (), apesar do nome, permite esperar não apenas a aparência, mas também a ocultação do elemento, se você definir o segundo parâmetro como true. Aqui, usamos para aguardar a ocultação de um elemento marcador, indicando que o instantâneo de dados ainda não foi carregado e a história ainda não está pronta para uma captura de tela.
Se você tentar encontrar o método waitForVisible () na documentação da Hermione, não encontrará nada. O fato é que o
método waitForVisible ()
é o método da API Webdriver IO . O método url (), respectivamente, também. No método url (), passamos o endereço do quadro de uma história específica, não o livro de histórias inteiro. Primeiro, isso é necessário para que a lista de histórias não seja exibida na janela do navegador - não precisamos testá-la. Em segundo lugar, se necessário, podemos ter acesso aos elementos DOM dentro do quadro (os métodos webdriverIO permitem executar o código JavaScript no contexto do navegador).
Para simplificar a gravação dos testes, criamos nosso invólucro sobre os testes mocha. O fato é que não há um sentido particular na elaboração detalhada de casos de teste para teste de regressão. Todos os casos de teste são iguais - 'deve ser igual a etalon'. Bem, também não quero duplicar o código para aguardar o carregamento de dados em cada teste. Portanto, o mesmo trabalho para todos os testes "monkey" é delegado à função wrapper, e os próprios testes são escritos de forma declarativa (bem, quase). Aqui está o texto desta função:
create-test-suite.js const themes = [ 'default', 'dark' ]; const rootClassName = '.explorer'; const loadingStubClassName = '.loading-stub'; const timeout = 2000; function createTestSuite(testSuite) { const { name, storyName, browsers, testCases, selector } = testSuite;
Um objeto que descreve o conjunto de testes é passado para a entrada da função. Cada suíte de teste é criada de acordo com o seguinte cenário: fazemos uma captura de tela do layout principal (por exemplo, uma área de um cartão de entidade ou uma área de uma lista de entidades), depois pressionamos programaticamente botões que podem levar à aparência de outros elementos (por exemplo, painéis pop-up ou menus de contexto) e “capturamos uma captura de tela »Cada um desses elementos separadamente. Assim, simulamos ações do usuário no navegador, mas não com o objetivo de testar um cenário de negócios, mas simplesmente para "capturar" o número máximo possível de componentes visuais. Além disso, a duplicação de informações visuais nas capturas de tela é mínima, porque as capturas de tela são feitas "no sentido horário" usando seletores. Exemplo de suíte de teste:
Determinação da cobertura
Então, como descobrimos a velocidade e a redundância, resta descobrir a eficácia de nossos testes, ou seja, determinar o grau de cobertura do código com os testes (aqui, por código, quero dizer folhas de estilo CSS).
Para histórias de teste, selecionamos empiricamente os cartões, listas e outros elementos mais complicados a serem preenchidos para cobrir o maior número possível de estilos com uma captura de tela. Por exemplo, para testar um cartão de entidade, foram selecionados cartões com um grande número de tipos diferentes de controles (texto, número, transferências, datas, grades etc.). Os cartões para diferentes tipos de entidades têm especificidades próprias; por exemplo, um painel com uma lista de versões de documentos pode ser exibido em um cartão de documentos e a correspondência nessa tarefa é exibida no cartão de tarefas. Dessa forma, para cada tipo de entidade, sua própria história e um conjunto de testes específicos para esse tipo etc. foram criados. No final, achamos que tudo parecia coberto de testes, mas queríamos um pouco mais de confiança do que "gostar".
Para avaliar a cobertura no Chrome DevTools, existe uma ferramenta com o nome Cobertura muito adequada para este caso:

A cobertura permite determinar quais estilos ou qual código js foi usado ao trabalhar com a página do navegador. O relatório sobre o uso de listras verdes indica o código usado, vermelho - não usado. E tudo ficaria bem se tivéssemos uma aplicação do nível "olá, mundo", mas o que fazer quando tivermos milhares de linhas de código? Os desenvolvedores de cobertura entenderam isso muito bem e forneceram a capacidade de exportar o relatório para um arquivo que já pode ser trabalhado programaticamente.
Devo dizer imediatamente que até agora não encontramos uma maneira de coletar o grau de cobertura automaticamente. Teoricamente, isso pode ser feito usando o navegador sem cabeça do aluno, mas o aluno não funciona sob o controle do selênio, o que significa que não poderemos reutilizar o código de nossos testes. Então, por enquanto, vamos pular esse tópico extremamente interessante e trabalhar com canetas.
Depois de executar os testes no modo manual, obtemos um relatório de cobertura, que é um arquivo json. No relatório para cada css, js, ts, etc. o arquivo indica seu texto (em uma linha) e os intervalos do código usado neste texto (na forma de índices de caracteres dessa linha). Abaixo está uma parte do relatório:
cobertura.json [ { "url": "http://localhost:6006/theme-default.css", "ranges": [ { "start": 0, "end": 8127 } ], "text": "... --theme_primary-accent: #5b9bd5;\r\n --theme_primary-light: #ffffff;\r\n --theme_primary: #f4f4f4;\r\n ..." }, { "url": "http://localhost:6006/main.css", "ranges": [ { "start": 0, "end": 610 }, { "start": 728, "end": 754 } ] "text": "... \r\n line-height:1;\r\n}\r\n\r\nol, ul{\r\n list-style:none;\r\n}\r\n\r\nblockquote, q..." ]
À primeira vista, não há nada difícil em encontrar seletores de css não utilizados. Mas então o que fazer com essa informação? De fato, na análise final, precisamos encontrar seletores não específicos, mas componentes que esquecemos de cobrir com os testes. Os estilos de um componente podem ser definidos por mais de uma dúzia de seletores. Como resultado, com base nos resultados da análise do relatório, obtemos centenas de seletores não utilizados e, se você lidar com cada um deles, poderá gastar muito tempo.
Aqui, expressões regulares nos ajudam. Obviamente, eles só funcionarão se as convenções de nomenclatura para classes css forem cumpridas (em nosso código, as classes css são nomeadas de acordo com a metodologia BEM - block_name_name_name_modifier). Usando expressões regulares, calculamos os valores exclusivos dos nomes dos blocos, que não são mais difíceis de associar aos componentes. Obviamente, também estamos interessados em elementos e modificadores, mas não em primeiro lugar, primeiro precisamos lidar com um “peixe” maior. Abaixo está um script para processar um relatório de cobertura
cobertura.js const modules = require('./coverage.json').filter(e => e.url.endsWith('.css')); function processRange(module, rangeStart, rangeEnd, isUsed) { const rules = module.text.slice(rangeStart, rangeEnd); if (rules) { const regex = /^\.([^\d{:,)_ ]+-?)+/gm; const classNames = rules.match(regex); classNames && classNames.forEach(name => selectors[name] = selectors[name] || isUsed); } } let previousEnd, selectors = {}; modules.forEach(module => { previousEnd = 0; for (const range of module.ranges) { processRange(module, previousEnd, range.start, false); processRange(module, range.start, range.end, true); previousEnd = range.end; } processRange(module, previousEnd, module.length, false); }); console.log('className;isUsed'); Object.keys(selectors).sort().forEach(s => { console.log(`${s};${selectors[s]}`); });
Executamos o script primeiro colocando o arquivo de cobertura.json exportado do Chrome DevTools e gravando o escape em um arquivo .csv:
nó cobertura.js> cobertura.csvVocê pode abrir esse arquivo usando o Excel e analisar os dados, incluindo a determinação da porcentagem de cobertura de código pelos testes.

Em vez de um currículo
O uso do livro de histórias como base para testes visuais se justificou completamente - temos um grau suficiente de cobertura do código css com testes com um número relativamente pequeno de histórias e custos mínimos para a criação de novos.
A transição para um novo mecanismo nos permitiu eliminar a duplicação de informações visuais nas capturas de tela, o que simplificou bastante o suporte aos testes existentes.
O grau de cobertura do código css é mensurável e, de tempos em tempos, é monitorado. Obviamente, existe uma grande questão - como não esquecer a necessidade desse controle e como não perder algo no processo de coleta de informações sobre cobertura. Idealmente, eu gostaria de medir o grau de cobertura automaticamente em cada execução de teste, para que, quando o limite especificado for atingido, os testes caiam com um erro. Vamos trabalhar nisso, se houver novidades, eu definitivamente direi a você.