Я учусь использовать этот курс тестирования для тестирования подключенных компонентов, настраивая store factory
помощник по тестированию, который «создает хранилище для тестирования, соответствующее конфигурации нашего хранилища». Ниже вы можете увидеть мой подключенный образец компонента, а также код, используемый для настройки тестов, в котором я создаю подключенную неглубокую ферментную оболочку моего образца компонента. Однако похоже, что начальное состояние, которое я передаю образцу компонента, в данном случае {jotto: 'foo'}
не передается моему образцу компонента при создании этой неглубокой оболочки. Я что-то делаю не так и как мне правильно воссоздать необходимую конфигурацию хранилища при запуске ферментных тестов? Спасибо!
Пример компонента:
import React from 'react';
import {connect} from 'react-redux';
const SampleComponent = (props) => {
console.info(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.info(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] } } }
Просто напомню об этом курсе Udemy... это не самый лучший инструмент для обучения. Инструктор подходит к тестированию, используя data attributes
, которые не нужны для jest
и enzyme
тестирования (они также загромождают DOM
неиспользуемыми атрибутами).
Кроме того, ее опыт работы с кодом находится на уровне новичка, и она делает довольно много ошибок и странных вариантов кода. Тем не менее, узнайте из него все, что можно, и начните изучать тесты, созданные теми, кто поддерживает популярные пакеты npm (наиболее хорошо документированные и популярные пакеты будут содержать тесты, которые научат вас более практичному подходу к unit
и integration
тестированию).
В любом случае, я отвлекся, у вас есть два варианта тестирования container
:
export
class
/pure function
, shallow
или mount
оберните его и обновите поддельными реквизитами (очень просто, меньше головной боли и чаще делается)<Provider>
и react-router-dom <MemoryRouter>
, а затем mount
(может стать очень сложным, поскольку требует полуглубокого понимания: фермента и того, как он интерпретирует DOM при монтировании компонента, действия редукса/ потока редуктора, как создавать фиктивные реализации и/или фиктивные файлы и как правильно обрабатывать действия на основе promise
).Рабочие примеры (щелкните вкладку Tests
, чтобы запустить тесты; найдите .tests.js
в каталогах, указанных ниже):
Примечание. Codesandbox в настоящее время имеет некоторые ограничения тестирования, как указано ниже, поэтому, пожалуйста, настройте его для своего локального проекта.
контейнеры/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: "[email protected]",
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);
});
});
контейнеры/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: "[email protected]",
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);
});
});
Вы держите пари. Вместо 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__/…
Мы с коллегой говорили о ее практиках, и хотя она научит основам тестирования, она также научит некоторым вредным привычкам (например, использованию refs
для извлечения input
значений, когда вы должны использовать state
для управления формами: reactjs.org/docs/forms.html#управляемые-компоненты). Тем не менее, я настоятельно рекомендую этот курс в качестве альтернативы: udemy.com/react-2-е издание
Я считаю атрибут data-test более надежным, чем выбор элементов на основе имени класса или даже имени элемента. Другие разработчики могут не знать, что имя класса используется для целей тестирования, и имена элементов могут быть изменены, если вводятся стилизованные компоненты. С атрибутами data-test ясно, для чего используется атрибут, и нет необходимости его менять. Также вы можете использовать npmjs.com/package/babel-plugin-react-remove-properties, чтобы удалить атрибуты проверки данных из производственной сборки.
Решение: я забыл параметр браузера для своего корневого редуктора, поскольку использовал connected-react-router
.
import rootReducer from 'reducers/rootReducer';
import {createBrowserHistory} from 'history';
export const storeFactory = (initialState) => createStore(rootReducer(createBrowserHistory()), initialState);
Это исправило мою проблему!
Спасибо, Мэтт! Какую альтернативу вы бы предложили вместо использования атрибутов данных? Есть ли у вас какие-либо предложения по пакетам для обзора, которые имеют хорошие принципы тестирования, и какие-либо другие ресурсы, блоги, курсы, на которые можно взглянуть? Я скажу в ее защиту, что мне очень нравится курс и то, как она подходит к предмету, и я определенно многому учусь, но ценю любые отзывы и другие точки зрения!