Testing Actions, Reducers, & Contexts in React - javascript

I've built multiple React functional components using Hooks and Context. Everything works fine. Now I need to write tests for everything. I'm confused about how to move forward with some of them so wanted to reach out to the community.
Actions
Here's a sampling from one of my Actions files:
export const ADD_VEHICLE: 'ADD_VEHICLE' = 'ADD_VEHICLE';
export const UPDATE_VEHICLE: 'UPDATE_VEHICLE' = 'UPDATE_VEHICLE';
type AddVehicleAction = {type: typeof ADD_VEHICLE, isDirty: boolean};
type UpdateVehicleAction = {type: typeof UPDATE_VEHICLE, id: number, propName: string, payload: string | number};
export type VehiclesActions =
| AddVehicleAction
| UpdateVehicleAction;
How am I supposed to test this Actions file? I don't mean in conjunction with anything else, I mean it and only it?
From the comments, it appears I have agreement that there's nothing to test DIRECTLY in this file.
Reducers
Each of my Reducers files is directly connected to and supports a specific Context. Here's a sampling of one of my Reducers files:
import type { VehiclesState } from '../VehiclesContext';
import type { VehiclesActions } from '../actions/Vehicles';
import type { Vehicle } from '../SharedTypes';
import { ADD_VEHICLE,
UPDATE_VEHICLE
} from '../actions/Vehicles';
export const vehiclesReducer = (state: VehiclesState, action: VehiclesActions) => {
switch (action.type) {
case ADD_VEHICLE: {
const length = state.vehicles.length;
const newId = (length === 0) ? 0 : state.vehicles[length - 1].id + 1;
const newVehicle = {
id: newId,
vin: '',
license: ''
};
return {
...state,
vehicles: [...state.vehicles, newVehicle],
isDirty: action.isDirty
};
}
case UPDATE_VEHICLE: {
return {
...state,
vehicles: state.vehicles.map((vehicle: Vehicle) => {
if (vehicle.id === action.id) {
return {
...vehicle,
[action.propName]: action.payload
};
} else {
return vehicle;
}
}),
isDirty: true
};
}
If you wanted to build tests for JUST this Reducers file, what approach would you use? My thought was to render the DOM like this:
function CustomComponent() {
const vehiclesState = useVehiclesState();
const { isDirty,
companyId,
vehicles
} = vehiclesState;
const dispatch = useVehiclesDispatch();
return null;
}
function renderDom() {
return {
...render(
<VehiclesProvider>
<CustomComponent />
</VehiclesProvider>
)
};
}
While this code above does run, I now have the problem that both vehiclesState and dispatch are not accessible within my test code so I'm trying to figure out how to "surface" those within each describe / it construct. Any suggestions would be appreciated.
Contexts
My Contexts follow the same pattern outlined by Kent C. Dodds: https://kentcdodds.com/blog/how-to-use-react-context-effectively - in that the StateContext & DispatchContext are separated, and there's a default state. Given this code pattern and given that I'll already have a separate test file for the Context's Reducers, what specifically could one test for ONLY for the Context?

Same as my comment, I really think you should read redux docs for writing tests so that you get a general idea of what to do.
But since you already have a reducer, you want to write your test case to follow this pattern
you will have at least 1 test per action
each test will have a a "previous state", which will be altered
you will call your reducer, passing the action and the previous state
you will assert your new state is the same as expected
Here's a code example:
it('adds a new car when there are no cars yet', () => {
// you want to put here values that WILL change, so that you don't risk
// a false positive in your unit test
const previousState = {
vehicles: [],
isDirty: false,
};
const state = reducer(previousState, { type: ADD_VEHICLE });
expect(state).toEqual({
vehicles: [{
id: 1,
vin: '',
license: '',
}],
isDirty: true,
});
});
it('adds a new car when there are existing cars already, () => {
// ...
});
I'd also recommend to use action creators rather than directly creating action objects, since it's more readable:
// actions.js
export const addVehicle = () => ({
type: ADD_VEHICLE
})
// reducer.test.js
it('adds a new car when there are no cars yet', () => {
//...
const state = reducer(previousState, actions.addVehicle());

Related

Typescript reducer's switch case typeguard doesn't work with object spread

I have a reducer that does different actions depending on the action.type, actions payload is different for certain actions.
export enum ActionType {
UpdateEntireState = "UPDATE_ENTIRE_STATE",
UpdateStateItem = "UPDATE_STATE_ITEM"
}
type TypeEditData = {
id: string;
name: string;
surname: string;
age: number;
};
export type State = TypeEditData[];
export type Action = UpdateEntireState | UpdateStateItem;
type UpdateEntireState = {
type: ActionType.UpdateEntireState;
payload: State;
};
type UpdateStateItem = {
type: ActionType.UpdateStateItem;
payload: { id: string; data: TypeEditData };
};
export function reducer(state: State, action: Action): State {
const { type, payload } = action;
switch (type) {
case ActionType.UpdateEntireState: {
return [...payload];
}
case ActionType.UpdateStateItem: {
const person = state.filter((item) => item.id === payload.id);
return [...state, person[0]];
}
default: {
throw Error("Wrong type of action!");
}
}
}
This code won't work, the errors will say that my action payload can be State or { id: string; data: TypeEditData }.
However, if I access the payload property inside switch case using dot notation like so
return [...action.payload];
There won't be any errors and the type guard will work fine.
How const { type, payload } = action; differs from action.type and action.payload in terms of types and why doesn't typeguard work with spread syntax?
TS version - 4.3.4
The issue is that you've defined payload before there was type information available on action, so it has the union type
State | {
id: string;
data: TypeEditData;
};
Define a local variable or simply use action.payload within each case statement and the compiler knows what type it has:
export function reducer(state: State, action: Action): State {
// const { type, payload } = action;
switch (action.type) {
case ActionType.UpdateEntireState: {
return [...action.payload];
}
case ActionType.UpdateStateItem: {
const person = state.filter((item) => item.id === action.payload.id);
return [...state, person[0]];
}
default: {
throw Error("Wrong type of action!");
}
}
}
Variable type is established explicitly at declaration (e.g. const a: string) or implicitly at initialization (e.g. a = 4). Subsequent typeguard constructs are not used to re-evaluate the type of the variable. On the contrary, since the type of the variable is already defined at that point, that type is used to validate whether the later construct is valid for the variable.
Action interface by default comes with type property.
export interface Action {
type: string;
}
If you can extend Action interface to add payload as array of objects then typescript wont throw you an error.
Something like this, in your reducer function you can use like this
interface CustomAction extends Action{
payload: Array<any>
}
export function reducer(state: State, action: CustomAction): State {

Access property on dynamic TypeScript Interface

I'm currently in the process of migrating a nuxt application with Vuex from JS to TS. I'm starting with one of ou Vuex modules. The thing is that we use a library we wrote at our company and that is used in other projects (can't touch it or migrate it to TS...) to generate some base state, getters, setters, actions and mutations for each module given a certain entity. This library is written is JS with no types available so I'm working on some shims to get some typings in my project. Let me walk you through the whole workflow before I explain my problem.
The library exports a createEntity function that takes a String and returns an object that looks like this:
{
path, //The name of the module
mutationPrefix, //path.uppercase()
actionPrefix, //path
singleSchema, //some normalizr stuff
multipleSchema, //some normalizr stuff
storeKey: 'entities', //the default name for the key in the store
}
We then use the Entity that has been returned to generate the baseState/baseMutations/baseGetters/baseActions of this store module (these are basic CRUD operations on a given entity).
For the module I'm working on (the first I'm migrating to TS), the state.ts file looks like this
import { createState } from '#kissmylabs/vuex-entitystore'
import { Provider } from '~/types/models/Provider'
import { provider } from './entities' // The entity created by our createEntity() library function
export const baseCreateForm = (): Provider => ({
username: '',
email: '',
password: '',
// ... some other form fields
})
export const getBaseState = () => ({
...createState(provider), // The state created by our createState() library function
pagination: {
total: 0,
},
createForm: baseCreateForm(),
isImpersonating: false,
})
export default getBaseState
So I'm trying to implement types for my state, using some TS Interfaces. My interface BaseState (the return type of my createState() library function) looks like this
interface BaseState {
storeKey: {
byId: any,
allIds: Array<number>,
active: Array<any>,
deleted: Array<any>,
isFetching: boolean,
errors: Array<any>,
}
}
This is working mostly fine, my types are working, except for the main storeKey part. This is because the createState function takes the entity.storeKey as the key for the returned state, like so:
export const createState = (entity) => {
return {
[`${entity.storeKey}`]: {
byId: {},
allIds: [],
active: [],
deleted: [],
isFetching: false,
errors: [],
},
}
}
So the whole state generation thing works fine, types are ok and everything, the problem comes when I'm trying to reference my state in a mutation for example. In my mutations.ts I want to be able to do something like
export const mutations = {
...createMutations(provider), // My base mutations, typed fine
ADD_PROVIDER_FILE(state: State, { id, file }: { id: number, file: any }) {
state.entities.byId[id].files.push(file) // The problem is here "Property entity doesn't exist"
},
// More mutations...
}
export default mutations
TS does not recognize the fact that my entities key on the state object can be pretty much anything and is generated dynamically by my createState function, depending on a given entity's storeKey attribute.
So my question is: Is there a way to declare an interface for my BaseState that declares storeKey as an object with any name but still containing the right attributes and types inside ?
What I've tried so far :
[key: string] instead of storeKey as my key in BaseState
Extracted the content of the storeKey object in a new interface and doing something like :
interface BaseStateKeys {
byId: any,
allIds: Array<number>,
active: Array<any>,
deleted: Array<any>,
isFetching: boolean,
errors: Array<any>,
}
interface BaseState {
[key: string]: BaseStateKeys
}
None of this worked.
Anything clever I'm missing ? Thanks for help !

Ngrx : Cannot assign to read only property 'Property' of object '[Object]'

I'm using ngrx store.
In my state I have to items
export interface ISchedulesState {
schedulings: ISchedules;
actualTrips: ISchedule[];
}
Here are my interfaces
export interface ISchedules {
[key: string]: ISchedule[];
}
export interface ISchedule {
dest: number;
data: string
}
In reducer I update actualTrips
export const SchedulingReducers = (
state = initialSchedulingState,
action: SchedulesAction
): ISchedulesState => {
switch (action.type) {
case ESchedulesActions.GetSchedulesByDate: {
return {
...state
};
}
case ESchedulesActions.GetSchedulesByDateSuccess: {
return {
...state,
schedulings: action.payload
};
}
case ESchedulesActions.GetSchedulesByTime: {
let time = action.payload;
state.actualTrips = [...(state.schedulings[time] || [])]; // if not data return empty array
return state;
}
default:
return state;
}
};
But actually I get an error
ERROR TypeError: Cannot assign to read only property 'actualTrips' of object '[object Object]'
The basic principle of Redux pattern is immutability of state and its parts, because it let's us to detect changes just by object reference instead of comparing whole objects.
In your reducer, you cannot directly assign a property of state (state.actualTrips =), because change detector (and selectors) would not detect it as changed.
To modify state, you return a copy of the state with new modifications.
const time = action.payload;
return {
...state,
actualTrips: [...(state.schedulings[time] || [])]
}
If you want to change state.actualTrips = myNewValue is not allowed because there is a strict Setting. So one way is may to clonedeep and return the object, like newState = cloneOfState... I didn't test it. So I changed the setting in app.module for Store.
My Example: change the strictStateImmutability to false (full Docu here: https://ngrx.io/guide/store/configuration/runtime-checks )
StoreModule.forRoot(ROOT_REDUCERS_TOKEN, {
metaReducers,
runtimeChecks: {
// strictStateImmutability and strictActionImmutability are enabled by default
strictStateSerializability: true,
strictActionSerializability: true,
strictActionWithinNgZone: true,
strictActionTypeUniqueness: true,
// if you want to change complexe objects and that we have. We need to disable these settings
// change strictStateImmutability, strictActionImmutability
strictStateImmutability: false, // set this to false
strictActionImmutability: true,
},
}),
That error happened me when I changed the input values in the template. I was using Angular11 + NGRX11 so I understood I was changed a value from store, this was my fix:
Before:
this.store.dispatch(new Actions.LoginUser({ user: this.user }));
After:
const clone = {
user: Object.assign({}, this.user)
};
this.store.dispatch(new Actions.LoginUser(clone));
I found the answer at https://stackoverflow.com/a/58279719
Simply copy the state into a new object
const oldState = getState();
let state = JSON.parse(JSON.stringify(oldState)); // here
state.propertyToChange = "newValue";
patchState({
...state
});

Typescript + immutability-helper #set

I've come to join the typescript wagon and i wantede to keep using this immutability-helper lib when im updating my redux store, but for some reason now i get this error when im trying to execute a update?:
[ts] Argument of type '{ flags: { hideChat: { $set: boolean; }; }; }'
is not assignable to parameter of type 'Spec'.
Object literal may only specify known properties, and 'flags' does not
exist in type 'Spec'.
export interface GlobalStateInit {
flags: {
hideChat: boolean
}
}
const initialState: GlobalStateInit = {
flags: {
hideChat: false
}
}
const reducer: Reducer<GlobalStateInit, GlobalAction> = (state = initialState, action) => {
switch (action.type) {
case getType(actions.toggleChat):
return update(state, {
flags: {
hideChat: { $set: !state.flags.hideChat }
}
})
default:
return state
}
}
export { reducer as GlobalReducer }
I was asuming this should be trivial and should just work out of the box, my jest test running in the background can figure this out but the VScode TS Linter gets a bit angry.
Not sure if this is a bug or if its just VScode that messes up.
This is a deliberate compiler feature. Here's a short version of the same problem:
interface Example {
name: string,
val: number
}
const example: Example = {
name: 'Fenton',
val: 1,
more: 'example'
}
The more property isn't part of the Example interface, so the compiler is going to warn you that you might have made a mistake. For example, if there is an optional member that you typed wrong, it will catch that for you:
interface Example {
name: string,
val: number,
side?: string
}
const example: Example = {
name: 'Fenton',
val: 1,
sdie: 'Left' // Oops, I meant "side"
}
In cases where you think you know better than the compiler, you can tell it that you're in charge:
interface Example {
name: string,
val: number
}
const example = {
name: 'Fenton',
val: 1,
more: 'example'
} as Example
This allows additional members, while still ensuring you don't make a mistake such as:
// Lots of errors!
const example = {
more: 'example'
} as Example

How do you define Flow types for Redux Actions?

I'm working through methods of typing Redux actions with Flow. Using the following definitions it is partially working in both the action creator and the reducer. Autocomplete picks up all 4 actions as possible values for action.type in both locations.
// #flow
type ModuleActionTypes1 = 'moduleOne/ACTION_ONE' | 'moduleOne/ACTION_TWO';
type ModuleActionTypes2 = 'moduleTwo/ACTION_ONE' | 'moduleTwo/ACTION_TWO';
type ActionTypes = ModuleActionTypes1 | ModuleActionTypes2;
type Action<T> = {
type: ActionTypes,
payload: T
};
type Payload = {
url: string
};
const actionCreator = (url: string): Action<Payload> => {
return {
type: 'moduleOne/ACTION_ONE',
payload: {
url: url
}
};
};
const reducer = (state: string, action: Action<Payload>): string => {
if(action.type === 'wrong') {
// action.type cannot be this
}
switch (action.type) {
case 'moduleOne/ACTION_ONE':
case 'wrong': // action type cannot be this
const { url } = action.payload;
return url;
default:
return state;
}
};
It almost does what I want. There are two main issues:
The switch cases don't have to be one of action.type, so typos could slip through. This suggests I should stick with string constants.
When I import type { ModuleActionTypes1 } from '../moduleOne'; (instead of defining it in the same file for testing) autocomplete stops working but flow reports no errors on the command line. All the files contain the #flow comment.
Is there a better way to do this?
So, it's a little different from your question, but let me suggest doing this a little differently. Right now, your action type says that any payload can belong to any action. Flow can be a lot more helpful if you use your types to define only allowable states.
I don't know what your actions do, so here is a random example:
type CreatePerson = { type: 'create-person', person: Person };
type DestroyPerson = { type: 'destroy-person', personId: number };
type ChangePersonName = { type: 'change-name', personId: number, name: string }
type PersonActions = CreatePerson | DestroyPerson | ChangePersonName
Setting it up this way helps ensure that each Redux action has the right data in the right types. In your reducer, you can still do a switch/if on action.type to pick which case to use. But now, by picking out the type by string, Flow also knows exactly what type of data exists on the action. This can let you avoid casting or unsafe destructuring that Flow can't type check.

Categories