salsita / prism

React / Redux action composition made simple http://salsita.github.io/prism/
496 stars 24 forks source link

Unit tests examples #58

Closed krzysztofpniak closed 7 years ago

krzysztofpniak commented 7 years ago

I'm 100% sure that unit tests are easy in redux-elm, but it would be nice if docs provide some of examples.

tomkis commented 7 years ago

Well yeah, I didn't go into a depth because basically unit testing in redux-elm should follow the best practices of unit testing Sagas & Reducers.

Testing reducers is very easy:

it('should increment', () => {
   const initialState = reducer();

   expect(reducer(initialState, { type: 'Increment' })).toEqual(1);
});

So it's all about expecting output based on input.

Testing Sagas is theoretically easy, but it's not that easy in practice because you need to follow some good practices, like for example avoiding yield*:

function* apiSaga() {
   yield put({ type: 'SetLoadingSpinner' });
   try {
     yield call(api);
   } finally {
     yield put({ type: 'ResetLoadingSpinner' });
   }
}

// You better do this
function* rootSaga() {
    yield take('CallAPI');

    // You should realize that you can choose between call and fork
    // depending on use case
    yield call(apiSaga);
}

// Instead of this
function* rootSaga(){
    yield take('CallAPI');
    yield* apiSaga();
}

Because now you can write tests quite easily:

it('should call an API when CallAPI kicks in', function() {
   const it = rootSaga();

   // Saga should wait for CallAPI
   expect(it.next().value).toEqual(take('CallAPI'));

   // And then call the API
   // which is encapsulated in apiSaga, we don't need to test internals
   // because we just rely on the correctness proven by other unit test
   expect(it.next().value).toEqual(apiSaga);
});

it('should show spinner, call api and hide spinner', function() {
  const it = apiSaga();

  // Show the spinner
  expect(it.next().value).toEqual(put({ type: 'SetLoadingSpinner' }));

  // Call API
  expect(it.next().value).toEqual(call(api));

  // Hide the spinner
  expect(it.next().value).toEqual(put({ type: 'ResetLoadingSpinner' }));
});

it('should hide the spinner even when api fails', function() {
  const it = apiSaga();

  // Show the spinner
  expect(it.next().value).toEqual(put({ type: 'SetLoadingSpinner' }));

  // Call API
  expect(it.next().value).toEqual(call(api));

  // Even when API fails, just reset the spinner
  expect(it.throw(new Error('API error'))).toEqual(put({ type: 'ResetLoadingSpinner' }));
});

However, both of these examples should rather be part of redux and redux-saga.