Unit Testing
In programming, unit testing is the method to test a single unit of code. A single unit of code may be one or more program modules combined together or the smallest unit of working code that cannot be divided further, by doing so the unit on the whole is not functional as expected.If you are familiar with reactjs testing libraries like jest and enzyme, jasmine or react-testing-library, you must have tested individual components on the top level by shallow rendering. Shallow rendering renders a particular component without rendering it's children for it to be tested.Here we will be learning how to unit test a react component using context API with the help of enzyme testing utility.
Enzyme
Enzyme is not a testing library.It is more of a testing utility which lets you manipulate,traverse,interact with the DOM elements. Enzyme can be used with jest or karma or mocha depending upon the need.
Context API
Context API by reactjs provides a way to pass down the data to the components without having to actually pass the data as props at each level. For example, consider component A having 3 nested children B,C and D.If A has to pass some values to D,instead of prop drilling the values, one can wrap the component D as a consumer of the context and A as a provider.
Context API might sound similar to react-redux architecture but it is completely different from it.
For those who do not have an experience working with context API, you can go through the reactjs documentation for better understanding
unit testing context api
Consider the following piece of code to setup a context:
import React from 'react';
export default React.createContext("en")
Let's call this file languageContext.js. Now since the context is now defined, we can wrap our root component as the provider.
import React, { useEffect, useState } from "react";
import "./App.css";
import GuessedWord from "./GuessedWord/GuessedWord";
import LanguagePicker from "./GuessedWord/LanguagePicker";
import languageContext from "./languageContext";
/**
* @function reducer to update state automatically when dispatch is called
* @param state {object} previous state
* @param action {object} type and payload properties
* @return {object} new state
*/
const reducer = (state, action) => {
switch (action.type) {
case "setSecretWord":
return { ...state, secretWord: action.payload };
case "setLanguage":
return { ...state, language: action.payload };
default:
throw new Error(`Invalid action type ${action.type}`);
}
};
function App(props) {
//const [secretWord,setSecretWord]=useState('');
const [state, dispatch] = React.useReducer(reducer, {
secretWord: "",
language: "en",
});
const success = false;
const guessedWords = [];
const setSecretWord = (secretWord) => {
dispatch({ type: "setSecretWord", payload: secretWord });
};
const setLanguage = (lang) => {
dispatch({ type: "setLanguage", payload: lang });
};
useEffect(() => {
getSecretWord(setSecretWord);
}, []);
let content;
if (!state.secretWord.length) content = <div data-test="spinner" />;
else
content = (
<>
<h1>Jotto</h1>
<languageContext.Provider value={state.language}>
<LanguagePicker setLanguage={setLanguage} />
<GuessedWord guessedWords={guessedWords} />
</languageContext.Provider>
</>
);
return <div data-test="component-app">{content}</div>;
}
export default App;
The LanguagePicker component is as follows:
import React from "react";
import propTypes from "prop-types";
function LanguagePicker({setLanguage}) {
const languages = [
{ code: "en", symbol: "🇺🇸" },
{ code: "ar", symbol: "🇦🇪" },
];
const languageIcons = languages.map((lang) => {
return (
<span
key={lang.code}
data-test="language-icon"
onClick={() => setLanguage(lang.code)}
>
{lang.symbol}
</span>
);
});
return <div data-test="component-language-picker">{languageIcons}</div>;
}
LanguagePicker.propTypes = {
setLanguage: propTypes.func.isRequired,
};
export default LanguagePicker;
The LanguagePicker component allows us to select a language of preference which we store in a local state of our root-level component. We wrap the App.js,our root-level-component as a Context Provider. This is done so that the root-level component acts a data provider to the child components. We pass the data as a "value" prop. So each of the child components now has access to this value prop as each of them act as a consumer.
The GuessedWords component is as follows:
import React from "react";
import PropTypes from "prop-types";
//services and helpers
import languageContext from "../context/languageContext";
import {getStringsByLanguage} from './strings';
function GuessedWord({ guessedWords }) {
const language=React.useContext(languageContext);
if (!guessedWords.length)
return (
<div data-test="guessed-word-component">
<span data-test="instructions">{getStringsByLanguage(language,"guessPrompt")}</span>
</div>
);
return (
<div data-test="guessed-word-component">
<table data-test="table">
<thead>
<tr>
<th>GuessedWord</th>
<th>Match Count</th>
</tr>
</thead>
<tbody>
{guessedWords.map((guess, index) => (
<tr key={index} data-test="rows">
<th>{guess.guessedWord}</th>
<th>{guess.match}</th>
</tr>
))}
</tbody>
</table>
</div>
);
}
GuessedWord.propTypes = {
guessedWords: PropTypes.arrayOf(
PropTypes.shape({
guessedWord: PropTypes.string.isRequired,
match: PropTypes.number.isRequired,
})
).isRequired,
};
export default GuessedWord;
We make use of React.useContext() hook to access the "value" prop of the parent component's provider wrapper. We pass the languageContext as an argument to the hook.
The function getStringsByLanguage returns the localized string based on the language chosen. Now our task is to unit test this component. We test two scenarios here. If the language chosen is "en", we will return "Try to guess the secret word". If the language chosen is "ar" , we return "حاول تخمين الكلمة السرية". The code for the unit test file is as follows:
import { shallow } from "enzyme";
import checkPropTypes from "check-prop-types";
import GuessedWord from "./GuessedWord";
const defaultProps = {
guessedWords: [{ guessedWord: "train", match: 3 }],
};
const setup = (props = defaultProps) => {
return shallow(<GuessedWord {...props} />);
};
describe("language picker scenarios", () => {
test("should prompt the guess instruction in english", () => {
const wrapper = setup({ guessedWords: [] });
const guessInstruction = wrapper.find(`[data-test="instructions"]`);
expect(guessInstruction.text()).toBe("Try to guess the secret word");
});
test("should prompt the guess instruction in arabic", () => {
const originalUseContext = React.useContext;
const mockReactUseContext = jest.fn().mockReturnValue("ar");
React.useContext = mockReactUseContext;
const wrapper = setup({ guessedWords: [] });
const guessInstruction = wrapper.find(`[data-test="instructions"]`);
expect(guessInstruction.text()).toBe("حاول تخمين الكلمة السرية");
React.useContext=originalUseContext;
});
});
We shallow render the GuessedWords component. We know that the default language is "en" in the context provider.So the first test case should be obvious to you. In the second test case, we mock the original React.useContext() hook and we set the language as "ar"; Now since the language is set to arabic and the GuessedWords component is shallow rendered, our assertion should be the text displayed in arabic.It is always a good practice to mock your functions or hooks in your component and restore them after each assertion.This is done so that the original definition of your function or hook is restored for all other test cases in your test suite which are using them.
Top comments (0)