visit
You can find the first part here: Why testing?
const initialState = {
todos: [
{ id: 1, text: 'Do boring stuff', completed: false, color: 'purple' },
{ id: 2, text: 'Write tests!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded':
{
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled':
{
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged':
{
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}
Like we said earlier testing redux reducers is simple. We need to make sure that the function updates the current state as we expect it to.
So in the below case:
case 'todos/todoAdded':
{
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
it('should add a todo if the action type equals to todos/todoAdded', () => {
const appReducerCall = appReducer(
initialState,
{
type: 'todos/todoAdded',
payload: 'Buy beer'
});
const result = {
...initialState,
...initialState.todos.push({
id: 3,
text: 'Buy beer',
completed: false
})
};
expect(appReducerCall).toEqual(result)
})
Let’s go to next todos/todoToggled. Have a look at it.
case 'todos/todoToggled':
{
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
}
The key is to find the action.payload. So what happens is that we loop through all todos array using the map Array function. Then if the current todo.id element equals to the payload it returns the current todo.
Have a look at the below test:
it('should change the completed to opposite value when calling todos/todoToggled with existing id', () => {
const appReducerCall = appReducer(
initialState,
{ type: 'todos/todoToggled', payload: 1 }
);
const result = {
...initialState,
...initialState.todos[0].completed = true
};
expect(appReducerCall).toEqual(result)
})
And we’ve got the last case to test! Let’s give it a go-to get our first full 100%!
This time we call filters/statusFilterChanged. The only thing that happens is that we override the filter’s status property by passing any payload. Simple! Let’s pass the correct type and then the payload and assert that the new state has our new passed state. it('should change the filter status of the current state using the payload', () => {
const appReducerCall = appReducer(
initialState,
{ type: 'filters/statusFilterChanged', payload: 'status' }
);
const result = {
...initialState,
filters: {
status: 'status',
colors: []
}
};
expect(appReducerCall).toEqual(result)
})
Now, look at that! Full 100% coverage of our reducer.js code!
To start please run
npm i --save-dev @testing-library/react
This will give you access to the render and the selectors which the library provides.The problem the library is solving is according to the website:You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down.
It makes sense. It is the same philosophy we used to ‘test’ our private nextTodoId function in redux. We do not care about implementation details, we just want to be sure that our app works when we change something. This philosophy works great with BDD, so Behavior Driven Development.
import React from 'react';
import { Provider } from 'react-redux';
import store from './store/store'
import EntryComponent from './EntryComponent'
function App() {
return (
<Provider store={store}>
<EntryComponent />
</Provider>
);
}
export default App;
But we can use this example to introduce the
render
method. It is important to understand that when we test our components, we do not test them as they appear in the browser. Jest uses a browser to render our components, but as these are unit tests we only render the currently tested component.
So, in this case, we are going to render the whole app what will cause a lot of components to have low coverageLet’s create an App.test.js file in the same directory as the App.js exist. And now we need to import several elements.import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
describe(‘App.js’, () => {
})
it('should render the App component', () => {
const { container } = render( < App / > );
expect(container.firstChild).toBeTruthy();
})
Let’s have a look at the above. The
render
method returns several constants, one of them is the container which is the result of the render.We can access several properties of the
container
and one of them is the firstChild which we use to assert that it is truthy. Obviously, it is.What is interesting is that now we rendered more components which are children to the App.js. Let’s have a look at the coverage.import logo from './logo.svg';
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import './App.css';
import Todo from './Todo'
const selectTodos = state => state.todos;
function EntryComponent() {
const todos = useSelector(selectTodos);
const dispatch = useDispatch();
const [todo, setTodo] = useState('');
const addTodo = () => {
setTodo('')
dispatch({type: 'todos/todoAdded', payload: todo});
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<div style={{display: 'flex'}}>
<input placeholder='Add another todo' onChange={event => setTodo(event.target.value)} value={todo}></input>
<button onClick={addTodo} disabled={todo ? false : true}>+</button>
</div>
<ul>
{
todos.map((item) => {
return (<Todo key={item.text} todo={item.text}/>)
})
}
</ul>
</header>
</div>
)
}
export default EntryComponent
it('should render the EntryComponent component', () => {
const { container } = render( < EntryComponent / >);
expect(container.firstChild).toBeTruthy();
})
could not find react-redux context value; please ensure the component is wrapped in a <Provider>
// test-utils.js
import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// Import your own reducer
import reducer from './store/reducer'
function render(
ui,
{
initialState,
store = createStore(reducer, initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '@testing-library/react'
// override render method
export { render }
First of all, we do no longer import the render from
’@testing-library/react’
but now from the ‘test-utils.js’
we created.Copy the same initialState we used in the reducer.js.And now the passing test looks like this: it('should render the EntryComponent component', () => {
const { container } = render( < EntryComponent / > , { initialState });
expect(container.firstChild).toBeTruthy();
})
In order to find elements the render method returns even more helpers, like
getByText
, getByTestId
. We can use those to find DOM elements. Check out the code below. it('should add a new todo to the list when a new todo is not empty and after pressing the button', () => {
const { getByText, getByPlaceholderText } = render(
< EntryComponent / > ,
{ initialState }
);
fireEvent.change(
getByPlaceholderText('Add another todo'),
{ target: { value: '23' } }
)
fireEvent.click(getByText('+'))
expect(getByText('23')).toBeInTheDocument();
})
The Add button has got a ‘+’ text so we can use
getByText
to locate it, and the input field has got a placeholder so we use getByPlaceholderText
.In order to fill the field and press a button we use the fireEvent method using fireEvent.change for the input and fireEvent.click for the button respectively. We change the field first and fill it in with a string ‘23’ then we press the button. In the final line, we assert again using getByText that the ‘23’ is in the Document! And our tests pass.Just remember that
toBeInTheDocument
is not part of the testing library and you need to install and set up ‘jest-dom’.Let’s check out our coverage again.You will find out in the next part alongside snapshot testing examples and why NOT to rely on snapshots. We will learn what are spies and how we leverage them in our testing.
But to get to the next part you need to like react to this post.