#reactjs #redux #react-redux #enzyme
#reactjs #сокращение #реагирование-сокращение #фермент
Вопрос:
Я учусь на этом курсе тестирования тестировать подключенные компоненты, настраивая store factory
помощник по тестированию, который «создает хранилище для тестирования, соответствующее конфигурации нашего хранилища». Ниже вы можете увидеть мой подключенный образец компонента, а также код, используемый для настройки тестов, в которых я создаю подключенную неглубокую ферментную оболочку моего образца компонента. Однако, похоже, что начальное состояние, которое я передаю компоненту sample, в данном случае {jotto: 'foo'}
не передается моему компоненту sample при создании этой мелкой оболочки. Я делаю что-то не так и как я могу правильно воссоздать необходимую конфигурацию хранилища при выполнении тестов enzyme? Спасибо!
Образец компонента:
import React from 'react';
import {connect} from 'react-redux';
const SampleComponent = (props) => {
console.log(props);
return (
<div>This is a sample component!</div>
);
};
const mapStateToProps = (state) => ({
jotto: state.jotto,
});
export default connect(mapStateToProps)(SampleComponent);
редуктор:
import * as jottoActionTypes from 'actionTypes/jottoActionTypes';
export const initialState = {
isSuccess: false,
};
const jotto = (state = initialState, action) => {
switch (action.type) {
case jottoActionTypes.CORRECT_GUESS:
return {
...state,
isSuccess: true,
};
default:
return state;
}
};
export default jotto;
корневой редуктор:
import {combineReducers} from 'redux';
import {connectRouter} from 'connected-react-router';
import jotto from 'reducers/jottoReducer';
export default (historyObject) => combineReducers({
jotto,
router: connectRouter(historyObject),
});
Настройка теста:
import React from 'react';
import {shallow} from 'enzyme';
import {createStore} from 'redux';
import rootReducer from 'reducers/rootReducer';
import SampleComponent from './sampleComponent';
export const storeFactory = (initialState) => createStore(rootReducer, initialState);
const store = storeFactory({jotto: 'foo'});
const wrapper = shallow(<SampleComponent store={store} />).dive();
console.log(wrapper.debug());
// Result:
{ store:
{ dispatch: [Function: dispatch],
subscribe: [Function: subscribe],
getState: [Function: getState],
replaceReducer: [Function: replaceReducer],
[Symbol(observable)]: [Function: observable] },
jotto: undefined,
dispatch: [Function: dispatch],
storeSubscription:
Subscription {
store:
{ dispatch: [Function: dispatch],
subscribe: [Function: subscribe],
getState: [Function: getState],
replaceReducer: [Function: replaceReducer],
[Symbol(observable)]: [Function: observable] },
parentSub: undefined,
onStateChange: [Function: bound onStateChange],
unsubscribe: [Function: unsubscribe],
listeners:
{ clear: [Function: clear],
notify: [Function: notify],
get: [Function: get],
subscribe: [Function: subscribe] } } }
Ответ №1:
Просто предупреждаю об этом курсе Udemy … это не лучший инструмент обучения. Инструктор подходит к тестированию, используя data attributes
которые не нужны для jest
и enzyme
тестирования (они также заполняют DOM
неиспользуемыми атрибутами).
Кроме того, ее опыт работы с кодом находится примерно на начальном уровне, и она допускает довольно много ошибок и нечетных вариантов выбора кода. Тем не менее, извлеките из этого все, что сможете, и начните изучать тесты, созданные теми, кто поддерживает популярные пакеты npm (большинство хорошо документированных и популярных пакетов будут содержать тесты, которые научат вас более практичному подходу к unit
и integration
тестированию).
В любом случае, я отвлекся, у вас есть два варианта тестирования container
:
export
class
/pure function
,shallow
илиmount
оберните его и обновите поддельными реквизитами (очень просто, меньше головной боли и чаще выполняется)- Оберните свой компонент в redux
<Provider>
и react-router-dom<MemoryRouter>
, а затемmount
это (может стать очень сложным, поскольку это требует полу-глубокого понимания: enzyme и того, как он интерпретирует DOM при монтировании компонента, потока действий / редуктора redux, как создавать фиктивные реализации и / или фиктивные файлы и как правильно обрабатыватьpromise
действия на основе).
Рабочие примеры (перейдите на Tests
вкладку, чтобы запустить тесты; найдите .tests.js
в каталогах, упомянутых ниже):
Примечание: Codesandbox в настоящее время имеет некоторые ограничения тестирования, как указано ниже, поэтому, пожалуйста, настройте для вашего локального проекта.
containers/Dashboard/__tests__/UnconnectedDashboard.test.js (вы можете так же легко mount
обернуть этот несвязанный компонент для утверждения в отношении его глубоко вложенных дочерних узлов)
import { Dashboard } from "../index.js";
/*
codesandbox doesn't currently support mocking, so it's making real
calls to the API; as a result, the lifecycle methods have been
disabled to prevent this, and that's why I'm manually calling
componentDidMount.
*/
const getCurrentProfile = jest.fn();
const fakeUser = {
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz",
address: {
street: "Kulas Light",
suite: "Apt. 556",
city: "Gwenborough",
zipcode: "92998-3874",
geo: {
lat: "-37.3159",
lng: "81.1496"
}
},
phone: "1-770-736-8031 x56442",
website: "hildegard.org",
company: {
name: "Romaguera-Crona",
catchPhrase: "Multi-layered client-server neural-net",
bs: "harness real-time e-markets"
}
};
const initialProps = {
getCurrentProfile,
currentUser: {},
isLoading: true
};
describe("Unconnected Dashboard Component", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Dashboard {...initialProps} />);
wrapper.instance().componentDidMount();
});
afterEach(() => wrapper.unmount());
it("initially renders a spinnner", () => {
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("Spinner")).toHaveLength(1);
});
it("displays the current user", () => {
wrapper.setProps({ currentUser: fakeUser, isLoading: false });
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
});
it("displays a signup message if no users exist", () => {
wrapper.setProps({ isLoading: false });
expect(getCurrentProfile).toHaveBeenCalled();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
});
});
containers/Dashboard/__tests__/ConnectedDashboard.test.js
import Dashboard from "../index";
// import { getCurrentProfile } from "../../../actions/profileActions";
import * as types from "../../../types";
/*
codesandbox doesn't currently support mocking, so it's making real
calls to the API; however, actions like getCurrentProfile, should be
mocked as shown below -- in your case, you wouldn't need to use
a promise, but instead just mock the "guessedWord" action and return
store.dispatch({ ... })
*/
const fakeUser = {
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "Sincere@april.biz",
address: {
street: "Kulas Light",
suite: "Apt. 556",
city: "Gwenborough",
zipcode: "92998-3874",
geo: {
lat: "-37.3159",
lng: "81.1496"
}
},
phone: "1-770-736-8031 x56442",
website: "hildegard.org",
company: {
name: "Romaguera-Crona",
catchPhrase: "Multi-layered client-server neural-net",
bs: "harness real-time e-markets"
}
};
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
describe("Connected Dashboard Component", () => {
let store;
let wrapper;
beforeEach(() => {
store = createStoreFactory();
wrapper = mount(
<Provider store={store}>
<MemoryRouter>
<Dashboard />
</MemoryRouter>
</Provider>
);
});
afterEach(() => wrapper.unmount());
it("initially displays a spinner", () => {
expect(wrapper.find("Spinner")).toHaveLength(1);
});
it("displays the current user after a successful API call", async () => {
/*
getCurrentProfile.mockImplementationOnce(() => new Promise(resolve => {
resolve(
store.dispatch({
type: types.SET_SIGNEDIN_USER,
payload: fakeUser
})
);
});
await flushPromises();
wrapper.update();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
*/
store.dispatch({
type: types.SET_SIGNEDIN_USER,
payload: fakeUser
});
wrapper.update();
expect(wrapper.find("DisplayUser")).toHaveLength(1);
});
it("displays a signup message if no users exist", async () => {
/*
getCurrentProfile.mockImplementationOnce(() => new Promise((resolve,reject) => {
reject(
store.dispatch({
type: types.FAILED_SIGNEDIN_USER
})
);
});
await flushPromises();
wrapper.update();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
*/
store.dispatch({
type: types.FAILED_SIGNEDIN_USER
});
wrapper.update();
expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
});
});
Комментарии:
1. Спасибо, Мэтт! Какую альтернативу вы бы предложили вместо использования атрибутов данных? Есть ли у вас какие-либо предложения по пакетам для ознакомления, в которых есть хорошие принципы тестирования, и какие-либо другие ресурсы, блоги, курсы, на которые можно обратить внимание? Я скажу в ее защиту, что мне действительно нравится курс и то, как она подходит к предмету, и я определенно многому учусь, но ценю любые отзывы и другие точки зрения!
2. Еще бы. Вместо использования
data attributes
вы можете найти по:component
(Spinner
),element.className
(div.sk-fading-circle
),className
(.sk-fading-circle
), .. и так далее. Смотрите список примеров здесь: airbnb.io/enzyme/docs/api/ReactWrapper/find.html . Что касается пакетов, я использовал этот, чтобы начать свое обучение: github.com/JedWatson/react-select/blob/master/src/__tests __/…3. Мы с коллегой говорили о ее практике, и, хотя она расскажет о некоторых основах тестирования, она также научит некоторым вредным привычкам (например, использованию
refs
для извлеченияinput
значений, когда вы должны использовать levargingstate
для управления формами: reactjs.org/docs/forms.html#controlled-components ). Тем не менее, я бы настоятельно рекомендовал этот курс в качестве альтернативы: udemy.com/react-2nd-edition4. Я нахожу атрибут data-test более надежным, чем выбор элементов на основе имени класса или даже имени элемента. Другие разработчики могут не знать, что имя класса используется для целей тестирования, и имена элементов могут быть изменены, если введены стилизованные компоненты. С атрибутами data-test понятно, для чего используется attr, и нет необходимости его изменять. Также вы можете использовать npmjs.com/package/babel-plugin-react-remove-properties чтобы удалить атрибуты проверки данных из рабочей сборки.
Ответ №2:
Решение: я забыл параметр браузера для моего корневого редуктора, учитывая, что я использовал connected-react-router
.
import rootReducer from 'reducers/rootReducer';
import {createBrowserHistory} from 'history';
export const storeFactory = (initialState) => createStore(rootReducer(createBrowserHistory()), initialState);
Комментарии:
1. Это устранило мою проблему!