7

I am trying to understand the process of react and redux testing, i am using testing library to use the dom node queries for testing my project, but i am still confused of the way i should test the redux implementations in my react project:

I created a custom render function instead of the normal render method from react testing library

import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares);

//test-utils.js
//creating a custom render function so we can wrap our App with
//react-redux provider
const render = (ui, initialState) => {
  const store = mockStore(initialState);
  //App wrapper and mocked store passed to it
  const Wrapper = ({ children }) => {
    return <Provider store={store}>{children}</Provider>
  }
  return rtlRender(ui, { wrapper: Wrapper })
}

// re-export everything
export * from '@testing-library/react'
// override render method
export { render }

and in App.test.js, i am manipulating the initialState manually .this is part of confusing i don't know if i am doing right here:

describe('App', () => {
  const { getByText, getByTestId, findByText, queryByText } = screen;

  let initialState = {
    data: {
      books: [],
      error: '',
      loading: false
    },
    //rest of the state
  }

  it('should render App correctly with given redux state', () => {
    const { container } = render(<App />, initialState);
    expect(container.firstChild).toMatchSnapshot();
    expect(getByTestId(/header/)).toHaveTextContent('React Testing')
  });

  it('displays loading message before data get fetched', () => {
    initialState = {
      ...initialState,
      data: {
        ...initialState.data,
        loading: true
      }
    }
    render(<App />, initialState);
    expect(getByText(/...loading books/)).toBeInTheDocument();
  });

   it('display an error message if any thing wrong happened while fetching data', () => {
     initialState = {
       ...initialState,
       data: {
         ...initialState.data,
         error: 'something went wrong'
       }
     }
     render(<App />, initialState);
     expect(getByText(/something went wrong/)).toBeInTheDocument();
   })
})

This is for example the action creator that i am calling in App component

export const fetchData = () => dispatch => {
    dispatch({ type: SET_LOADING }); // this set loading to true

    return axios.get("https://api.jsonbin.io/b/57d5760ea")
        .then(res => {
            dispatch({
                type: FETCH_DATA, // this set data
                payload: res.data.books
            });
            dispatch({ type: STOP_LOADING })
        })
        .catch(err => {
            dispatch({
                type: SET_ERROR, // this set errors
                payload: 'Something went wrong'
            })
        })
}

And this is App.js component:

function App({ fetchData, data: { loading, error, books } }) {
  useEffect(() => {
   fetchData()
  }, []);

  return (
    <div className="App">
      <header data-testid="header">
        <h2>React Testing</h2>
        <Bag />
      </header>
      {
        error ? error :
          !loading ? <Bookstore books={books} /> : <span data-testid='loading-message'>...loading books</span>
      }
    </div>
  );
}

const mapStateToProps = state => ({
  data: state.data,
});

I am not sure if using the initialState like this is a right way to do that as i didn't find any other way to implement in my test cases, and i experienced the problem when i tried to test if the loading message will disappear after data fetched using waitForElementToBeRemoved as i always get timeout error indicating loading never get to false as in the actual app!

Is using initialState like this right or wrong or can be used in another way to be correct??

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Code Eagle
  • 1,293
  • 1
  • 17
  • 34
  • 1
    What exactly is your concern with initialState? If you set it in tests the way it's expected by reducers then it's ok. Otherwise it's not. It's impossible to say because you didn't post Redux part. *indicating loading never get to false as in the actual app* - it may indicate they you used it the wrong way. What exactly did you try with waitForElementToBeRemoved? Please, provide the actual code. – Estus Flask Oct 19 '20 at 18:36

1 Answers1

5

If what you want is to test is App.js's behaviour depending on the fetch result then I would approach it differently.

import { fetchData } from './fetchDataLocation';

jest.mock('./fetchDataLocation', () => ({
  fetchData: jest.fn()
}))

jest.mock('./Error', () => jest.fn(() => 'Error'));
jest.mock('./Loading', () => jest.fn(() => 'Loading'));
jest.mock('./Bookstore', () => jest.fn(() => 'Bookstore'));

describe('App', () => {
  describe('with error', () => {
    beforeEach(() => {
      Error.mockClear();
      Loading.mockClear();
      fetchData.mockImplementation(() => Promise.reject('Error'));
    })

    test('renders loading component', () => {
      const { container } = render(<App />);
      expect(Loading).toBeCalled(); // or toBeCalledTimes(1) or lastCalledWith(XYZ) if you want to test the props
    })

    test('renders error component', () => {
      const { container } = render(<App />);
      expect(Error).toBeCalled();
    })
  })

  describe('with data', () => {
    beforeEach(() => {
      Loading.mockClear();
      Bookstore.mockClear();
      fetchData.mockImplementation(() => Promise.resolve([{ id: 2 }]));
    })

    test('renders loading component', () => {
      const { container } = render(<App />);
      expect(Loading).toBeCalled(); // or toBeCalledTimes(1) or lastCalledWith(XYZ) if you want to test the props
    })

    test('renders bookstore component', () => {
      const { container } = render(<App />);
      expect(Bookstore).lastCalledWith({ books: [{ id: 2 }]})
    })
  })
});

It's important to keep separation of concerns, Foo component only needs to care about how it will behave depending on the props. If the component has a side effect like a fetch then mock the fetch to return different scenarios and test them accordingly.

alextrastero
  • 3,703
  • 2
  • 21
  • 31