Преглед изворни кода

Added reducerTester, reducer tests and tests

Hugo Häggmark пре 6 година
родитељ
комит
6a84a85a80

+ 137 - 0
public/app/features/datasources/state/reducers.test.ts

@@ -0,0 +1,137 @@
+import { reducerTester } from 'test/core/redux/reducerTester';
+import { dataSourcesReducer, initialState } from './reducers';
+import {
+  dataSourcesLoaded,
+  dataSourceLoaded,
+  setDataSourcesSearchQuery,
+  setDataSourcesLayoutMode,
+  dataSourceTypesLoad,
+  dataSourceTypesLoaded,
+  setDataSourceTypeSearchQuery,
+  dataSourceMetaLoaded,
+  setDataSourceName,
+  setIsDefault,
+} from './actions';
+import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks';
+import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
+import { DataSourcesState } from 'app/types';
+import { PluginMetaInfo } from '@grafana/ui';
+
+const mockPlugin = () => ({
+  defaultNavUrl: 'defaultNavUrl',
+  enabled: true,
+  hasUpdate: true,
+  id: 'id',
+  info: {} as PluginMetaInfo,
+  latestVersion: 'latestVersion',
+  name: 'name',
+  pinned: true,
+  state: 'state',
+  type: 'type',
+  module: {},
+});
+
+describe('dataSourcesReducer', () => {
+  describe('when dataSourcesLoaded is dispatched', () => {
+    it('then state should be correct', () => {
+      const dataSources = getMockDataSources(0);
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(dataSourcesLoaded(dataSources))
+        .thenStateShouldEqual({ ...initialState, hasFetched: true, dataSources, dataSourcesCount: 1 });
+    });
+  });
+
+  describe('when dataSourceLoaded is dispatched', () => {
+    it('then state should be correct', () => {
+      const dataSource = getMockDataSource();
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(dataSourceLoaded(dataSource))
+        .thenStateShouldEqual({ ...initialState, dataSource });
+    });
+  });
+
+  describe('when setDataSourcesSearchQuery is dispatched', () => {
+    it('then state should be correct', () => {
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(setDataSourcesSearchQuery('some query'))
+        .thenStateShouldEqual({ ...initialState, searchQuery: 'some query' });
+    });
+  });
+
+  describe('when setDataSourcesLayoutMode is dispatched', () => {
+    it('then state should be correct', () => {
+      const layoutMode: LayoutModes = LayoutModes.Grid;
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(setDataSourcesLayoutMode(layoutMode))
+        .thenStateShouldEqual({ ...initialState, layoutMode: LayoutModes.Grid });
+    });
+  });
+
+  describe('when dataSourceTypesLoad is dispatched', () => {
+    it('then state should be correct', () => {
+      const state: DataSourcesState = { ...initialState, dataSourceTypes: [mockPlugin()] };
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, state)
+        .whenActionIsDispatched(dataSourceTypesLoad())
+        .thenStateShouldEqual({ ...initialState, dataSourceTypes: [], isLoadingDataSources: true });
+    });
+  });
+
+  describe('when dataSourceTypesLoaded is dispatched', () => {
+    it('then state should be correct', () => {
+      const dataSourceTypes = [mockPlugin()];
+      const state: DataSourcesState = { ...initialState, isLoadingDataSources: true };
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, state)
+        .whenActionIsDispatched(dataSourceTypesLoaded(dataSourceTypes))
+        .thenStateShouldEqual({ ...initialState, dataSourceTypes, isLoadingDataSources: false });
+    });
+  });
+
+  describe('when setDataSourceTypeSearchQuery is dispatched', () => {
+    it('then state should be correct', () => {
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(setDataSourceTypeSearchQuery('type search query'))
+        .thenStateShouldEqual({ ...initialState, dataSourceTypeSearchQuery: 'type search query' });
+    });
+  });
+
+  describe('when dataSourceMetaLoaded is dispatched', () => {
+    it('then state should be correct', () => {
+      const dataSourceMeta = mockPlugin();
+
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(dataSourceMetaLoaded(dataSourceMeta))
+        .thenStateShouldEqual({ ...initialState, dataSourceMeta });
+    });
+  });
+
+  describe('when setDataSourceName is dispatched', () => {
+    it('then state should be correct', () => {
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(setDataSourceName('some name'))
+        .thenStateShouldEqual({ ...initialState, dataSource: { name: 'some name' } });
+    });
+  });
+
+  describe('when setIsDefault is dispatched', () => {
+    it('then state should be correct', () => {
+      reducerTester()
+        .givenReducer(dataSourcesReducer, initialState)
+        .whenActionIsDispatched(setIsDefault(true))
+        .thenStateShouldEqual({ ...initialState, dataSource: { isDefault: true } });
+    });
+  });
+});

+ 1 - 1
public/app/features/datasources/state/reducers.ts

@@ -15,7 +15,7 @@ import {
 import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
 import { reducerFactory } from 'app/core/redux';
 
-const initialState: DataSourcesState = {
+export const initialState: DataSourcesState = {
   dataSources: [],
   dataSource: {} as DataSourceSettings,
   layoutMode: LayoutModes.List,

+ 58 - 0
public/test/core/redux/reducerTester.test.ts

@@ -0,0 +1,58 @@
+import { reducerFactory, actionCreatorFactory } from 'app/core/redux';
+import { reducerTester } from './reducerTester';
+
+interface DummyState {
+  data: string[];
+}
+
+const initialState: DummyState = {
+  data: [],
+};
+
+const dummyAction = actionCreatorFactory<string>('dummyAction').create();
+
+const mutatingReducer = reducerFactory(initialState)
+  .addMapper({
+    filter: dummyAction,
+    mapper: (state, action) => {
+      state.data.push(action.payload);
+      return state;
+    },
+  })
+  .create();
+
+const okReducer = reducerFactory(initialState)
+  .addMapper({
+    filter: dummyAction,
+    mapper: (state, action) => {
+      return {
+        ...state,
+        data: state.data.concat(action.payload),
+      };
+    },
+  })
+  .create();
+
+describe('reducerTester', () => {
+  describe('when reducer mutates state', () => {
+    it('then it should throw', () => {
+      expect(() => {
+        reducerTester()
+          .givenReducer(mutatingReducer, initialState)
+          .whenActionIsDispatched(dummyAction('some string'))
+          .thenStateShouldEqual({ ...initialState, data: ['some string'] });
+      }).toThrow();
+    });
+  });
+
+  describe('when reducer does not mutate state', () => {
+    it('then it should not throw', () => {
+      expect(() => {
+        reducerTester()
+          .givenReducer(okReducer, initialState)
+          .whenActionIsDispatched(dummyAction('some string'))
+          .thenStateShouldEqual({ ...initialState, data: ['some string'] });
+      }).not.toThrow();
+    });
+  });
+});

+ 79 - 0
public/test/core/redux/reducerTester.ts

@@ -0,0 +1,79 @@
+import { Reducer } from 'redux';
+
+import { ActionOf } from 'app/core/redux/actionCreatorFactory';
+
+export interface Given<State> {
+  givenReducer: (reducer: Reducer<State, ActionOf<any>>, state: State) => When<State>;
+}
+
+export interface When<State> {
+  whenActionIsDispatched: (action: ActionOf<any>) => Then<State>;
+}
+
+export interface Then<State> {
+  thenStateShouldEqual: (state: State) => Then<State>;
+}
+
+interface ObjectType extends Object {
+  [key: string]: any;
+}
+
+const deepFreeze = <T>(obj: T): T => {
+  Object.freeze(obj);
+
+  const isNotException = (object: any, propertyName: any) =>
+    typeof object === 'function'
+      ? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments'
+      : true;
+  const hasOwnProp = Object.prototype.hasOwnProperty;
+
+  if (obj && obj instanceof Object) {
+    const object: ObjectType = obj;
+    Object.getOwnPropertyNames(object).forEach(propertyName => {
+      const objectProperty: any = object[propertyName];
+      if (
+        hasOwnProp.call(object, propertyName) &&
+        isNotException(object, propertyName) &&
+        objectProperty &&
+        (typeof objectProperty === 'object' || typeof objectProperty === 'function') &&
+        Object.isFrozen(objectProperty) === false
+      ) {
+        deepFreeze(objectProperty);
+      }
+    });
+  }
+
+  return obj;
+};
+
+interface ReducerTester<State> extends Given<State>, When<State>, Then<State> {}
+
+export const reducerTester = <State>(): Given<State> => {
+  let reducerUnderTest: Reducer<State, ActionOf<any>> = null;
+  let resultingState: State = null;
+  let initialState: State = null;
+
+  const givenReducer = (reducer: Reducer<State, ActionOf<any>>, state: State): When<State> => {
+    reducerUnderTest = reducer;
+    initialState = { ...state };
+    initialState = deepFreeze(initialState);
+
+    return instance;
+  };
+
+  const whenActionIsDispatched = (action: ActionOf<any>): Then<State> => {
+    resultingState = reducerUnderTest(initialState, action);
+
+    return instance;
+  };
+
+  const thenStateShouldEqual = (state: State): Then<State> => {
+    expect(state).toEqual(resultingState);
+
+    return instance;
+  };
+
+  const instance: ReducerTester<State> = { thenStateShouldEqual, givenReducer, whenActionIsDispatched };
+
+  return instance;
+};