Redux: Não é possível fazer referência a contêineres envolvidos em um provedor ou conectar-se com o Enzyme

Criado em 20 mar. 2016  ·  51Comentários  ·  Fonte: reduxjs/redux

Não consigo fazer referência a nada envolto em um <Provider> e um connect

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
const Component = ({...}) => (<div id="abc"></div>);
export default connect(..., ...)(Component);

Eu segui: https://github.com/reactjs/redux/issues/1481 até os exemplos onde os testes foram escritos com enzima, porém, os containers nunca são testados neles. Então não sei se devo/posso testar contêineres inteligentes?

@fshowalter Alguma ideia?

Comentários muito úteis

Mesmo que não esteja diretamente relacionado ao Redux. Resolvi isso chamando shallow() no contêiner novamente. Ele renderiza o componente interno com o estado de armazenamento passado para ele.

Exemplo:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

Espero que isto ajude

Todos 51 comentários

Nem connect() nem Provider fazem parte desta biblioteca. É mais fácil discutir e rastrear esses problemas quando eles são arquivados no repositório apropriado: https://github.com/reactjs/react-redux.

Acho que isso realmente faz sentido. Como você está renderizando superficialmente, apenas o componente Provider será renderizado - seu ContainerComponent será apenas deixado em seu formulário de saída de objeto ou o que quer que a renderização superficial produza.

Dois pensamentos aqui:

Primeiro, observe que o componente wrapper gerado por connect() realmente procura props.store antes de procurar context.store , portanto, se você realmente sentir que precisa testar um componente conectado , você deve ser capaz de renderizar <ConnectedComponent store={myTestStore} /> sem precisar se preocupar com provedor ou contexto ou qualquer coisa.

A segunda pergunta é se você realmente precisa se preocupar em testar o componente totalmente conectado. O argumento que eu vi é que se você pode testar seu componente "simples" com props específicos, e você pode testar sua implementação mapStateToProps , você pode assumir com segurança que react-redux irá juntá-los corretamente, e não precisa realmente testar a própria versão conectada.

@gaearon você está certo, desculpe. Não sabia se deveria aumentar isso em react ou repo de enzimas.

@markerikson O motivo para testar o componente inteligente é mapToDispatchProps onde eu queria ter certeza de que o despachante correto foi chamado pelo componente encapsulado. Apenas passar a loja para o ConnectedComponent significa que não vou testar o mapeamento de estado ou despachar funções de retorno de chamada.

Vou pensar mais sobre isso e levantar um problema no repositório relevante, se precisar. Obrigado pela ajuda.

@mordra : quando você diz "o despachante certo foi chamado", você realmente quer dizer "criador de ação certo"?

Você poderia fazer algo assim (a sintaxe provavelmente está desativada, mas você deve ter a ideia):

let actionSpy = sinon.spy();

let wrapper = shallow(<PlainComponent someActionProp={actionSpy} />);
wrapper.find(".triggerActionButton").simulate("click");
expect(actionSpy.calledOnce).to.be.true;
expect(actionSpy.calledWith({type : ACTION_I_WAS_EXPECTING)).to.be.true;

Em outras palavras, renderize seu componente simples, passe espiões para os adereços que seriam ações retornadas de mapDispatch e acione qualquer comportamento que o componente precise para fazê-lo chamar essas ações.

Além disso, de acordo com meu comentário anterior: você deve ser capaz de testar seus mapStateToProps e mapDispatchToProps separadamente e se sentir seguro de que o React-Redux os chamará adequadamente, sem ter que tentar testar a versão conectada si mesmo para verificar se é esse o caso.

@markerikson Não posso fazer o que você está sugerindo porque meu PlainComponent não sabe sobre ações ou criadores de ações. Não sei se esta é a maneira correta de fazer isso, mas se você olhar para:
https://github.com/mordra/cotwmtor/blob/master/client/charCreation/charCreation.jsx
você verá que meu mapDispatchToProps contém toda a lógica:

const mapDispatchToProps = (dispatch) => {
  return {
    onCompleted      : (player) => {
      Meteor.call('newGame', player, function (data) {
        console.log('new game return: ' + data);
      });
      dispatch(routeActions.push('/game'));
      dispatch({type: "INIT_GAME", map: generateAreas(), buildings: generateBuildings(dispatch)});
    },
    onCancelled      : () => dispatch(routeActions.push('/')),
    onChangeName     : input => dispatch(actions.changeName(input)),
    onChangeGender   : gender => dispatch(actions.setGender(gender)),
    onSetDifficulty  : lvl => dispatch(actions.setDifficulty(lvl)),
    onChangeAttribute: (attr, val) => dispatch(actions.setAttribute(attr, val))
  }
};

então se eu espiasse os adereços passados ​​para o PlainComponent, eu não seria capaz de testar se as ações corretas são chamadas, mas apenas os parâmetros passados ​​para os criadores da ação estão corretos.
Eu precisaria testar o componente connect separadamente.

Ah. Isso me ajuda a entender mais.

Então, com a ressalva de que eu realmente tenho muito pouca experiência prática em escrever testes, alguns pensamentos:

  • O que você realmente tem são criadores de ação, eles não são definidos separadamente porque você deseja acesso a dispatch . Isso não é muito facilmente testável por si só. Você provavelmente gostaria de testar o comportamento deles também e, para fazer isso, gostaria de defini-los como suas próprias funções fora de mapDispatch .
  • Todos eles parecem bons candidatos para uso com redux-thunk, o que certamente tornaria mais fácil definir essas funções por conta própria e permitir que eles acessassem dispatch .
  • Seu PlainComponent _sabe_ sobre "ações", ou pelo menos retornos de chamada neste caso, em que você está passando isso, e em algum lugar em seu componente você está executando this.props.onSetDifficulty("HARD") ou algo assim. Quando sugeri passar espiões para ações, esse é o tipo de coisa que eu estava sugerindo substituir. Então, se você passou um spy por onSetDifficulty , você pode verificar se seu componente o chamou e passou um valor aceitável para o nível de dificuldade.

Em última análise, acho que você deve ser capaz de tornar as coisas muito mais testáveis, pegando essas funções definidas em mapDispatch e definindo-as separadamente. Então você não terá que se preocupar em ter que ter mapDispatch conectado para testar seu componente corretamente, e pode se concentrar se o componente acabou de chamar um determinado retorno de chamada de prop com os argumentos corretos ou algo assim.

Além disso, do ponto de vista do componente: em última análise, ele não está realmente preocupado se {action: CHANGE_NAME, name : "Fred"} foi despachado. Tudo o que sabe é que chamou this.props.onChangeName("Fred") , e _that_ é o que você deveria estar tentando testar - que chamou um retorno de chamada prop da maneira certa.

@mordra no seu caso, eu exportaria mapDispatchToProps e testaria isoladamente. Você pode verificar se os nomes das propriedades estão todos corretos e passar um espião para enviar para testar seus criadores de ação.

Dito isso, costumo evitar mapDispatchToProps em favor de mapActionCreatorsToProps que mapeia criadores de ação já formados que posso facilmente testar isoladamente. Nesse caso, apenas testei se todos os meus adereços estão definidos para evitar erros de digitação na importação.

Por fim, observe que você pode usar renderização normal (não superficial). Você precisaria de jsdom ou de um navegador real para isso, mas então você pode renderizar qualquer nível de profundidade.

Não sabia se deveria aumentar isso em react ou repo de enzima

Desculpe, eu não quis dizer repositórios React ou Enzyme. Eu quis dizer React Redux , que é a biblioteca que você está usando.

Obrigado pela ajuda caras, isso é um monte de informação para eu ir e digerir. @markerikson @fshowalter Dei mais uma olhada e vou aceitar sua sugestão de exportar o mapState/Dispatch e testá-los separadamente, faz sentido testar os callbacks que evocam as ações esperadas.

O componente @gaearon Stateless não é renderizado sem superficial e apenas examinando os problemas, parece haver problemas ao renderizar componentes com estado que aninharam componentes sem estado, então fiz uma nota mental para evitar esse caminho por enquanto.

Não tenho certeza de quais problemas você está se referindo. Você pode usar renderização superficial com componentes funcionais muito bem. A renderização superficial funciona apenas em um nível de profundidade (não importa como o componente é definido). Esta é a sua principal característica—não é recursiva para que os testes de componentes permaneçam independentes. Se você tiver problemas específicos com renderização superficial que possa reproduzir, registre bugs no repositório React. Obrigado!

@gaearon : para esclarecer, se eu fizer isso:

let wrapper = shallow(<A><B /></A>);

B será renderizado ou não? Porque eu acho que essa é a pergunta original - tentando renderizar um <Provider> em torno de um componente conectado para testar a conexão.

Mesmo que não esteja diretamente relacionado ao Redux. Resolvi isso chamando shallow() no contêiner novamente. Ele renderiza o componente interno com o estado de armazenamento passado para ele.

Exemplo:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import events from 'events';

const mockStore = configureMockStore([ thunk ]);
const storeStateMock = {
  myReducer:{
    someState: 'ABC'
  }
};

let store;
let component;
describe('tests for MyContainerComponent', () => {
  beforeEach(() => {
    store = mockStore(storeStateMock);
    component = shallow(<MyContainerComponent store={store} />).shallow();
  });

  it('renders container', () => {
    expect(component.find('div.somediv')).to.have.length.of(1);
  });
  ...
});

Espero que isto ajude

Devemos fazer uma maneira mais acessível de fazer isso por meio de um módulo personalizado, talvez?

Edit: Então podemos declarar connect como um método para Enzyme, como component = shallowWithConnect()

@ev1stensberg Esta é uma ótima ideia. Decidimos se esta é a abordagem e/ou o trabalho começou sobre isso? Se não, eu adoraria contribuir.

Para quem se deparar com esse problema no futuro, aqui está a abordagem que funciona para mim: apenas teste o componente simples por si só. Exporte a definição do componente como uma exportação nomeada e o componente conectado (para uso em seu aplicativo) como a exportação padrão. Voltando ao código da pergunta original:

// test
let component = shallow(<Provider store={store}><ContainerComponent /></Provider>);
component.find('#abc'); // returns null

let component = shallow(<Provider store={store}><div id="abc"></div></Provider>);
component.find('#abc'); // returns the div node

// ContainerComponent
export const Component = ({...}) => (<div id="abc"></div>); // I EXPORT THIS, TOO, JUST FOR TESTING
export default connect(..., ...)(Component);

Depois é só fazer

const component = shallow(<Component {...props} />)
// test component

E como outros apontaram acima, se você também tiver cobertura para as funções que você passa para conectar, você deve ter cobertura total para o seu componente como um todo.

Bem, a primeira coisa a notar aqui é a filosofia do :

  1. Comunique o estado da loja para um
  2. Modifique o estado por meio de ações de despacho com base no eventos.

Se eu estiver bem, então você só precisa testar 2 coisas:

  1. seu componente está recebendo as props corretas geradas a partir do estado (pode ser via seletores)?
  2. seu componente está despachando as ações corretas?

Então esta é a minha abordagem

import React from 'react';
import { shallow } from 'enzyme';
import { fromJS } from 'immutable';
import { createStore } from 'redux';
// this is the <Map /> container
import Map from '../index';
// this is an action handled by a reducer
import { mSetRegion } from '../reducer';
// this is an action handled by a saga
import { sNewChan } from '../sagas';
// this is the component wrapped by the <Map /> container
import MapComponent from '../../../components/Map';

const region = {
  latitude: 20.1449858,
  longitude: -16.8884463,
  latitudeDelta: 0.0222,
  longitudeDelta: 0.0321,
};
const otherRegion = {
  latitude: 21.1449858,
  longitude: -12.8884463,
  latitudeDelta: 1.0222,
  longitudeDelta: 2.0321,
};
const coordinate = {
  latitude: 31.788,
  longitude: -102.43,
};
const marker1 = {
  coordinate,
};
const markers = [marker1];
const mapState = fromJS({
  region,
  markers,
});
const initialState = {
  map: mapState,
};
// we are not testing the reducer, so it's ok to have a do-nothing reducer
const reducer = state => state;
// just a mock
const dispatch = jest.fn();
// this is how i mock the dispatch method
const store = {
  ...createStore(reducer, initialState),
  dispatch,
};
const wrapper = shallow(
  <Map store={store} />,
);
const component = wrapper.find(MapComponent);

describe('<Map />', () => {
  it('should render', () => {
    expect(wrapper).toBeTruthy();
  });

  it('should pass the region as prop', () => {
    expect(component.props().region).toEqual(region);
  });

  it('should pass the markers as prop', () => {
    expect(component.props().markers).toEqual(markers);
  });
  // a signal is an action wich will be handled by a saga
  it('should dispatch an newChan signal', () => {
    component.simulate('longPress', { nativeEvent: { coordinate } });
    expect(dispatch.mock.calls.length).toBe(1);
    expect(dispatch.mock.calls[0][0]).toEqual(sNewChan(coordinate));
  });
  // a message is an action wich will be handled by a reducer
  it('should dispatch a setRegion message', () => {
    component.simulate('regionChange', otherRegion);
    expect(dispatch.mock.calls.length).toBe(2);
    expect(dispatch.mock.calls[1][0]).toEqual(mSetRegion(otherRegion));
  });
});

@markerikson o que você quis dizer com:

você pode testar sua implementação mapStateToProps

digamos que você esteja exportando o contêiner conectado por meio de uma exportação ES6. Você não teria acesso ao mapStateToProps privado. Você estava falando de alguma outra forma ou pode ser específico?

@mordrax

Você pode verificar se os nomes das propriedades estão todos corretos

Então, se você expor seu mapStateToProps e torná-lo público, então diga que você renderiza seu ContainerComponent do seu teste, incluindo o envio de uma loja falsa e o envio de um prop de estado, então seu mapStateToProps recebe esse estado, mapeia-o para um prop. Mas então como você pode testá-lo a partir desse ponto? Connect chamará mapStateToProps mesclando assim os adereços, mas onde está a costura e qual ponto é a costura no código durante esse processo/fluxo onde você pode interceptar os adereços que estão sendo definidos no componente de apresentação? Eu acho que o raso duplo como @guatedude2 pode ser uma maneira de verificar os adereços mapeados sem costuras ...

também em seu raso.shallow. Então o componente connect() passa o que de volta, um wrapper certo? E esse wrapper o que envolve o componente de apresentação?

@granmoe você pode explicar isso com mais detalhes sobre o que você quer dizer exatamente:

se você também tiver cobertura para as funções que você passa para o connect, você deve ter cobertura total para o seu componente como um todo.

Também estou com @tugorez em termos de _o que_ quero testar e por quê.

@markerikson tão bom exemplo com o espião. É uma coisa que você pode testar, mas você precisaria de mais avaliadores para testar a "unidade de comportamento". Apenas testar que uma ação de espionagem foi chamada de mapDispatchToProps não nos conta toda a história sobre o componente contêiner. Ele não afirma o resultado esperado com base na lógica do manipulador do seu contêiner.

Não é suficiente apenas testar se você passou props ou state, você quer testar o comportamento do componente container. Isso significa testar duas coisas:

1) o componente de contêiner conectou os adereços ao componente de apresentação e, se o fez, existe um certo estado que espero que o componente de apresentação tenha uma vez que é renderizado com base nesse estado específico passado por adereços para o componente de apresentação. Quem sabe, talvez algum desenvolvedor burro apareça e adicione mais código ao mapStateToProps, você nem sempre pode confiar que ele mapeará as coisas corretamente, esse é o objetivo de testar a lógica do contêiner. Embora não haja realmente nenhuma lógica no mapStateToProps, quem sabe novamente algum desenvolvedor aparece e adiciona uma instrução if lá ... bem, esse é um comportamento que pode atrapalhar as coisas.

2) a lógica do manipulador de despacho (comportamento) funciona no contêiner.

@dschinkel :

A prática típica para definir componentes conectados ao Redux é export default connect()(MyComponent) , e também export MyComponent como uma exportação nomeada. Da mesma forma, como mapState é apenas uma função, você também pode export const mapState = () => {} e depois importá-la e testá-la separadamente.

Quanto ao teste do "componente do contêiner" versus o "componente de apresentação": o pensamento geral aqui é que você provavelmente não precisa se preocupar em testar o contêiner na maior parte, onde "contêiner" === "a saída do connect ". O React-Redux já tem um conjunto completo de testes de como isso deve se comportar, e não há razão para você duplicar esse esforço. Nós _sabemos_ que ele trata corretamente a assinatura da loja, chamando mapState e mapDispatch e passando props para o componente encapsulado.

O que _você_ como consumidor da biblioteca vai se interessar é como o _seu_ componente se comporta. Na verdade, não deveria importar se o seu próprio componente está recebendo props de um wrapper connect , um teste ou qualquer outra coisa - é apenas como esse componente se comporta com um determinado conjunto de props.

(Além disso, FWIW, eu percebo que você é absolutamente o especialista em testes e eu não, mas _parece_ que você está ficando um pouco paranóico com as coisas :) Se você está preocupado com um mapState sendo quebrado acidentalmente, então escreva testes para ele e siga em frente.)

Agora, se o seu componente encapsulado renderizar outros componentes conectados dentro dele, e especialmente se você estiver fazendo renderização completa em vez de renderização superficial, pode ser muito mais simples testar as coisas fazendo const wrapper = mount(<Provider store={testStore}><MyConnectedComponent /></Provider> . Mas, no geral, minha opinião é que na maioria das vezes você _deve_ ser capaz de testar sua função mapState e o componente "simples" separadamente, e você realmente não deveria estar tentando testar a versão conectada apenas para verificar que a saída de mapState está sendo passada para o componente simples.

Para referência, você pode querer olhar para a versão em miniatura de connect que Dan escreveu um tempo atrás para ilustrar o que ele faz internamente: https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e .

Sim, quero dizer, se você exportar mapStateToProps e dispatchStateToProps, isso seria melhor. Eu não quero testar se connect() (o colaborador no meu teste) funciona, definitivamente não. Então, essa seria uma maneira clara de fazer isso, exporte esses dois métodos:

example-container-spec.js

describe('testing mapPropsToState directly', () => {
    it.only('returns expected state', () => {
        const state = {firstName: 'Dave'};
        const output = mapStateToProps(state);

        expect(output.firstName).to.equal('Dave');
    });
});

Recipiente

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

export const mapStateToProps = (state) => ({
    firstName: state.firstName
})

export default connect(mapStateToProps)(Example);

Apresentação

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

Então, novamente, você também pode fazer algo assim com renderização _shallow_ e ainda mergulhar no componente filho:

example-container-spec.js

it('persists firstName to presentation component', () => {
        var state = {firstName: "Dave"};
        const container = mount(<ExampleContainer store={fakeStore(state)}/>),
            example = container.find(Example);

        expect(example.length).to.equal(1);
        expect(example.text()).to.equal(state.firstName);
    });

Recipiente

import React from 'react';
import { connect } from 'react-redux'
import Example from './Example';

const mapStateToProps = (state) => ({
    name: state.firstName
})

export default connect(mapStateToProps)(Example);

Apresentação

import React, { Component } from 'react';

export default class Example extends Component {
    render(){
        return (
            <div className='example'>
                {this.props.firstName}
            </div>
        )
    }
}

ainda outra maneira de fazer isso superficialmente, é fazer um duplo superficial. Shallow no pai raso irá rasoar o componente filho que está envolvendo, algo assim:

it('get child component by double shallow()', () => {
        const container = shallow(<ExampleContainer store={fakeStore({})}/>),
            presentationalChildComponent = container.find(Example).shallow().find('.example');

        expect(presentationalChildComponent).to.have.length(1);
    });

Portanto, é apenas outra maneira, você está verificando o resultado do seu componente de apresentação.

De qualquer maneira funcionaria ... no entanto, como você disse, mapStateToProps sendo exportado é provavelmente o melhor, pois é tudo o que me importa testar em termos de configuração do estado da prop.

Eu percebo que você é absolutamente o especialista em testes e eu não sou, mas parece que você está ficando um pouco paranóico com as coisas :)

não, eu não estou, testar corretamente o que significa ter bons testes que não são frágeis e estão fornecendo valor é importante. Primeiro ninguém é um _expert_. Todos estão aprendendo continuamente, alguns simplesmente não querem admitir isso por causa de seus egos. É chamado de Artesanato de Software (também conhecido como Profissionalismo).

Testar apenas o comportamento e não testar os colaboradores (connect() em si) como você disse é importante. Descobrir isso pela primeira vez e testar bem é importante. Certificar-se de que você não está testando colaboradores é importante e isso às vezes requer discussão entre outros desenvolvedores.

Você pode ter testes frágeis, e é importante não ter testes frágeis. Os testes devem ser levados a sério, e isso significa verificar continuamente se você está se aproximando corretamente. Estou simplesmente tentando descobrir o meu fluxo. I TDD, então como eu começo, é importante, e bons testes são importantes com isso. Portanto, ninguém deve se sentir "paranóico" quando procura entender diferentes maneiras de testar componentes do React.

Novo repositório de teste React chegando...
Na verdade, vou compartilhar um repositório em breve que mostra vários estilos e combinações e abordagens para testar o react-redux. Acho que vai ajudar as pessoas porque não sou o único a pensar nisso. Repetidas vezes você vê pessoas fazendo as mesmas perguntas sobre o teste do contêiner redux e outros componentes. Os documentos do Redux nem os documentos do react-redux fazem justiça a isso, falta documentação na área de testes IMO. Ele apenas arranha a superfície, e nós precisamos de um bom repositório que mostre vários estilos sobre como abordar o teste do React, então eu o colocarei em breve.

If anyone would like to contribute to that repo, please get a hold of me para que eu possa adicionar você como um colaborador nele. Eu adoraria que as pessoas adicionassem exemplos. Eu gostaria de ver exemplos com React Test Utils + mocha , com Enzyme , Ava , Jest , tape , etc.

Conclusão :

Acho que ambas as abordagens que discutimos estão bem. Teste diretamente o método, ou teste-o como eu fiz acima, mergulhe nele. Às vezes você não quer basear seus testes em um método específico porque você pode obter testes frágeis... daí porque as pessoas foram picadas no passado com testes. Portanto, testar ou não em torno de uma função, depende se isso testa a "unidade". Às vezes você não quer tornar um método público, e pode ser melhor testar o contrato acima disso e manter alguns deles privados. Portanto, é sempre importante pensar no que você está fazendo com frequência quando escreve testes. Nada de errado com isso. Escrever bons testes não é fácil.

É exatamente isso que o TDD promove, fazendo isso com frequência. Para que você acabe com um código mais enxuto, melhor e menos frágil. O TDD força você a pensar em testar antecipadamente, não depois. Essa é a diferença e por que pessoas como eu querem entender diferentes fluxos e abordagens, porque eu tenho que quando faço TDD, isso me força a reavaliar o design em pequenos pedaços com frequência, e isso significa pensar sobre minha abordagem de teste com frequência.

@markerikson obrigado por suas entradas, adoro falar de testes. Boa coisa cara. Eu não estava questionando sua experiência, apenas que eu realmente não tentei testar mapStateToProps diretamente! Você não está se saindo mal por não ser um testador;). Eu sou simplesmente novo no redux, simples assim, então terei perguntas que não apenas "tocam na superfície". Devemos discutir os testes em um nível mais baixo para que as pessoas entendam o que estão fazendo, como podem fazê-lo e se estão se beneficiando de sua abordagem. Para fazer isso, você precisa entender connect, react-redux, provedor em um nível mais baixo ... para saber como "apenas testar o comportamento". Eu não vi muitas pessoas exportando mapStateToProps, o que também é interessante para mim ... e isso me faz pensar por que não.

WeDoTDD.com
BTW para qualquer pessoa interessada, eu comecei WeDoTDD.com no ano passado (escrito em React btw). Se uma equipe específica (todos os desenvolvedores em uma equipe específica) TDDs, ou toda a sua empresa (algumas empresas têm desenvolvedores onde todos os TDDs, especialmente empresas de consultoria), entre em contato comigo em slack.wedotdd.com

Atualizar.

depois de brincar mais com testes de componentes conectados e refletir sobre o que está funcionando muito bem para mim, concordo 100% agora com a declaração de @markerikson aqui:

se você realmente sentir que precisa testar um componente conectado, poderá renderizar sem precisar se preocupar com provedor ou contexto ou qualquer coisa.

Não vi necessidade de introduzir <Provider /> em meus testes e parece que você está testando um colaborador se fizer isso quando o objetivo não é testar se <Provider /> está funcionando. Você deve testar se o comportamento em seu componente funciona e não importa se seu contêiner está obtendo a loja por meio de contexto. Apenas passe a loja como um prop..connect só precisa de alguma maneira de chegar na loja, não importa como (contexto ou props)... livrar-se do provedor completamente em seus testes.

Também não vejo a necessidade de usar a opção de contexto da enzima, que é mais uma maneira de persistir a loja através do contexto do React. Esqueça completamente o contexto do react em seus testes, é um inchaço totalmente desnecessário e torna seus testes mais complexos e muito mais difíceis de manter e ler.

Teste sua função pura mapStateToProps diretamente exportando-a em seu arquivo de contêiner. Estou usando um combo de testes mapStateToProps + também alguns testes que testam meu container superficialmente, verificando se o componente de apresentação pelo menos está recebendo os adereços que espero e então paro por aí com os adereços testando. Também decidi não fazer nenhum raso duplo para meus testes de componentes de contêiner. TDD está me dando um feedback de que ao tentar fazer isso, os testes ficam muito complicados e você provavelmente está fazendo algo errado. Esse "errado" é "não duplique coisas superficiais para seus testes de componentes de contêiner". Em vez disso, crie um novo conjunto de testes de componentes de apresentação que testam a saída de seus componentes de apresentação de forma independente.

Fico feliz em saber que você resolveu as coisas. (FWIW, meus comentários foram em grande parte em resposta às suas perguntas sobre "costuras" e "cavar adereços", o que realmente parecia ir a níveis desnecessários de detalhes para pouco ou nenhum benefício.)

Usar um <Provider> /context _is_ será necessário se você estiver fazendo uma renderização completa de um componente que renderiza outros componentes conectados, pois esses filhos conectados estarão procurando o armazenamento no contexto.

Se você tiver sugestões para melhorar os documentos atuais do Redux em testes, envie um PR.

@markerikson bom ponto na montagem. Eu só faria uma montagem se precisasse testar o ciclo de vida do componente que estou testando. Eu não o usaria para mergulhar profundamente em componentes filhos... que não se qualificariam mais como um teste de unidade, e é basicamente um teste de integração e você estaria acoplando esse teste aos detalhes de implementação quando você deve testar esses componentes filhos em seus própria isoladamente, não por meio de um componente pai.

De qualquer forma, coisas boas, obrigado!

Certo, apenas dizendo que se você _fazer_ usar mount para verificar o ciclo de vida do componente em questão, e esse componente renderizar outros componentes conectados, você _precisará_ do armazenamento disponível no contexto para esses componentes aninhados para evitar erros quando o componente que está sendo testado é renderizado.

(edit: aparentemente estou me repetindo, desculpe - os perigos de responder perguntas em vários lugares sem sempre voltar em um tópico ou mesmo rolar um pouco para cima.)

ah ok entendi! sim não pensei nisso...

Obrigado por todos os comentários aqui, @markerikson e @dschinkel! Eles são muito úteis. Agora decidi exportar meu componente desconectado e testá-lo como faria com qualquer outro, e também testar meus mapStateToProps e mapDispatchToProps separadamente. No entanto, há um caso em que esses testes não pegam, e é quando map(State/Dispatch)ToProps não é realmente passado para a chamada para connect . Exemplo:

import React from 'react';
import { connect } from 'react-redux';

export const mapStateToProps = ({ name }) => ({ name });

export const Example = ({ name }) => <h1>{name}</h1>;

export default connect({ name } => ({ name: firstName }))(Example); // Whoops, didn't actually pass in `mapStateToProps`.

Isso provavelmente nunca acontecerá na prática, mas poderia, especialmente porque os linters não reportariam mapStateToProps como uma variável não utilizada, pois está sendo exportada.

@danny-andrews Você poderia ser mais específico sobre como testar sua função mapDispatchToProps?

@leizard

Eu não testo mapDispatchToProps ou mapStateToProps diretamente. Mudei de ideia desde este tópico e aconselho a não fazer isso.

Além disso, testando-os, você encontrará o problema @danny-andrews, mas o problema é mais sobre bons testes e tentar expor essas duas funções não é uma boa ideia. Acabou o teste. Você deve testar esses dois métodos indiretamente, testando o comportamento ou testando os adereços por meio de seu contêiner raso. Descobri que não há razão para tentar expor esses métodos privados e agora percebi que também era uma prática ruim de teste tentar fazê-lo.

Então estou discordando do @markerikson , teste através do seu componente conectado.

Encontrar a API/contrato certo para testar e não testar demais, essa é a chave, baby :).

@dschinkel

Você deve testar esses dois métodos indiretamente testando o comportamento...
Então estou discordando do @markerikson , teste através do seu componente conectado.

Você está sugerindo que você deve deixar de exportar o componente desconectado completamente e apenas testar o componente conectado? Eu acho que é "excesso de testes". Isso vincula seus testes desnecessariamente aos detalhes de implementação do componente que está testando. Porque a funcionalidade do componente desconectado (se tiver algum, é outra lata de worms) não tem relação com o local de onde ele recebe seus adereços. Talvez você queira transformar esse componente em um componente puramente de apresentação no futuro. Se você testar o componente conectado, terá que alterar todos os seus testes para não injetar mais uma loja, mas passar os adereços diretamente. Se você testou o componente desconectado, não precisa fazer nada, exceto eliminar os testes específicos do componente conectado (mais sobre isso abaixo).

Eu concordo que você não deve testar diretamente mapStateToProps / mapDispatchToProps , no entanto. Expor esses métodos privados simplesmente para fins de teste sempre me pareceu um cheiro de código. Na verdade, acho que esta é a única vez que você deve testar o componente conectado. Então, para resumir:

  1. Exportar componente não conectado e componente conectado
  2. NÃO exporte mapStateToProps ou mapDispatchToProps
  3. Teste toda a lógica do componente por meio do componente desconectado
  4. Apenas teste o componente conectado ao testar a interação com redux (props de dados são passados ​​do local apropriado na loja/props de ação são atribuídos aos criadores de ação apropriados, etc.).

A única sobrecarga extra ao fazer o número 4 é que você precisa eliminar o armazenamento redux para passá-lo para o componente conectado. No entanto, isso é muito fácil, pois sua API é apenas três métodos.

Método MapStateToProps

TestContainer.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

export const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer); // Ewww, exposing private methods just for testing

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { mapStateToProps } from './TestContainer';

it('maps auth.userPermissions to permissions', () => {
  const userPermissions = {};

  const { permissions: actual } = mapStateToProps({
    auth: { userPermissions },
  });

  expect(actual).toBe(userPermissions);
});

Novo método

TestContainer.jsx:

import { connect } from 'react-redux';

export const TestContainer = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

export default connect(mapStateToProps)(TestContainer);

TestContainer.spec.jsx:

import React from 'react';
import { shallow } from 'enzyme';

import TestContainer, { TestComponent } from './TestContainer';

it('renders TestComponent with approriate props from store', () => {
  const userPermissions = {};
  // Stubbing out store. Would normally use a helper method. Did it inline for simplicity.
  const store = {
    getState: () => ({
      auth: { userPermissions },
    }),
    dispatch: () => {},
    subscribe: () => {},
  };
  const subject = shallow(<TestContainer store={store} />).find(TestComponent);

  const actual = subject.prop('permissions');
  expect(actual).toBe(userPermissions);
});

sim, é isso que estou dizendo , não exporto e testo mapStateToProps , acabou o teste. Teste através da API . A API aqui é o Container .

@danny-andrews

Teste toda a lógica do componente por meio do componente desconectado

você pode site alguns exemplos?

Quero dizer a mesma coisa que você quando disse: "Você deve testar esses dois métodos indiretamente, testando o comportamento"

Isso pode ajudar a esclarecer os termos: quando digo "componente desconectado", quero dizer o componente bruto que é encapsulado na chamada para connect e quando digo "componente conectado" quero dizer o componente resultante que é retornado de a chamada para connect . "Componente conectado" e "contêiner" acredito que sejam sinônimos. Do meu acima:

// Unconnected component. Test all component logic by importing this guy.
export const TestComponent = ({ permissions }) => (permissions['view-message'] ? 'Hello!' : null);

const mapStateToProps = ({ auth }) => ({ permissions: auth.userPermissions });

// Connected component (container). Only test interaction with redux by importing this guy.
export default connect(mapStateToProps)(TestComponent);

Algumas pessoas preferem dividi-los inteiramente e fazer com que seus "contêineres" sejam apenas chamadas para connect que envolvem um componente puramente de apresentação. Nesse caso, os números 1 e 3 da lista desaparecem e os únicos testes que você precisa escrever são aqueles que verificam a interação com redux (número 4).

Estou pensando em escrever um post ENORME sobre isso. Estou ocupado no momento no código, mas responderei mais tarde

Depois de ler este tópico várias vezes, cheguei à conclusão de que existem 3 abordagens recomendadas:

  1. Exporte mapDispatchToProps e mapStateToProps e teste-os separadamente
  2. Renderize superficialmente o componente Container e teste se a fiação está correta
  3. Use seletores em vez de mapStateToProps e criadores de ação em vez de mapDispatchToProps e teste-os separadamente; escreva testes usando os criadores de ação, redutores e seletores juntos para garantir que todo o fluxo funcione.

No final, acho que todas as opções são válidas, mas têm seus próprios prós e contras. O "mais puro" é provavelmente o 2º, mas também envolve mais trabalho. Minha preferência pessoal iria para a opção 3.
Escrevi com mais detalhes sobre isso no meu blog , onde também tenho alguns exemplos de código.

@ucorina
No mundo atual dos módulos ES6, cada arquivo é considerado um módulo encapsulado em seu próprio escopo. Exportar é sua API do pequeno módulo e parece super errado exportar métodos, apenas para que você possa testá-los. É semelhante a como você tornaria públicos os métodos privados, apenas para testá-los.

É por isso que estou inclinado para a abordagem @dschinkel e não exporto mapDispatch e mapState, porque para mim eles são implementação privada do componente conectado.

No entanto, não tenho certeza do que fazer com um componente, que apenas envolve outro componente em uma conexão.

Você já escreveu o post do blog? @dschinkel adoraria ler

@AnaRobynn

Sim, concordo com você que apenas exportar mapDispatch e mapState parece errado, e eu provavelmente não usaria, mas é uma opção válida, mesmo que menos "pura" . Deixei lá como uma opção possível também porque Dan Abramov sugeriu essa técnica exata aqui: https://github.com/reduxjs/react-redux/issues/325#issuecomment -199449298.

Para responder à sua pergunta de "o que fazer com um componente, que apenas envolve outro componente em uma conexão", eu diria para não testá-lo. Não há lógica de negócios relacionada ao seu aplicativo e você não deseja testar a implementação connect - que já foi testada na biblioteca react-redux .

Se você ainda quiser testar de qualquer maneira, para testar seu código no futuro, sempre pode seguir a segunda abordagem que descrevo aqui, que está mais próxima do que @dschinkel está descrevendo.

@ucorina Exatamente, acho que opção é a que eu iria também! Obrigado por explicar melhor sua resposta. É um post de blog bem escrito. Parabéns!

@ucorina Desculpe incomodar novamente, mas depois de dormir um pouco, também não tenho certeza se concordo totalmente com a opção 2. Eu não tenho certeza. Também não tenho certeza sobre o benefício de testar mapStateToProps.

Você acha que é bom que a "Opção 2", indiretamente também teste os criadores da ação? E também testando indiretamente os seletores? Isso é uma coisa boa?
Não estou 100% convencido disso, mas também estou confuso com todas as (des)informações por aí. Há tantas idéias diferentes para testar.

  1. Use somente seletores dentro de mapStateToProps

    • zombe deles durante os testes de contêiner, se necessário

    • Teste os seletores separadamente.

    • Use apenas criadores de ação dentro mapDispatchToProps

    • zombe deles durante os testes de contêiner, se necessário.

    • Teste os criadores de ação separadamente.

    • Superfície o componente conectado

    • afirmar que é processado

    • teste qualquer lógica extra que teve que ser adicionada a mapStateToProps , mapDispatchToProps ou mergeProps e ainda não foi testada em outro lugar. (despachos condicionais, seletores que recebem argumentos de ownProps , etc)



      • neste caso você está apenas testando os mocks foram chamados na quantidade correta de vezes e com os argumentos corretos.



Alternativa:

  • Simule o estado da loja para testar o contêiner + seletores juntos
  • Zombe de chamadas de API externas para testar contêiner + ações juntos

@AnaRobynn Acredito que a segunda abordagem de @ucorina é a correta.

Muito do que ouço sugere:

  1. Você deve expor mapDispatchToProps e testá-lo diretamente
  2. Você não precisa testar sua chamada para connect() porque connect() já foi testado

Em relação a 1, não acho uma boa prática expor internos para testar. Seus testes A: agora assumem estrutura interna, e B: deve testar a coisa como eles realmente serão usados.

Em relação ao 2, você _absolutamente_ deve testar algo que chame uma biblioteca externa. Este é um dos pontos fracos em qualquer aplicação de quebra. Com qualquer biblioteca, ainda é possível que uma alteração importante seja introduzida em uma versão secundária/patch, e você _quer_ que seus testes falhem rapidamente.

Você acha que é bom que a "Opção 2", indiretamente também teste os criadores da ação? E também testando indiretamente os seletores? Isso é uma coisa boa?

Sim, a cobertura de teste redundante é uma coisa boa.

@philihp @dougbacelar
Acredito que cresci um pouco relacionado a testar aplicativos React. Minhas atuais crenças e pensamentos sobre o assunto:

  1. Não exponha mapStateToProps e mapDispatchToProps se você não precisar desses mapeamentos em vários componentes. Exportá-los apenas para testar é um antipadrão.
  2. Atualmente vejo os containers como colaboradores (funções que delegam outras funções e essas outras funções executam a lógica real).
    O que isso significa?
  3. Os contêineres não executam nenhuma lógica
  4. A lógica para selecionar um estado é para funções seletoras (que podem ser testadas puramente por unidade)
  5. A lógica das ações está oculta nos criadores de ação (o que pode depender se você está usando redux-thunk ou não são colaboradores por si mesmos)
  6. A visão renderizada é outra função (que potencialmente tem outra função de colaborador ou puramente renderiza coisas

=> A configuração de teste para isso é bastante simples quando você usa as bibliotecas de teste adequadas (eu recomendo testdouble.js).
=> Simule seu seletor e ações via testdouble.js (usando td.when() )
=> Renderize superficialmente seu componente container, dive() uma vez para ter acesso aos métodos do componente view
=> Dado o conjunto de regras de td.when() afirma se o componente se comporta corretamente

Não há necessidade de injetar alguma biblioteca fakeStore, não há problema em apagar a loja.

Exemplo:

/** <strong i="27">@format</strong> */

import React from 'react';
import { shallow } from 'enzyme';

function setup(props) {
  const HasOrganization = require('./HasOrganization').default;
  const defaultProps = {
    store: {
      subscribe: Function.prototype,
      getState: Function.prototype,
      dispatch: Function.prototype,
    },
  };
  const container = shallow(<HasOrganization {...defaultProps} {...props} />);
  return {
    container,
    wrapper: container.dive(),
  };
}

describe('<HasOrganization /> collaborations', () => {
  let getCurrentOrganizationId;
  beforeEach(() => {
    getCurrentOrganizationId = td.replace('../containers/App/userSelectors')
      .selectCurrentOrganizationId;
  });

  test('not render anything when the id is not valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(null);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(0);
  });

  test('render something when the id is valid', () => {
    const Dummy = <div />;
    td.when(getCurrentOrganizationId()).thenReturn(1);
    const { wrapper } = setup({ children: Dummy });

    expect(wrapper.find('div').length).toBe(1);
  });
});

Pensamentos?

@AnaRobynn Concordo plenamente com os pontos 1 e 2!

Meus contêineres geralmente são muito diretos:

// imports hidden

const mapStateToProps = (state, { userId }) => ({
  isLoading: isLoading(state),
  hasError: hasError(state),
  placeholders: getPlaceholders(userId)(state), // a list of some sort
  shouldDoStuff: shouldDoStuff(state),
});

const mapDispatchToProps = {
  asyncActionPlaceholder,
};

export const mergeProps = (stateProps, dispatchProps, ownProps) => {
  return {
    ...stateProps,
    ...dispatchProps,
    ...ownProps,
    onMount: () => {
      if (stateProps.shouldDoStuff) {
        dispatchProps.asyncActionPlaceholder(ownProps.userId);
      }
    },
  };
};

export default compose(
  connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps
  ),
  callOnMount(props => props.onMount())
)(SampleComponent);

Então eu faria

  1. espionar todos os seletores e criador de ação
  2. test getPlaceholders spy foi chamado com o userId correto
  3. assert SampleComponent foi renderizado com os adereços corretos
  4. chame a prop onMount de SampleComponent e verifique se ela despacha a ação simulada quando shouldDoStuff===true
  • Coisas como renderização condicional de coisas dependendo dos adereços que eu testaria em um teste separado por SampleComponent
  • Eu também prefiro usar redux-mock-store para zombar da loja como configureMockStore([thunk])() mas acho bom não fazer isso...

TL;DR I basicamente apenas seletores de teste foram chamados com argumentos corretos, ações simuladas foram despachadas com sucesso (se depender de alguma lógica) e que o componente filho seja renderizado com as props corretas

@philihp

  • Sou muito contra testar bibliotecas externas, prefiro zombar de tudo e testar as coisas isoladamente.
  • Sinto-me seguro confiando que os testes de ponta a ponta detectarão quaisquer problemas importantes causados ​​por atualizações de bibliotecas.
  • A razão pela qual eu não gosto de testar containers+selectors+action creators todos juntos é que você terá que testar novamente os mesmos seletores e criadores de ação para todos os outros containers que os reutilizarem. Nesse ponto, é melhor adicionar mais casos de teste aos próprios testes de unidade.

@dougbacelar Você pode fornecer algum código de teste de amostra para testar o componente que você mostrou. Você está usando enzima? Algo mais? Raso? Montar?

@dougbacelar

Sou muito contra testar bibliotecas externas, prefiro zombar de tudo e testar as coisas isoladamente.

Zombe quando precisar, mas se você pode usar a coisa real, por que não? A simulação é útil quando certas chamadas são lentas ou têm efeitos colaterais, como solicitações de rede.

A razão pela qual eu não gosto de testar containers+selectors+action creators todos juntos é que você terá que testar novamente os mesmos seletores e criadores de ação para todos os outros containers que os reutilizarem. Nesse ponto, é melhor adicionar mais casos de teste aos próprios testes de unidade.

Sim , teste seus seletores, criadores de ação de forma independente. Teste-os completamente com muitas entradas esperadas.

Sim , também teste seus contêineres. Se isso significa que eles fornecem cobertura sobreposta em seletores e criadores de ação, isso não é uma coisa ruim.

Aqui está o que quero dizer...

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { saveColor } from '../actions/save';
import { selectColors } from '../reducers/colors.js';
import ColorButtons from '../components/ColorButtons';
const ColorButtons = ({ colors, onClick }) => (
  <div>
    {colors.map(color => {
      <button type="button" key={color} onClick={onClick(color)}>{color}</button>
    })}
  </div>
);
ColorButtons.propTypes = {
  colors: PropTypes.arrayOf(PropTypes.string),
  onClick: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
  colors: selectColors(state.colors),
});
const mapDispatchToProps = dispatch => ({
  onClickColor: color => () => {
    dispatch(saveColor({ color }));
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ColorButtons);
import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import ColorButtons from '../ColorButtons';
import { saveColor } from '../../actions/save';
import { selectColors } from '../../reducers/colors.js';
const buildStore = configureStore();
describe('<ColorButtons />', () => {
  let store;
  let wrapper;
  const initialState = { colors: ['red', 'blue'] };
  beforeEach(() => {
    store = buildStore(initialState);
    wrapper = shallow();
  });
  it('passes colors from state', () => {
    expect(wrapper.props().colors).toEqual(selectColors(initialState.colors));
  });
  it('can click yellow', () => {
    const color = 'yellow';
    wrapper.props().onClick(color)();
    expect(store.getActions()).toContainEqual(saveColor({ color }));
  });
});

Aqui, isso não está testando todos os parâmetros diferentes que selectColors e saveColor poderiam obter; está testando que quando <ColorButtons /> é criado, ele obtém as propriedades que esperamos que ele obtenha. Absolutamente testar aqueles em outro teste.

import { selectColors } from '../colors.js';
describe('selectColors', () => {
  it('returns the colors', state => {
    expect(selectColors(['red'])).toEqual(['red'])
  })
  it('returns an empty array if null', state => {
    expect(selectColors(null)).toEqual([])
  })
  it('returns a flattened array', state => {
    expect(selectColors(['red', ['blue', 'cyan']])).toEqual(['red','blue','cyan'])
  })
})

Sou muito contra testar bibliotecas externas, prefiro zombar de tudo e testar as coisas isoladamente.

Zombe quando precisar, mas se você pode usar a coisa real, por que não? A simulação é útil quando certas chamadas são lentas ou têm efeitos colaterais, como solicitações de rede.

Discordo, em parte.
Eu nunca zombo de bibliotecas externas (não zombe do que você não possui), mas eu escrevo um wrapper por exemplo axios. No meu teste de colaboração, posso apenas simular todas as funções e garantir que tudo esteja conectado corretamente.

Nos meus testes de colaboração sempre zombo de módulos, funções, etc.
Essas funções chamadas pelo seu colaborador podem ser facilmente testadas em unidade.

Aqui está o que quero dizer...

Também discordo.
Você está testando implicitamente seus redutores e seletores aqui. Se é isso que você quer fazer tudo bem, mas eu prefiro simular essas funções e apenas verificar se elas são chamadas corretamente. O resto é tratado pelos redutores/seletores.

Este tópico está refazendo pontos feitos em https://martinfowler.com/articles/mocksArentStubs.html , Classical and Mockist Testing

Acordado. Bloqueio.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

olalonde picture olalonde  ·  3Comentários

dmitry-zaets picture dmitry-zaets  ·  3Comentários

CellOcean picture CellOcean  ·  3Comentários

timdorr picture timdorr  ·  3Comentários

jbri7357 picture jbri7357  ·  3Comentários