Тестирование Ngrx State в Angular с помощью Jest
Update 15 марта 2020: Методы и подходы для организации state в данной статье устарели. Актуальная статья — Тестирование Ngrx store в Angular. Методы и подходы для упрощения тестирование stat’ов Ngrx в Nx.
Напишем тесты для Translation State
, разработке которого было описано в предыдущей статье.
Если вы ранее не работали с
Nx
илиNgrx
, то рекомендуется прочитать официальную документацию для данных технологий, а также посмотреть статью — О реализации State в Angular.
Для State обычно генерируются следующий список файлов тестов, которые можно увидеть в папке +state
:
translation.effects.spec.ts
translation.facade.spec.ts
translation.reducer.spec.ts
translation.selectors.spec.ts
Данные файлы содержат тесты для эффектов, фасада, редъюсера и селекторов. В списке, нет только тестов для экшенов, но они не тестируются, так как экшены это просто классы с одним/двумя полями, которые не содержат логики.
От классов с экшенами можно отказаться вообще, следуя руководству по
Ngrx
, но данные действия вряд ли упростят State.
Основные задачи тестов
При тестировании seletor’ов
, основной задачей тестов является возвращение значений из State.
При тестировании reducer’а
, основной задачей тестов является установка значений в хранилище State.
При тестировании effect’ов
, основной задачей тестов является вызов тех или иных action’ов, а также выполнение сопутствующей логики, такой как загрузка данных с сервера или вычисление специализированных значений и их сохранение.
При тестировании facade’а
, основной задачей тестов является воспроизведение полного цикла отработки события.
Тестирование Селекторов (Selectors)
Nx
генерирует тесты для селекторов, с демо данными. Удалим все тесты связанные с демо данными, а также переименуем название переменной, в которой храниться весь Store
, а также зададим строгие типы:
let store: TranslationPartialState;const KEY = TRANSLATION_FEATURE_KEY;beforeEach(() => {
store = createStore(KEY, initialState);
});
Отметим, что существует множество подходов к написанию тестов, в частности Unit тестов. В данной статье, будем придерживаться идеи, что основное назначение теста проверить работоспособность основного назначения тестируемого объекта.
В Translation State
хранит следующие значения, которые и будут протестированы:
currentLanguage
initialized
initError
initiating
language
languages
setError
setting
Одним из подходов, связанных с уменьшением кода в тестах, связан с созданием
Mock
|Stub
классов и данных, которые являются урезанными объектами, каких-либо данных и сервисов. Так как в State обычно хранятся простые объекты и значения, то для данных значений создается набор данных с постфиксом, с шаблоном вида — <NAME_PROPERTY
>^Stub
.
В папке с библиотекой translation
внутри папки testing
, создадим папку stubs
, в который добавим файл —translation.stub.ts
:
// libs/translation/src/testing/stub/translation.stub.tsimport { TranslationConfig } from '@luxcar/translation';
export const languageEnStub = 'en';
export const languageRuStub = 'ru';
export const languagesStub = [languageEnStub, languageRuStub];
export const translationConfigStub: TranslationConfig = {
currentLanguage: languageRuStub,
language: languageEnStub,
languages: languagesStub
};
Напишем первый тест для currentLanguage
:
it('getCurrentLanguage() should return current language', () => {
store = createStore(KEY, initialState, { currentLanguage: languageEnStub });
const results = translationQuery.getCurrentLanguage(store);
expect(results).toBe(languageEnStub);
});
В тесте можно увидеть, что перед выполнением запроса данных из Store
, сначала устанавливаются требуемые значения.
Также в названии теста будет строка, шаблон которой:
get<NAME_STATE_PROPERTY>() should return NAME_STATE_PROPERTY
Для задания сторы, используется функция createStore
, которая делает только присвоение и мерж объектов по ключу и возвращает результат:
{ [STATE_KEY]: {...initialState, ...overrideInitialProperties } }
Фактически, все тесты select’ов
будут выглядеть именно так, как был описан тест.
Теперь приведем все тесты для Translation Select’ов
:
import { createStore } from '@medium-stories/store/testing';
import { languageEnStub, languagesStub, translationErrorStub } from '../../testing';
import { initialState, TRANSLATION_FEATURE_KEY, TranslationPartialState } from './translation.reducer';
import { translationQuery } from './translation.selectors';
describe('TranslationSelectors', () => {
const KEY = TRANSLATION_FEATURE_KEY;
let store: TranslationPartialState;
beforeEach(() => {
store = createStore(KEY, initialState);
});
describe('Translation Selectors', () => {
it('getCurrentLanguage() should return current language', () => {
store = createStore(KEY, initialState, { currentLanguage: languageEnStub });
const results = translationQuery.getCurrentLanguage(store);
expect(results).toBe(languageEnStub);
});
it('getInitialized() should return initialized', () => {
store = createStore(KEY, initialState, { initialized: true });
const results = translationQuery.getInitialized(store);
expect(results).toBeTruthy();
});
it('getInitError() should return initError', () => {
store = createStore(KEY, initialState, { initError: translationErrorStub });
const results = translationQuery.getInitError(store);
expect(results).toBe(translationErrorStub);
});
it('getInitiating() should return initiating', () => {
store = createStore(KEY, initialState, { initiating: true });
const results = translationQuery.getInitiating(store);
expect(results).toBeTruthy();
});
it('getLanguage() should return language', () => {
store = createStore(KEY, initialState, { language: languageEnStub });
const results = translationQuery.getLanguage(store);
expect(results).toBe(languageEnStub);
});
it('getLanguages() should return languages', () => {
store = createStore(KEY, initialState, { languages: languagesStub });
const results = translationQuery.getLanguages(store);
expect(results.length).toBe(languagesStub.length);
});
it('getSetError() should return setError', () => {
store = createStore(KEY, initialState, { setError: translationErrorStub });
const results = translationQuery.getSetError(store);
expect(results).toBe(translationErrorStub);
});
it('getSetting() should return setting', () => {
store = createStore(KEY, initialState, { setting: true });
const results = translationQuery.getSetting(store);
expect(results).toBeTruthy();
});
});
});
Заметим, что мы не используем сокращенные пути для тестов
import { languageEnStub, languagesStub, translationErrorStub } from '../../testing';
хотя возможно использовать и короткие пути*:
import { languageEnStub, languagesStub, translationErrorStub } from '@medium-stories/translation/testing';
Какой подход использовать — решайте сами, но короткие ссылки, могут породить ненужные зависимости в сборках проекта, таких как webpack
, что в дальнейшем скажется на производительности и оптимизации приложения, поэтому используйте это осторожно.
Если вы не знакомы с
Jest
и тестированием вJavascript/Typescript
, рекомендуется прочитать ознакомиться с документацией поJest
, а также прочитать про тестирование на сайтеNx Jest
.
Теперь запустим написанные тесты:
ng test translation -t TranslationSelectors
В результате получим:
Тестирование Reducer’а
Отредактируем файл с тестом, сгенерированный Nx
. Удалим все функции и переменные, и добавим наш state и зададим в него initialState
let state: TranslationState;
beforeEach(() => {
state = initialState;
});
Как упоминалось ранее, все тесты reducer’а направлены на то, чтобы установить значения в State. Поэтому напишем тесты для всех action’ов, которые приводят к изменению значению State’а.
Для Translation State
это:
InitiatingTranslation
TranslationInitialized
TranslationInitError
SettingLanguage
LanguageSet
LanguageSetError
Напишем тест для — InitiatingTranslation
:
it('InitiatingTranslation() should set initError null and initiating true', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const result = translationReducer(state, action);
expect(result.initError).toBeNull();
expect(result.initiating).toBeTruthy();
});
Для названия теста, обычно используется следующий шаблон:
ACTION_NAME() should set LIST_CHANGED PROPERTIES [WITH_VALUES]
в котором можно указать, изменяющиеся сохраняемые свойства, но обычно туда указываются значимые свойства.
Чтобы понять, какое свойство значимое, подумайте, если данный тест не пройдет, то какую информацию вы хотели бы увидеть — часто у вам будет состояние State и сообщение теста в котором написано, что initiating должен быть true, а в State он false.
Для задания State, используется функция createState
, которая делает только присвоение и мерж объектов и возвращает результат:
{...initialState, ...overrideInitialProperties }
Большинство тестов для reducer’а
будут написаны по аналогичному алгоритму.
Приведем все тесты для Translate Reducer
:
import { createState } from '@medium-stories/store/testing';
import { languageEnStub, translationConfigStub, translationErrorStub } from '../../testing';
import { fromTranslationActions } from './translation.actions';
import { initialState, translationReducer, TranslationState } from './translation.reducer';
describe('TranslationReducer', () => {
let state: TranslationState;
beforeEach(() => {
state = initialState;
});
describe('valid Translation actions ', () => {
it('InitiatingTranslation() should set initError null and initiating true', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const result = translationReducer(state, action);
expect(result.initError).toBeNull();
expect(result.initiating).toBeTruthy();
});
it('TranslationInitialized() should set initiating false and set translation config', () => {
state = createState(initialState, { initiating: true });
const action = new fromTranslationActions.TranslationInitialized(translationConfigStub);
const result = translationReducer(state, action);
expect(result.currentLanguage).toBe(translationConfigStub.currentLanguage);
expect(result.language).toBe(translationConfigStub.language);
expect(result.languages.length).toBe(translationConfigStub.languages.length);
expect(result.initiating).toBeFalsy();
});
it('TranslationInitError() should set initError and initiating false', () => {
state = createState(initialState, { initiating: true });
const action = new fromTranslationActions.TranslationInitError(translationErrorStub);
const result = translationReducer(state, action);
expect(result.initError).toBe(translationErrorStub);
expect(result.initiating).toBeFalsy();
});
it('SettingLanguage() should set setError null, currentLanguage = lang and setting true', () => {
const action = new fromTranslationActions.SettingLanguage(languageEnStub);
const result = translationReducer(state, action);
expect(result.currentLanguage).toBe(languageEnStub);
expect(result.setError).toBeNull();
expect(result.setting).toBeTruthy();
});
it('LanguageSet() should set setting false', () => {
state = createState(initialState, { setting: true });
const action = new fromTranslationActions.LanguageSet();
const result = translationReducer(state, action);
expect(result.setting).toBeFalsy();
});
it('LanguageSetError() should set setError and setting false', () => {
state = createState(initialState, { setting: true });
const action = new fromTranslationActions.LanguageSetError(translationErrorStub);
const result = translationReducer(state, action);
expect(result.setError).toBe(translationErrorStub);
expect(result.setting).toBeFalsy();
});
});
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = translationReducer(initialState, action);
expect(result).toBe(initialState);
});
});
});
Единственное замечание в том, что если в тесте проверяется какое либо поле, которое должно быть изменено, то нужно его установить, чтобы reducer
корректно его обработал, если это необходимо.
Теперь запустим написанные тесты:
ng test translation -t TranslationReducer
Результаты работы теста для reducer’а
:
Тестирование Effect’ов
Наверное, тестирование эффектов, является самой важной и самой сложной частью в покрытии тестами State
.
Так как Translation Effects
используют AbstractEffects
, вы можете ознакомиться с данным классов, в конце статьи, в разделе Утилиты и вспомогательные функции.
Если вы ранее не тестировали рекреативные методы или не знакомы с
Marble testing
, то рекомендуется ознакомиться с данной статьей.
Отредактируем файл теста эффектов. Сначала добавим основные объекты, с помощью которых мы будем тестировать эффекты:
let actions$: Observable<any>;
let effects: TranslationEffects;
let service: TranslationService;
let scheduler: TestScheduler;
let store: Store<TranslationPartialState>;
Настройка Angular будет следующей
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot(
{ [TRANSLATION_FEATURE_KEY]: translationReducer },
{
initialState: { [TRANSLATION_FEATURE_KEY]: initialState },
runtimeChecks: { strictActionImmutability: false }
}
),
EffectsModule.forRoot([])
],
providers: [
TranslationEffects,
DataPersistence,
provideMockActions(() => actions$),
{
provide: TranslationService,
useValue: {
setLanguage: jest.fn(),
getConfig: jest.fn()
}
}
]
});
store = TestBed.get(Store);
effects = TestBed.get(TranslationEffects);
scheduler = getTestScheduler();
const duration = scheduler.createTime('-|');
effects.setRetryStrategyOptions({
maxRetryAttempts: 3,
scalingDuration: duration,
scheduler
});
service = TestBed.get(TranslationService);
});
Как видно из кода, был сконфигурирован тестовый компонент, в которого инициализирован Store
, для корректной работы effect’ов
добавлены все провайдеры, которые объявлены в конструкторе эффектов
Также была сделана установка для marble testing
c созданием scheduler
.
В конце, получаем имплеминтацию Translation Service
из тестируемого компонента.
Теперь напишем тесты для инициализации языка. Для первого эффекта, будет два теста:
describe('init$', () => {
it('should return initiating translation', () => {
const action = new fromTranslationActions.InitTranslation();
const completion = new fromTranslationActions.InitiatingTranslation();
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.init$).toBeObservable(expected);
});
it('should return translation init canceled', () => {
store.dispatch(new fromTranslationActions.InitiatingTranslation());
const action = new fromTranslationActions.InitTranslation();
const completion = new fromTranslationActions.TranslationInitCanceled();
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.init$).toBeObservable(expected);
});
});
Первый случай, это корректный случай, когда была проверка на инициализацию, и так как ранее translation state
не был инициализирован, должен запуститься процесс инициализации.
Второй случай, когда translation state
уже находится в процессе инициализации. В данном случае, просто отменяется данный запрос на повторную инициализацию.
Второй эффект содержит тесты, для процесса инициализации
describe('initiating$', () => {
it('should return translation initialized', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const completion = new fromTranslationActions.TranslationInitialized(translationConfigStub);
actions$ = hot('^-a--', { a: action });
const response = cold('-a|', { a: null });
const expected = cold('---b', { b: completion });
service.getConfig = jest.fn(() => translationConfigStub);
service.init = jest.fn(() => response);
expect(effects.initiating$).toBeObservable(expected);
});
it('should return translation init error', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const completion = new fromTranslationActions.TranslationInitError(translationErrorStub);
actions$ = hot('^-a---', { a: action });
const response = cold('-#|', {}, translationErrorStub);
const expected = cold('---b', { b: completion });
service.getConfig = jest.fn(() => translationConfigStub);
service.init = jest.fn(() => response);
expect(effects.initiating$).toBeObservable(expected);
});
});
Как и в первом эффекте, есть два случая.
В первом случае, рассматривается кейс, когда инициализация произошла успешно, и translation state
был инициализирован.
Во втором случае, в ходе процесса инициализации произошла ошибка, и эффект вызывает соответствующий action
с ошибкой инициализации.
Данный алгоритм применим почти ко всем эффектам, разрабатываемым в данном приложении. Приведем оставшиеся тесты для эффектов.
import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { Store, StoreModule } from '@ngrx/store';
import { provideMockActions } from '@ngrx/effects/testing';
import { DataPersistence, NxModule } from '@nrwl/angular';
import { cold, getTestScheduler, hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { languageEnStub, translationConfigStub, translationErrorStub } from '../../testing';
import { TranslationService } from '../interfaces/translation-service.interface';
import { fromTranslationActions } from './translation.actions';
import { TranslationEffects } from './translation.effects';
import { initialState, TRANSLATION_FEATURE_KEY, TranslationPartialState, translationReducer } from './translation.reducer';
describe('TranslationEffects', () => {
let actions$: Observable<any>;
let effects: TranslationEffects;
let service: TranslationService;
let scheduler: TestScheduler;
let store: Store<TranslationPartialState>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot(
{ [TRANSLATION_FEATURE_KEY]: translationReducer },
{
initialState: { [TRANSLATION_FEATURE_KEY]: initialState },
runtimeChecks: { strictActionImmutability: false }
}
),
EffectsModule.forRoot([])
],
providers: [
TranslationEffects,
DataPersistence,
provideMockActions(() => actions$),
{
provide: TranslationService,
useValue: {
init: jest.fn(),
getConfig: jest.fn(),
setLanguage: jest.fn()
}
}
]
});
store = TestBed.get(Store);
effects = TestBed.get(TranslationEffects);
scheduler = getTestScheduler();
const duration = scheduler.createTime('-|');
effects.setRetryStrategyOptions({
maxRetryAttempts: 3,
scalingDuration: duration,
scheduler
});
service = TestBed.get(TranslationService);
});
describe('init$', () => {
it('should return initiating translation', () => {
const action = new fromTranslationActions.InitTranslation();
const completion = new fromTranslationActions.InitiatingTranslation();
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.init$).toBeObservable(expected);
});
it('should return translation init canceled', () => {
store.dispatch(new fromTranslationActions.InitiatingTranslation());
const action = new fromTranslationActions.InitTranslation();
const completion = new fromTranslationActions.TranslationInitCanceled();
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.init$).toBeObservable(expected);
});
});
describe('initiating$', () => {
it('should return translation initialized', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const completion = new fromTranslationActions.TranslationInitialized(translationConfigStub);
actions$ = hot('^-a--', { a: action });
const response = cold('-a|', { a: null });
const expected = cold('---b', { b: completion });
service.getConfig = jest.fn(() => translationConfigStub);
service.init = jest.fn(() => response);
expect(effects.initiating$).toBeObservable(expected);
});
it('should return translation init error', () => {
const action = new fromTranslationActions.InitiatingTranslation();
const completion = new fromTranslationActions.TranslationInitError(translationErrorStub);
actions$ = hot('^-a---', { a: action });
const response = cold('-#|', {}, translationErrorStub);
const expected = cold('---b', { b: completion });
service.getConfig = jest.fn(() => translationConfigStub);
service.init = jest.fn(() => response);
expect(effects.initiating$).toBeObservable(expected);
});
});
describe('set$', () => {
it('should return set language', () => {
store.dispatch(new fromTranslationActions.TranslationInitialized(translationConfigStub));
const action = new fromTranslationActions.SetLanguage(languageEnStub);
const completion = new fromTranslationActions.SettingLanguage(languageEnStub);
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.set$).toBeObservable(expected);
});
it('should return language set canceled', () => {
store.dispatch(new fromTranslationActions.TranslationInitialized(translationConfigStub));
store.dispatch(new fromTranslationActions.SettingLanguage(languageEnStub));
const action = new fromTranslationActions.SetLanguage(languageEnStub);
const completion = new fromTranslationActions.LanguageSetCanceled();
actions$ = hot('^-a--', { a: action });
const expected = cold('--b', { b: completion });
expect(effects.set$).toBeObservable(expected);
});
});
describe('setting$', () => {
it('should return language set', () => {
store.dispatch(new fromTranslationActions.TranslationInitialized(translationConfigStub));
const action = new fromTranslationActions.SettingLanguage(languageEnStub);
const completion = new fromTranslationActions.LanguageSet();
actions$ = hot('^-a--', { a: action });
const response = cold('-a|', { a: null });
const expected = cold('---b', { b: completion });
service.setLanguage = jest.fn(() => response);
expect(effects.setting$).toBeObservable(expected);
});
it('should return translation save error', () => {
store.dispatch(new fromTranslationActions.TranslationInitialized(translationConfigStub));
const action = new fromTranslationActions.SettingLanguage(languageEnStub);
const completion = new fromTranslationActions.LanguageSetError(translationErrorStub);
actions$ = hot('^-a---', { a: action });
const response = cold('-#|', {}, translationErrorStub);
const expected = cold('---b', { b: completion });
service.setLanguage = jest.fn(() => response);
expect(effects.setting$).toBeObservable(expected);
});
});
});
Теперь запустим написанные тесты
ng test translation -t TranslationEffects
Результаты работы теста для эффектов:
Тестирование Translation Facade
Последней частью является тестирование facade
. Но так как все основные тесты были сделаны ранее, тесты для facade чаще всего просто является дополнительным слоем защиты на работоспособность State и не являются достаточно подробными, но тут стоит отметить, что все зависит сколько и какую логику вы закладываете в facade
. В данных статьях это всего лишь вызов новых action’ов и обращение к observable
свойствам state через селекторы.
Содержимое файла теста почти аналогично файлу теста эффектов, за исключением значения теста:
import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { readFirst } from '@nrwl/angular/testing';
import { of } from 'rxjs';
import { translationConfigStub } from '../../testing';
import { TranslationService } from '../interfaces/translation-service.interface';
import { TranslationEffects } from './translation.effects';
import { TranslationFacade } from './translation.facade';
import { initialState, TRANSLATION_FEATURE_KEY, TranslationPartialState, translationReducer } from './translation.reducer';
describe('TranslationFacade', () => {
let facade: TranslationFacade;
let store: Store<TranslationPartialState>;
beforeEach(() => {});
describe('used in NgModule', () => {
beforeEach(() => {
@NgModule({
imports: [
StoreModule.forFeature(TRANSLATION_FEATURE_KEY, translationReducer, { initialState }),
EffectsModule.forFeature([TranslationEffects])
],
providers: [
TranslationFacade,
{
provide: TranslationService,
useValue: {
init: jest.fn(() => of(null)),
getConfig: jest.fn(() => translationConfigStub),
setLanguage: jest.fn(() => of(null))
}
}
]
})
class CustomFeatureModule {}
@NgModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot(
{},
{
runtimeChecks: { strictActionImmutability: false }
}
),
EffectsModule.forRoot([]),
CustomFeatureModule
]
})
class RootModule {}
TestBed.configureTestingModule({ imports: [RootModule] });
store = TestBed.get(Store);
facade = TestBed.get(TranslationFacade);
});
it('init() should initialized language on app', async done => {
try {
let initialized = await readFirst(facade.initialized$);
expect(initialized).toBeFalsy();
facade.init();
initialized = await readFirst(facade.initialized$);
expect(initialized).toBeTruthy();
done();
} catch (err) {
done.fail(err);
}
});
});
});
В данном случае реализуем, был реализован один тест для инициализации языка.
Теперь запустим написанные тесты:
ng test translation -t TranslationFacade
После запуска увидим:
Теперь запустим все тесты в translation
, и удостоверимся, что все тесты проходят:
ng test translation
Все тесты прошли! :)
Утилиты и вспомогательные функции
Abstract Effects
AbstractEffects
— абстрактный класс эффектов, в которых будут методы для задания общих свойств, а также обертка над Store
.
Отметим, что наследование зачастую является плохим решением, и данную технику ООП стоит использовать осторожно. Автор статьи, придерживается мнения, если можно обойтись без наследования, то лучше так и сделать.
import { DEFAULT_GENERIC_RETRY_STRATEGY, GenericRetryStrategyOptions } from '../utils/generic-retry-strategy.util';
import { md5 } from '../utils/md5.util';/**
* Abstract Effects
*/
export abstract class AbstractEffects<T = any> {
/**
* Generic retry strategy options
*/
protected genericRetryStrategyOptions = DEFAULT_GENERIC_RETRY_STRATEGY; protected constructor(protected readonly key: string) {} /**
* Return effect id by payload
* @param payload payload
*/
getIdByPayload(payload: any): string {
return md5(JSON.stringify(payload));
} /**
* Return state from "partial" store
* @param state State
* @param key External state key
*/
getState<S = any>(state: S, key?: string): T {
return state[key || this.key];
} /**
* Set generic retry strategy options
* Notice: Required only for testing
*
* @param options options
*/
setRetryStrategyOptions(options: Partial<GenericRetryStrategyOptions>): void {
this.genericRetryStrategyOptions = { ...this.genericRetryStrategyOptions, ...options };
}
}
Метод — getState
возвращает из глобального Store
, локальный State
по ключу.
Метод — getIdByPayload
позволяет задать Id effect’у
по payload’у
, который нужен для того, чтобы DataPersistence
смог отменить effect, если он был вызван повторно.
Метод — setRetryStrategyOptions
устанавливает параметры для GenericRetryStrategy
. Данный метод нужен формально только для тестирования.
GenericRetryStrategy
— это повторные запросы, для доступа к серверу или данным. Например, из-за недоступности сервера (502), вместо показа ошибки, GenericRetryStrategy
позволяет делать повторные запросы через 1 секунду, увеличивая ожидание на 1 секунду после каждой попытки. Если после 3 неудачных попыток, не было получено ответа, то только в этом случае показываем ошибку.
import { Observable, SchedulerLike, throwError, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';export interface GenericRetryStrategyOptions {
maxRetryAttempts?: number;
scalingDuration?: number;
excludedStatusCodes?: number[];
scheduler?: SchedulerLike;
}export const DEFAULT_GENERIC_RETRY_STRATEGY: GenericRetryStrategyOptions = {
maxRetryAttempts: 3,
scalingDuration: 1000,
excludedStatusCodes: [403],
scheduler: null
};export const genericRetryStrategy = ({
maxRetryAttempts = 3,
scalingDuration = 1000,
excludedStatusCodes = [403],
scheduler = null
}: GenericRetryStrategyOptions = {}) => (attempts: Observable<any>) => {
return attempts.pipe(
mergeMap((error, i) => {
const retryAttempt = i + 1;
if (retryAttempt > maxRetryAttempts || excludedStatusCodes.find(e => e === error.status)) {
return throwError(error);
}return timer(retryAttempt * scalingDuration, scheduler);
})
);
};
Также можно увидеть функцию — md5
. В данном случае, это простой клон функции md5
без вендоров, но возможно подключить библиотеку для реализации md5
, или вообще отказаться от md5
и генерировать единый хеш для Id
самостоятельно.
CreateStore и CreateState
Для тестирования будем использовать две простые функции:
/**
* Create store
*
* @param key State feature key
* @param initialState Initial state
* @param params Extend initial params
*/
export function createStore<S = object, P = object>(key: string, initialState: S, params: Partial<S> = {}): P {
return {
[key]: { ...initialState, ...params }
} as any;
}/**
* Create state
*
* @param initialState Initial state
* @param params Extend initial params
*/
export function createState<S = object>(initialState: S, params: Partial<S> = {}): S {
return { ...initialState, ...params };
}
Функция createStore
— позволяет создать Partial<RootState>
по заданному ключу.
Функция createState
— позволяет создать State из initialState
с переопределением/добавлением свойств.
Исходники
Все исходники находятся на github, в репозитории https://github.com/Fafnur/medium-stories
Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий tag — translation-state-tests
.
git checkout translation-state-tests
Предыдущие статьи:
- Статья о настройке Prettier, tslint и eslint в Angular
- Статья про LocalStorage, SessionStorage в Angular Universal
- Статья про мультиязычность в Angular & Universal с помощью ngx-translate
- Статья про Redux в Angular с помощью Ngrx. Создание Store в Angular
- Статья про Организацию Stat’ов в Angular c Ngrx и Nx