Intro
В первый раз, когда я активно отвечаю на вопрос о переполнении стека, мне пришлось создать учетную запись, но здесь все идет.
Юнит-тестирование редукционных действий - все о тестировании совместной работы.Всегда разумно отделять «модульные» тесты для совместной работы от простых, простых для всех, модульных тестов
Первая очистка
Сначала я бы абстрагировал вызовы выборки и хранилища локали в их собственные функции-оболочки.Это имеет пару преимуществ:
- легче тестировать (функции совместной работы Тесты AKA не должны выполнять большую логику, просто вызывая другие функции)
- API - это ваше управление (легкобиблиотеки подкачки)
// api.js
import {
API_URL_AUTH,
API_URL,
CLIENT_ID,
CLIENT_SECRET,
} from "../config/consts";
const authenticationData = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: "password",
};
export const fetchToken = ({ username, password }) => {
return fetch(API_URL_AUTH + "oauth/token", {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({ ...authenticationData, username, password }),
})).json()
}
export fetchUser = token => fetch(API_URL + "user", {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
method: "GET",
})).json();
// localstorage.js
// import { AsyncStorage } from 'some-library' ???
export const storeItem = (key, value)=> AsyncStorage.setItem(key, value);
Это делает код действий намного более понятным, и это немного более прямолинейно в том, что вы пытаетесь выполнить, что в конечном итоге приведет к более легкому тестированию.
// actions.js
import { fetchToken, fetchUser } from "./api";
import { storeItem } from "./localstorage";
import { FETCHING, FETCHED } from "../actions/ActionTypes";
export const loginUser = ({ username, password }) => async (
dispatch,
getState,
{ signInUser, getCurrentUser }
) => {
dispatch({ type: FETCHING }); // probably needs a better action type
try {
const response = await fetchToken({ username, password });
if (!response.error) {
await storeItem ("accessToken", "Bearer " + response.access_token);
await fetchUser(response.access_token)
} else {
showMessage({
message: response.message,
type: "danger",
});
}
dispatch({ type: FETCHED }); // needs better action type
} catch (e) {
showMessage({
message: response.message,
type: "danger",
});
}
}
Да тесты
Обратите внимание, что (я считаю) действия в Redux являются тестами для коллабораторов, что означает, что они идеально подходят для функций имитации, поскольку вы только тестируете взаимодействия, а не фактическиелогика функций.Простые логические функции просты в модульном тестировании, функции, которые обертывают библиотеки (так что вы можете управлять API), обычно вообще не тестируются, или вы можете написать пару интеграционных тестов, которые проверяют обертки изолированно, но интегрируются ссама библиотека.
Вы видите, что, извлекая логику, сами действия становятся очень прямыми и, как результат, облегчают тестирование.
Из библиотеки testdouble, которую я использовал, следует отметить, что td.when
также проверяет вызов.Поэтому, если в вашем коде вы случайно вызвали fetchToken({ email, password })
или что-то еще, тест завершился бы неудачей по двум причинам:
fetchToken
был вызван с неверными аргументами thenResolve
hasnне возвращает обещание, содержащее { access_token: 'token123' }
, означающее, что fetchUser
также не будет вызываться с правильными аргументами, поскольку access_token
будет undefined
describe('login', () => {
let getState;
let dependencies;
let dispatch;
beforeEach(() => {
// mocking out depencies using testdouble.js (mocking library, dont pay too much attention to it
getState = td.function('getState');
dependencies = {
fetchToken: td.function('fetchToken'),
fetchUser: td.function('fetchUser'),
storeItem: td.function('storeItem'),
};
dispatch = td.function('dispatch');
});
test('shows a message on failure, when the token could not be retrieved', () => {
// bla
});
test('shows a message on failure, when the user could not be retrieved', () => {
// bla
});
test('logs the user in', async () => {
const credentials = { username: "marco@polo.com", password: '123' };
const user = { /* some object */ }
td.when(dependencies.fetchToken(credentials)).thenResolve({
access_token: 'token123'
});
td.when(dependencies.fetchUser('token123')).thenResolve(user);
await loginUser(credentials)(dispatch, getState, dependencies);
td.verify(dispatch({ type: FETCHING }));
td.verify(dispatch({ type: FETCHED }));
});
});
Заключительные замечания
У меня есть суть на моем github, который является еще одним примером того же принципа, поскольку это общий вопрос для людей, вступающих в конфликт с React.Вы можете найти это здесь: https://gist.github.com/venikx/6e03367300f47625d5373826e86afeae
Я предлагаю вам взглянуть на https://github.com/testdouble/contributing-tests/wiki/Test-Double,, который является удивительным ресурсом для тестирования
И взглянуть на https://www.youtube.com/watch?v=Af4M8GMoxi4, который более подробно объясняет эти типы совместной работы (рассказывает о функциях-оболочках, простых логических функциях и о том, как они должны взаимодействовать с этими функциями совместной работы)
Самая важная часть в ваших тестах - это изоляция поведения, которое не должноне ломается, когда вы реорганизуете часть кода.Тесты должны упростить вам рефакторинг кода, а не сложнее (именно поэтому мы любим функции-оболочки).
Надеюсь, это помогло.Теперь я возвращаюсь к скрытности ^^