Модульное тестирование ваших вызовов api может быть до некоторой степени обременительным, для простоты здесь я использую приложение create-react-app и Enzyme and Jest (без redux или redux-thunk).
В нашем приложении есть единственный компонент реакции
<Hello/>
, и мы собираемся сделать вызов API, чтобы получить информацию о пользователе в его методе жизненного цикла componentDidMount.
А отображать имя пользователя в абзаце после успешного вызова API.
Пока вызов api не будет разрешен, мы будем показывать
span
для загрузки.
Итак, наш компонент Hello имеет
import React from "react"; export default class Hello extends React.Component { constructor(props) { super(props); this.state = { user: null } } async componentDidMount() { try { const res = await fetch("http://localhost:3000/user"); if (res.status >= 400) throw new Error("something went wrong") const user = await res.json(); this.setState({ user }); } catch (err) { console.error(err); } } render() { return ( <div> <h2>Welcome to hello</h2> {this.state.user ? <p className="user">user:{this.state.user.userName}</p> : <span className="spinner"></span>} </div> ); } }
в приведенном выше коде я использую fetch, чтобы выполнить ajax-вызов конечной точки api, и я пометил componentDidMount
ключевым словом async
, чтобы мы могли await
, пока наш вызов api не будет разрешен. После успешного вызова api мы конвертируем ответ json в объект пользователя javascript и вызываем setState
с данными пользователя.
Давайте напишем для этого тест
Для компонента Hello
мы напишем два теста
Компонент Hello должен отображать загрузку
span
до успешного вызова API
Должен отобразить
paragraph
tag с именем пользователя после успешного вызова API и скрыть загрузкуspans
.
import React from "react";
import { shallow } from "enzyme";
import Hello from "./hello";
beforeAll(() => {
global.fetch = jest.fn();
//window.fetch = jest.fn(); if running browser environment
});
let wrapper;
beforeEach(() => {
wrapper = shallow(<Hello />, { disableLifecycleMethods: true });
});
afterEach(() => {
wrapper.unmount();
});
it("must render a loading span before api call success", () => {
expect(wrapper.find("span.spinner").exists()
).toBeTruthy();
});
it("must show the p.user and hide the loading span after api call success",
(done) => {
// here we are spying on componentDidMount to know that it has been called
const spyDidMount = jest.spyOn(Hello.prototype,"componentDidMount");
fetch.mockImplementation(() => {
return Promise.resolve({
status: 200,
json: () => {
return Promise.resolve({
userName: "manas",
userId: 1
});
}
});
});
const didMount = wrapper.instance().componentDidMount();
// expecting componentDidMount have been called
expect(spyDidMount).toHaveBeenCalled();
didMount.then(() => {
// updating the wrapper
wrapper.update();
expect(wrapper.find("p.user").text()).toContain("manas");
expect(wrapper.find("spans.loading").length).toBe(0);
spyDidMount.mockRestore();
fetch.mockClear();
done();
});
});
давайте разберем код тестирования на значимые части
Перед всеми тестами я издеваюсь над вызовом api с jest.fn()
. Это хорошая практика - имитировать вызов API в модульном тесте, поскольку мы проводим модульный тест, а не интеграционный тест.
beforeAll(() => { global.fetch = jest.fn(); //window.fetch = jest.fn(); if running browser environment });
Я создаю свойство fetch
в глобальном объекте с помощью global.fetch = jest.fn()
и устанавливаю его на функцию шутливого макета, чтобы сделать fetch
доступным в среде nodejs. Если вы запускаете свой код в codeanbox или jsfiddle, используйте вместо этого window.fetch = jest.fn()
.
let wrapper; beforeEach(() => { wrapper = shallow(<Hello />, { disableLifecycleMethods: true }); }); afterEach(() => { wrapper.unmount(); });
Перед каждым тестом я создаю неглубокую оболочку для Hello
компонента, используя метод поверхностного рендеринга Enzyme.
Здесь следует отметить, что shallow
функция вызывается со вторым параметром {disableLifecycleMethods:true}
.
Это предотвращает неглубокий рендеринг для вызова событий жизненного цикла реакции, таких как componentDidMount
, после рендеринга. В результате componentDidMount
не будет вызываться автоматически после неглубокой визуализации, и мы можем проверить наличие загрузки span
перед успешным вызовом api.
it("must render a loading span before api call success", () => { expect(wrapper.find("span.loading").length).toBe(1); });
приступим ко второму тесту.
Должен отображать тег
paragraph
с именем пользователя после успешного вызова API и скрывать загрузкуspans
const spyDidMount = jest.spyOn(Hello.prototype,"componentDidMount");
В приведенном выше коде мы шпионим за componentDidMount
, чтобы проверить, был ли вызван componentDidMount
.
fetch.mockImplementation(() => { return Promise.resolve({ status: 200, json: () => { return Promise.resolve({ userName: "manas", userId: 1 }); } }); });
В приведенном выше коде мы создаем фиктивную реализацию вызова fetch для нашего api. Это разрешается в пользовательский объект {userName:"manas",userId:1}
.
const didMount = wrapper.instance().componentDidMount();
Теперь я вручную вызываю метод componentDidMount
объекта wrapper
instance
, который возвращает обещание, поскольку наша componentDidMount
является async
функцией.
didMount.then(() => { // updating the wrapper wrapper.update(); expect(wrapper.find("p.user").text()).toContain("manas"); expect(wrapper.find("spans.loading").length).toBe(0); spyDidMount.mockRestore(); fetch.mockClear(); done(); });
Поскольку wrapper.instance().componentDidMount()
возвращает обещание, мы можем использовать его then()
метод для написания наших тестов, перед этим давайте обновим оболочку с помощью wrapper.update()
, поскольку успешный вызов api должен был вызвать setState({user})
и это должно было обновить представление Hello
компонента.
expect(wrapper.find("p.user").text()).toContain("manas"); expect(wrapper.find("spans.loading").length).toBe(0);
Ожидается, что оболочка теперь должна иметь абзац с текстом manas
внутри него. И ожидаем, что span
с классом загрузки больше не доступен в обновленном представлении.
spyDidMount.mockRestore(); fetch.mockClear();
Восстановление шпиона и очистка мока.
И последнее, но не менее важное: не забудьте вызвать done()
, который сообщает шутку, которую мы закончили с асинхронным тестом.