Metamorfose do teste redux-saga

A estrutura redux-saga fornece vários padrões interessantes para trabalhar com efeitos colaterais, mas, como verdadeiros desenvolvedores corporativos, precisamos cobrir todo o código com testes. Vamos descobrir como vamos testar nossas sagas.



Tome o clicker mais simples como exemplo. O fluxo de dados e o significado do aplicativo serão os seguintes:

  1. O usuário aperta um botão.
  2. Uma solicitação é enviada ao servidor, informando que o usuário pressionou um botão.
  3. O servidor retorna o número de cliques feitos.
  4. O estado registra o número de cliques feitos.
  5. A interface do usuário é atualizada e o usuário vê que o número de cliques aumentou.
  6. ...
  7. LUCRO.

Em nosso trabalho, usamos o Typecript, para que todos os exemplos estejam nesse idioma.

Como você provavelmente já adivinhou, implementaremos tudo isso com redux-saga . Aqui está o código para o arquivo sagas inteiro:

 export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } 

Neste exemplo simples, declaramos a saga processClick , que processa diretamente a ação e a saga watchClick , que cria um loop para processar action' .

Geradores


Portanto, temos a saga mais simples. Ele envia uma solicitação ao servidor ( call) , recebe o resultado e o passa para o redutor ( put) . Precisamos testar de alguma forma se a saga está transmitindo exatamente o que recebe do servidor. Vamos começar.

Para o teste, precisamos bloquear a chamada do servidor e verificar de alguma forma se exatamente o que veio do servidor foi para o redutor.

Como sagas são funções de gerador, o next() método next() , que está no protótipo do gerador, será a maneira mais óbvia de testar. Ao usar esse método, temos a oportunidade de receber o próximo valor do gerador e transferir o valor para o gerador. Assim, tiramos da caixa a oportunidade de receber chamadas em molhado. Mas tudo é tão róseo? Aqui está um teste que escrevi em geradores simples:

 it('should increment click counter (behaviour test)', () => { const saga = processClick() expect(saga.next().value).toEqual(call(ServerApi.SendClick)) expect(saga.next(10).value).toEqual(put(Actions.clickSuccess(10))) }) 

O teste foi conciso, mas o que é testado? De fato, ele simplesmente repete o código do método saga, ou seja, com qualquer alteração na saga, o teste terá que ser alterado.

Esse teste não ajuda no desenvolvimento.

Redux-saga-teste-plano


Depois de encontrar esse problema, decidimos pesquisá-lo no Google e de repente percebemos que não éramos os únicos e estávamos longe do primeiro. Diretamente na documentação do redux-saga desenvolvedores oferecem uma olhada em várias bibliotecas criadas especificamente para satisfazer os fãs de testes.

Da lista proposta, pegamos a biblioteca redux-saga-test-plan . Aqui está o código para a primeira versão do teste que escrevi com ele:

 it('should increment click counter (behaviour test with test-plan)', () => { return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 2] ]) .dispatch(Actions.click()) .call(ServerApi.SendClick) .put(Actions.clickSuccess(2)) .run() }) 

O construtor de teste no redux-saga-test-plan é a função expectSaga , que retorna a interface que descreve o teste. A saga de teste ( processClick da primeira listagem) é passada para a própria função.

Usando o método de provide , você pode bloquear chamadas do servidor ou outras dependências. Uma matriz de StaticProvider' , que descreve qual método deve retornar.

No bloco Act , temos um único método - dispatch . Uma ação é passada a ele, à qual a saga responderá.

O bloco assert consiste nos métodos de call put , que verificam se os efeitos correspondentes foram causados ​​durante o trabalho da saga.

Tudo termina com o método run() . Este método executa o teste diretamente.

As vantagens desta abordagem:

  • Ele verifica se o método foi chamado, e não a sequência de chamadas;
  • moki descreve claramente qual função fica molhada e qual retorna.

No entanto, há trabalho a fazer:

  • há mais código;
  • o teste é difícil de ler;
  • este é um teste de comportamento, o que significa que ele ainda está conectado à implementação da saga.

Os dois últimos golpes


Teste de condição


Primeiro, consertamos o último: fazemos um teste de estado a partir de um teste de comportamento. O fato test-plan permitir que você defina o state inicial e passe o reducer , que deve responder aos efeitos de put gerados pela saga, nos ajudará com isso. É assim:

 it('should increment click counter (state test with test-plan)', () => { const initialState = { clickCount: 11, return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) .dispatch(Actions.click()) .run() .then(result => expect(result.storeState.clickCount).toBe(14)) }) 

Neste teste, não verificamos mais se algum efeito foi acionado. Verificamos o estado final após a execução, e tudo bem.

Conseguimos nos livrar da implementação da saga, agora vamos tentar tornar o teste mais compreensível. Isso é fácil se você substituir then() por async/await :

 it('should increment click counter (state test with test-plan async-way)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) const result = await saga.dispatch(Actions.click()).run() expect(result.storeState.clickCount).toBe(14) }) 

Testes de integração


Mas e se também obtivemos uma operação de clique reverso (vamos chamá-la de desclique), e agora nosso arquivo sag se parece com o seguinte:

 export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* processUnclick() { const result = yield call(ServerApi.SendUnclick) yield put(Actions.clickSuccess(result)) } function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } function* watchUnclick() { yield takeEvery(ActionTypes.UNCLICK, processUnclick) } export default function* mainSaga() { yield all([watchClick(), watchUnclick()]) } 

Suponha que precisamos testar se, quando as ações de clicar e desmarcar são chamadas no estado, o resultado da última viagem ao servidor é gravado no estado. Esse teste também pode ser feito facilmente com o redux-saga-test-plan :

 it('should change click counter (integration test)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(mainSaga) .provide([ call(ServerApi.SendClick), 14], call(ServerApi.SendUnclick), 18] ]) .withReducer(rootReducer, initialState) const result = await saga .dispatch(Actions.click()) .dispatch(Actions.unclick()) .run() expect(result.storeState.clickCount).toBe(18) }) 

Observe que agora estamos testando o mainSaga , e não os manipuladores de mainSaga individuais.

No entanto, se executarmos o teste como está, obteremos o Vorning:



Isso se deve ao efeito takeEvery - este é um loop de processamento de mensagens que funcionará enquanto nosso aplicativo estiver aberto. Consequentemente, o teste no qual o takeEvery é chamado não poderá concluir o trabalho sem ajuda externa e o redux-saga-test-plan forçosamente encerra esses efeitos após 250 ms após o início do teste. Esse tempo limite pode ser alterado chamando expectSaga.DEFAULT_TIMEOUT = 50.
Se você não deseja receber esses elogios, um para cada teste com um efeito complexo, basta usar o método silentRun() vez do método run() .



Armadilhas


Onde sem armadilhas ... No momento da redação deste artigo, a versão mais recente do redux-saga: 1.0.2. Ao mesmo tempo, o redux-saga-test-plan pode trabalhar com ele apenas no JS.

Se você deseja TypeScript, você deve instalar a versão a partir do canal beta:
npm install redux-saga-test-plan@beta
e desative os testes da compilação. Para fazer isso, no arquivo tsconfig.json, você precisa especificar o caminho "./src/**/*.spec.ts" no campo "excluir".

Apesar disso, consideramos o redux-saga-test-plan a melhor biblioteca para testar o redux-saga . Se você tem redux-saga em seu projeto, talvez seja uma boa escolha para você.

O código fonte do exemplo no GitHub .

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


All Articles