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:
- O usuário aperta um botão.
- Uma solicitação é enviada ao servidor, informando que o usuário pressionou um botão.
- O servidor retorna o número de cliques feitos.
- O estado registra o número de cliques feitos.
- A interface do usuário é atualizada e o usuário vê que o número de cliques aumentou.
- ...
- 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 .