(in) Guerra Finita

guerra infinita


Nós temos um problema. O problema com o teste. O problema ao testar os componentes do React é bastante fundamental. É sobre a diferença entre unit testing e integration testing . É sobre a diferença entre o que chamamos de teste de unidade e o que chamamos de teste de integração, o tamanho e o escopo.


Não é sobre o teste em si, mas sobre a Arquitetura de Componentes. Sobre a diferença entre componentes de teste, bibliotecas independentes e aplicativos finais.


Todo mundo sabe como testar componentes simples (eles são simples), provavelmente sabe como testar aplicativos (E2E). Como testar coisas finitas e infinitas ...


Definir o problema


Existem duas maneiras diferentes de testar o React Component - shallow e tudo o mais, incluindo mount , react-testing-library webdriver , webdriver e assim por diante. Apenas shallow é especial - o resto se comporta da mesma maneira.


E essa diferença é sobre o tamanho e o escopo - sobre O QUE seria testado, e apenas parcialmente como .


Em resumo - shallow apenas gravará chamadas para React.createElement, mas não executará efeitos colaterais, incluindo renderização de elementos DOM - é um efeito colateral (algébrico) de React.createElement.


Qualquer outro comando executará o código que você forneceu, com todos os efeitos colaterais também sendo executados. Como seria real, e esse é o objetivo.


E o problema é o seguinte: you can NOT run each and every side effect .


Porque não


Pureza da função? Pureza e imutabilidade - as vacas sagradas de hoje. E você está matando um deles. Os axiomas do teste de unidade - sem efeitos colaterais, isolamento, zombaria, tudo sob controle.


  • Mas isso não é um problema para ... dumb components . Eles são burros, contêm apenas a camada de apresentação, mas não os "efeitos colaterais".


  • Mas isso é um problema para Containers . Contanto que eles não sejam burros, contenham o que quiserem, e totalmente sobre efeitos colaterais. Eles são o problema!



Provavelmente, se definirmos as regras de "O componente certo", poderíamos testar facilmente - ele nos guiará e nos ajudará.


TRDL: O componente finito

Componentes inteligentes e mudos


De acordo com Dan Abramov Article Presentation Components são:


  • Preocupa-se com a aparência das coisas.
  • Pode conter componentes de apresentação e de contêiner ** internos e, geralmente, possui alguns estilos e marcação DOM.
  • Freqüentemente, permita a contenção por this.props.children.
  • Não tem dependências no restante do aplicativo, como ações ou lojas do Flux.
  • Não especifique como os dados são carregados ou alterados.
  • Receba dados e retornos de chamada exclusivamente via adereços.
  • Raramente têm seu próprio estado (quando o fazem, é o estado da interface do usuário em vez de dados).
  • São gravados como componentes funcionais, a menos que precisem de estado, ganchos de ciclo de vida ou otimizações de desempenho.
  • Exemplos: Página, Barra Lateral, História, Informações do Usuário, Lista.
  • ....
  • E Containers são apenas fornecedores de dados / objetos para esses componentes.

De acordo com as origens: Na aplicação ideal ...
Recipientes são a árvore. Componentes são folhas de árvores.


Encontre o gato preto no quarto escuro


O molho secreto aqui, uma mudança que precisamos alterar nesta definição, está oculto dentro de "Pode conter componentes de apresentação e de recipiente ** " , deixe-me citar o artigo original:


Em uma versão anterior deste artigo, afirmei que os componentes de apresentação devem conter apenas outros componentes de apresentação. Não acho mais esse o caso. Se um componente é um componente de apresentação ou um contêiner é seu detalhe de implementação. Você poderá substituir um componente de apresentação por um contêiner sem modificar nenhum dos sites de chamada. Portanto, os componentes de apresentação e de container podem conter outros componentes de apresentação ou de container.

Ok, mas e a regra, que torna a unidade de componentes de apresentação testável - "Não há dependências no restante do aplicativo" ?


Infelizmente, ao incluir contêineres nos componentes da apresentação, você está tornando os segundos infinitos e injetando dependência no restante do aplicativo.


Provavelmente, isso não é algo que você deveria fazer. Portanto, não tenho outra escolha, a não ser tornar o componente burro finito:


COMPONENTES DE APRESENTAÇÃO DEVEM CONTER APENAS OUTROS COMPONENTES DE APRESENTAÇÃO


E a única pergunta, você deve perguntar (olhando para sua base de códigos atual): Como? : tableflip:?!


Hoje, os componentes e contêineres da apresentação não são apenas enredados, mas às vezes não são extraídos como entidades "puras" (olá GraphQL).


Solução 1 - DI


A solução 1 é simples - não contenha contêineres aninhados no componente burro - contém slots . Apenas aceite "conteúdo" (filhos), como adereços, e isso resolveria o problema:


  • você pode testar o componente burro sem "o resto do seu aplicativo"
  • você pode testar a integração com fumaça / integração / e2e test, não testes.

 // Test me with mount, with "slots emty". const PageChrome = ({children, aside}) => ( <section> <aside>{aside}</aside> {children} </section> ); // test me with shallow, or real integration test const PageChromeContainer = () => ( <PageChrome aside={<ASideContainer />}> <Page /> </PageChrome> ); 

Aprovado pelo próprio Dan:
{% twitter 1021850499618955272%}


O DI (injeção de dependência e inversão de dependência), provavelmente, é a técnica mais reutilizável aqui, capaz de tornar sua vida muito, muito mais fácil.


Aponte aqui - Componentes mudos são mudos!

Solução 2 - Limites


Esta é uma solução bastante declarativa e pode estender a Solution 1 - basta declarar todos os pontos de extensão . Basta envolvê-los com ... Boundary


 const Boundary = ({children}) => ( process.env.NODE_ENV === 'test' ? null : children // or `jest.mock` ); const PageChrome = () => ( <section> <aside><Boundary><ASideContainer /></Boundary></aside> <Boundary><Page /></Boundary> </section> ); 

Então - você pode desativar, apenas zero, o Boundary para reduzir o escopo do componente e torná-lo finito .


Aponte aqui - o limite está no nível do componente Dumb. O componente Dumb está controlando o quão Dumb é.

Solução 3 - Camada


É o mesmo que a Solução 2, mas com um limite mais inteligente, capaz de simular camada , camada ou o que você diz:


 const checkTier = tier => tier === currentTier; const withTier = tier => WrapperComponent => (props) => ( (process.env.NODE_ENV !== 'test' || checkTier(tier)) && <WrapperComponent{...props} /> ); const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); const ASideContainer = withTier('UI')(...) const Page = withTier('Page')(...) const PageChromeContainer = withTier('UI')(PageChrome); 

Mesmo que isso seja quase semelhante ao exemplo de Limite - o componente Dumb é Dumb e os Containers controlam a visibilidade de outros Containers.

Solução 4 - Preocupações separadas


Outra solução é apenas separar preocupações! Quero dizer - você já fez isso, e provavelmente é hora de utilizá-lo.


Ao connect componente ao Redux ou ao GQL, você está produzindo Containers conhecidos . Quero dizer - com nomes conhecidos - Container(WrapperComponent) . Você pode zombar deles pelo nome

 const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); // remove all components matching react-redux pattern reactRemock.mock(/Connect\(\w\)/) // all any other container reactRemock.mock(/Container/) 

Essa abordagem é um pouco grosseira - vai limpar tudo , dificultando o teste de Contaiers, e você pode usar uma zombaria um pouco mais complexa para manter o "primeiro":


 import {createElement, remock} from 'react-remock'; // initially "open" const ContainerCondition = React.createContext(true); reactRemock.mock(/Connect\(\w\)/, (type, props, children) => ( <ContainerCondition.Consumer> { opened => ( opened ? ( // "close" and render real component <ContainerCondition.Provider value={false}> {createElement(type, props, ...children)} <ContainerCondition.Provider> ) // it's "closed" : null )} </ContainerCondition.Consumer> ) 

Aponte aqui: não há lógica interna nem Apresentação, não Contêiner - toda lógica está externa.

Solução de bônus - preocupações separadas


Você pode manter um acoplamento rígido usando defaultProps e anular esses adereços em testes ...


 const PageChrome = ({Content = Page, Aside = ASideContainer}) => ( <section> <aside><Aside/></aside> <Content/> </section> ); 

Então


Acabei de publicar algumas maneiras de reduzir o escopo de qualquer componente e torná-lo muito mais testável. A maneira simples de tirar uma gear da gearbox de gearbox . Um padrão simples para facilitar sua vida.


Os testes E2E são ótimos, mas é difícil simular algumas condições, que podem ocorrer dentro de um recurso profundamente aninhado e estar pronto para eles. Você precisa ter testes de unidade para poder simular diferentes cenários. Você precisa ter testes de integração para garantir que tudo esteja conectado corretamente.


Você sabe, como Dan escreveu em outro artigo :


Por exemplo, se um botão puder estar em um dos 5 estados diferentes (normal, ativo, pairar, perigo, desabilitado), o código que atualiza o botão deve estar correto para 5 × 4 = 20 transições possíveis - ou proibir algumas delas. Como domar a explosão combinatória de possíveis estados e tornar a saída visual previsível?

Enquanto a solução certa aqui são as máquinas de estado, ser capaz de escolher um único átomo ou molécula e brincar com ele - é o requisito básico.


Os principais pontos deste artigo


  1. Os componentes de apresentação devem conter apenas outros componentes de apresentação.
  2. Recipientes são a árvore. Componentes são folhas de árvores.
  3. Você nem sempre precisa conter contêineres dentro dos de apresentação, mas não os contém apenas em testes.

Você pode se aprofundar no problema lendo o artigo da mídia , mas aqui vamos pular todo o açúcar.

PS: Esta é uma tradução da versão do artigo do ru-habr .

Source: https://habr.com/ru/post/pt436692/


All Articles