+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 149 of 354

πŸͺ Testing Redux: Store and Actions

Master comprehensive Redux testing in TypeScript with store testing, action creators, reducers, middleware, and async thunks πŸš€

πŸš€Intermediate
27 min read

Prerequisites

  • Understanding of Redux fundamentals and TypeScript πŸ“
  • Knowledge of Jest testing framework and React Testing Library ⚑
  • Familiarity with Redux Toolkit and async patterns πŸ’»

What you'll learn

  • Test Redux stores, actions, and reducers with comprehensive coverage 🎯
  • Master testing async thunks, middleware, and complex state transitions πŸ—οΈ
  • Handle Redux integration testing with React components πŸ›
  • Create maintainable test suites for Redux applications ✨

🎯 Introduction

Welcome to the Redux testing command center! πŸͺ If testing regular state management were like checking a simple light switch, then testing Redux would be like testing an entire electrical control system - complete with power distribution (store), control panels (actions), circuit breakers (reducers), and automated systems (middleware) that all need to work together flawlessly to power your entire application!

Redux applications involve complex state management with multiple moving parts: action creators, reducers, middleware, async operations, and store subscriptions. Testing these components requires understanding both the individual pieces and how they integrate together to form a cohesive state management system.

By the end of this tutorial, you’ll be a master of Redux testing, capable of thoroughly testing everything from simple synchronous actions to complex async thunks with error handling, retry logic, and optimistic updates. You’ll learn to test Redux in isolation and integration, ensuring your state management is bulletproof. Let’s build some rock-solid Redux tests! 🌟

πŸ“š Understanding Redux Testing Fundamentals

πŸ€” Why Redux Testing Is Essential

Redux testing ensures predictable state management, proper action handling, and reliable data flow throughout your application.

// 🌟 Setting up comprehensive Redux testing environment

import { configureStore, createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import React from 'react';

// Types for our Redux examples
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  isActive: boolean;
  createdAt: string;
}

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate?: string;
  assignedTo?: string;
}

interface ApiError {
  message: string;
  code: number;
  field?: string;
}

interface PaginationMeta {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
}

// Auth slice for user authentication
interface AuthState {
  user: User | null;
  token: string | null;
  loading: boolean;
  error: string | null;
  loginAttempts: number;
  lastLoginAttempt: string | null;
}

const initialAuthState: AuthState = {
  user: null,
  token: null,
  loading: false,
  error: null,
  loginAttempts: 0,
  lastLoginAttempt: null
};

// Async thunk for login
export const loginUser = createAsyncThunk<
  { user: User; token: string },
  { email: string; password: string },
  { rejectValue: ApiError }
>(
  'auth/loginUser',
  async (credentials, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(credentials)
      });

      if (!response.ok) {
        const errorData = await response.json();
        return rejectWithValue(errorData);
      }

      const data = await response.json();
      
      // Store token in localStorage
      localStorage.setItem('token', data.token);
      
      return data;
    } catch (error) {
      return rejectWithValue({
        message: 'Network error occurred',
        code: 0
      });
    }
  }
);

// Async thunk for logout
export const logoutUser = createAsyncThunk<
  void,
  void,
  { rejectValue: ApiError }
>(
  'auth/logoutUser',
  async (_, { rejectWithValue }) => {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
      });

      localStorage.removeItem('token');
    } catch (error) {
      return rejectWithValue({
        message: 'Logout failed',
        code: 500
      });
    }
  }
);

const authSlice = createSlice({
  name: 'auth',
  initialState: initialAuthState,
  reducers: {
    clearError: (state) => {
      state.error = null;
    },
    resetLoginAttempts: (state) => {
      state.loginAttempts = 0;
      state.lastLoginAttempt = null;
    },
    updateUserProfile: (state, action: PayloadAction<Partial<User>>) => {
      if (state.user) {
        state.user = { ...state.user, ...action.payload };
      }
    },
    setToken: (state, action: PayloadAction<string>) => {
      state.token = action.payload;
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload.user;
        state.token = action.payload.token;
        state.error = null;
        state.loginAttempts = 0;
        state.lastLoginAttempt = null;
      })
      .addCase(loginUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload?.message || 'Login failed';
        state.loginAttempts += 1;
        state.lastLoginAttempt = new Date().toISOString();
      })
      .addCase(logoutUser.pending, (state) => {
        state.loading = true;
      })
      .addCase(logoutUser.fulfilled, (state) => {
        state.loading = false;
        state.user = null;
        state.token = null;
        state.error = null;
        state.loginAttempts = 0;
        state.lastLoginAttempt = null;
      })
      .addCase(logoutUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload?.message || 'Logout failed';
      });
  }
});

export const { clearError, resetLoginAttempts, updateUserProfile, setToken } = authSlice.actions;
export const authReducer = authSlice.reducer;

// Todos slice for task management
interface TodosState {
  items: Todo[];
  loading: boolean;
  error: string | null;
  filter: 'all' | 'active' | 'completed';
  sortBy: 'createdAt' | 'priority' | 'dueDate';
  searchQuery: string;
  pagination: PaginationMeta;
}

const initialTodosState: TodosState = {
  items: [],
  loading: false,
  error: null,
  filter: 'all',
  sortBy: 'createdAt',
  searchQuery: '',
  pagination: {
    page: 1,
    limit: 10,
    total: 0,
    totalPages: 0
  }
};

// Async thunk for fetching todos
export const fetchTodos = createAsyncThunk<
  { todos: Todo[]; pagination: PaginationMeta },
  { page?: number; limit?: number; filter?: string },
  { rejectValue: ApiError }
>(
  'todos/fetchTodos',
  async (params = {}, { rejectWithValue }) => {
    try {
      const { page = 1, limit = 10, filter = 'all' } = params;
      const queryParams = new URLSearchParams({
        page: page.toString(),
        limit: limit.toString(),
        filter
      });

      const response = await fetch(`/api/todos?${queryParams}`);

      if (!response.ok) {
        const errorData = await response.json();
        return rejectWithValue(errorData);
      }

      return await response.json();
    } catch (error) {
      return rejectWithValue({
        message: 'Failed to fetch todos',
        code: 500
      });
    }
  }
);

// Async thunk for creating todo
export const createTodo = createAsyncThunk<
  Todo,
  Omit<Todo, 'id'>,
  { rejectValue: ApiError }
>(
  'todos/createTodo',
  async (todoData, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/todos', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        },
        body: JSON.stringify(todoData)
      });

      if (!response.ok) {
        const errorData = await response.json();
        return rejectWithValue(errorData);
      }

      return await response.json();
    } catch (error) {
      return rejectWithValue({
        message: 'Failed to create todo',
        code: 500
      });
    }
  }
);

// Async thunk for updating todo
export const updateTodo = createAsyncThunk<
  Todo,
  { id: string; updates: Partial<Todo> },
  { rejectValue: ApiError }
>(
  'todos/updateTodo',
  async ({ id, updates }, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        },
        body: JSON.stringify(updates)
      });

      if (!response.ok) {
        const errorData = await response.json();
        return rejectWithValue(errorData);
      }

      return await response.json();
    } catch (error) {
      return rejectWithValue({
        message: 'Failed to update todo',
        code: 500
      });
    }
  }
);

// Async thunk for deleting todo
export const deleteTodo = createAsyncThunk<
  string,
  string,
  { rejectValue: ApiError }
>(
  'todos/deleteTodo',
  async (id, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
      });

      if (!response.ok) {
        const errorData = await response.json();
        return rejectWithValue(errorData);
      }

      return id;
    } catch (error) {
      return rejectWithValue({
        message: 'Failed to delete todo',
        code: 500
      });
    }
  }
);

const todosSlice = createSlice({
  name: 'todos',
  initialState: initialTodosState,
  reducers: {
    setFilter: (state, action: PayloadAction<TodosState['filter']>) => {
      state.filter = action.payload;
      state.pagination.page = 1; // Reset to first page when filtering
    },
    setSortBy: (state, action: PayloadAction<TodosState['sortBy']>) => {
      state.sortBy = action.payload;
    },
    setSearchQuery: (state, action: PayloadAction<string>) => {
      state.searchQuery = action.payload;
      state.pagination.page = 1; // Reset to first page when searching
    },
    clearError: (state) => {
      state.error = null;
    },
    toggleTodoCompleted: (state, action: PayloadAction<string>) => {
      const todo = state.items.find(item => item.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    addTodoOptimistic: (state, action: PayloadAction<Todo>) => {
      state.items.unshift(action.payload);
    },
    removeTodoOptimistic: (state, action: PayloadAction<string>) => {
      state.items = state.items.filter(item => item.id !== action.payload);
    }
  },
  extraReducers: (builder) => {
    builder
      // Fetch todos
      .addCase(fetchTodos.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload.todos;
        state.pagination = action.payload.pagination;
        state.error = null;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload?.message || 'Failed to fetch todos';
      })
      // Create todo
      .addCase(createTodo.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(createTodo.fulfilled, (state, action) => {
        state.loading = false;
        // Remove optimistic todo if it exists, add real one
        state.items = state.items.filter(item => item.id !== 'temp-id');
        state.items.unshift(action.payload);
        state.error = null;
      })
      .addCase(createTodo.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload?.message || 'Failed to create todo';
        // Remove optimistic todo
        state.items = state.items.filter(item => item.id !== 'temp-id');
      })
      // Update todo
      .addCase(updateTodo.fulfilled, (state, action) => {
        const index = state.items.findIndex(item => item.id === action.payload.id);
        if (index !== -1) {
          state.items[index] = action.payload;
        }
      })
      .addCase(updateTodo.rejected, (state, action) => {
        state.error = action.payload?.message || 'Failed to update todo';
      })
      // Delete todo
      .addCase(deleteTodo.fulfilled, (state, action) => {
        state.items = state.items.filter(item => item.id !== action.payload);
      })
      .addCase(deleteTodo.rejected, (state, action) => {
        state.error = action.payload?.message || 'Failed to delete todo';
      });
  }
});

export const {
  setFilter,
  setSortBy,
  setSearchQuery,
  clearError: clearTodosError,
  toggleTodoCompleted,
  addTodoOptimistic,
  removeTodoOptimistic
} = todosSlice.actions;

export const todosReducer = todosSlice.reducer;

// Root state type
export interface RootState {
  auth: AuthState;
  todos: TodosState;
}

// Store configuration
export const createTestStore = (preloadedState?: Partial<RootState>) => {
  return configureStore({
    reducer: {
      auth: authReducer,
      todos: todosReducer
    },
    preloadedState,
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: {
          ignoredActions: ['persist/PERSIST']
        }
      })
  });
};

// Selectors
export const selectUser = (state: RootState) => state.auth.user;
export const selectIsAuthenticated = (state: RootState) => !!state.auth.token;
export const selectAuthLoading = (state: RootState) => state.auth.loading;
export const selectAuthError = (state: RootState) => state.auth.error;

export const selectTodos = (state: RootState) => state.todos.items;
export const selectTodosLoading = (state: RootState) => state.todos.loading;
export const selectTodosError = (state: RootState) => state.todos.error;
export const selectTodosFilter = (state: RootState) => state.todos.filter;

export const selectFilteredTodos = (state: RootState) => {
  const { items, filter, searchQuery } = state.todos;
  
  let filtered = items;
  
  // Apply filter
  switch (filter) {
    case 'active':
      filtered = filtered.filter(todo => !todo.completed);
      break;
    case 'completed':
      filtered = filtered.filter(todo => todo.completed);
      break;
    default:
      // 'all' - no filtering
      break;
  }
  
  // Apply search
  if (searchQuery.trim()) {
    filtered = filtered.filter(todo =>
      todo.text.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }
  
  return filtered;
};

export const selectTodoStats = (state: RootState) => {
  const todos = state.todos.items;
  const total = todos.length;
  const completed = todos.filter(t => t.completed).length;
  const active = total - completed;
  const highPriority = todos.filter(t => t.priority === 'high').length;
  
  return { total, completed, active, highPriority };
};

// Custom middleware for logging
export const loggerMiddleware: any = (store: any) => (next: any) => (action: any) => {
  console.log('Dispatching:', action.type);
  const result = next(action);
  console.log('Next state:', store.getState());
  return result;
};

// Test utilities
export const createMockStore = (initialState: Partial<RootState> = {}) => {
  return createTestStore({
    auth: { ...initialAuthState, ...initialState.auth },
    todos: { ...initialTodosState, ...initialState.todos }
  });
};

export const renderWithRedux = (
  component: React.ReactElement,
  { store = createMockStore(), ...renderOptions } = {}
) => {
  const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
    <Provider store={store}>{children}</Provider>
  );
  
  return {
    ...render(component, { wrapper: Wrapper, ...renderOptions }),
    store
  };
};

βœ… Testing Actions and Action Creators

🎬 Testing Synchronous Actions

Basic action testing verifies that actions are created correctly with proper types and payloads.

// 🌟 Comprehensive action creator testing

describe('Auth Actions', () => {
  // βœ… Testing synchronous action creators
  describe('Synchronous Actions', () => {
    it('should create clearError action', () => {
      const action = clearError();
      
      expect(action).toEqual({
        type: 'auth/clearError',
        payload: undefined
      });
    });

    it('should create resetLoginAttempts action', () => {
      const action = resetLoginAttempts();
      
      expect(action).toEqual({
        type: 'auth/resetLoginAttempts',
        payload: undefined
      });
    });

    it('should create updateUserProfile action', () => {
      const userUpdate = { name: 'Updated Name', email: '[email protected]' };
      const action = updateUserProfile(userUpdate);
      
      expect(action).toEqual({
        type: 'auth/updateUserProfile',
        payload: userUpdate
      });
    });

    it('should create setToken action', () => {
      const token = 'abc123token';
      const action = setToken(token);
      
      expect(action).toEqual({
        type: 'auth/setToken',
        payload: token
      });
    });
  });

  // βœ… Testing action creators with complex payloads
  describe('Complex Payload Actions', () => {
    it('should handle partial user profile updates', () => {
      const partialUpdate = { name: 'New Name' };
      const action = updateUserProfile(partialUpdate);
      
      expect(action.payload).toEqual(partialUpdate);
      expect(action.type).toBe('auth/updateUserProfile');
    });

    it('should handle empty user profile updates', () => {
      const emptyUpdate = {};
      const action = updateUserProfile(emptyUpdate);
      
      expect(action.payload).toEqual(emptyUpdate);
      expect(action.type).toBe('auth/updateUserProfile');
    });

    it('should handle user profile updates with all fields', () => {
      const fullUpdate: Partial<User> = {
        id: 'user-123',
        name: 'Full Name',
        email: '[email protected]',
        role: 'admin',
        isActive: false,
        createdAt: '2023-01-01T00:00:00Z'
      };
      
      const action = updateUserProfile(fullUpdate);
      
      expect(action.payload).toEqual(fullUpdate);
    });
  });
});

describe('Todos Actions', () => {
  // βœ… Testing todos synchronous actions
  describe('Filter and Search Actions', () => {
    it('should create setFilter action', () => {
      const filter = 'completed';
      const action = setFilter(filter);
      
      expect(action).toEqual({
        type: 'todos/setFilter',
        payload: filter
      });
    });

    it('should create setSortBy action', () => {
      const sortBy = 'priority';
      const action = setSortBy(sortBy);
      
      expect(action).toEqual({
        type: 'todos/setSortBy',
        payload: sortBy
      });
    });

    it('should create setSearchQuery action', () => {
      const searchQuery = 'important task';
      const action = setSearchQuery(searchQuery);
      
      expect(action).toEqual({
        type: 'todos/setSearchQuery',
        payload: searchQuery
      });
    });

    it('should handle all filter types', () => {
      const filters: Array<TodosState['filter']> = ['all', 'active', 'completed'];
      
      filters.forEach(filter => {
        const action = setFilter(filter);
        expect(action.payload).toBe(filter);
        expect(action.type).toBe('todos/setFilter');
      });
    });

    it('should handle all sort types', () => {
      const sortOptions: Array<TodosState['sortBy']> = ['createdAt', 'priority', 'dueDate'];
      
      sortOptions.forEach(sortBy => {
        const action = setSortBy(sortBy);
        expect(action.payload).toBe(sortBy);
        expect(action.type).toBe('todos/setSortBy');
      });
    });
  });

  // βœ… Testing optimistic update actions
  describe('Optimistic Update Actions', () => {
    it('should create addTodoOptimistic action', () => {
      const todo: Todo = {
        id: 'temp-123',
        text: 'Optimistic todo',
        completed: false,
        priority: 'medium'
      };
      
      const action = addTodoOptimistic(todo);
      
      expect(action).toEqual({
        type: 'todos/addTodoOptimistic',
        payload: todo
      });
    });

    it('should create removeTodoOptimistic action', () => {
      const todoId = 'temp-123';
      const action = removeTodoOptimistic(todoId);
      
      expect(action).toEqual({
        type: 'todos/removeTodoOptimistic',
        payload: todoId
      });
    });

    it('should create toggleTodoCompleted action', () => {
      const todoId = 'todo-456';
      const action = toggleTodoCompleted(todoId);
      
      expect(action).toEqual({
        type: 'todos/toggleTodoCompleted',
        payload: todoId
      });
    });
  });
});

// βœ… Testing action types and payloads validation
describe('Action Type Safety', () => {
  it('should have correct action types for auth actions', () => {
    expect(clearError().type).toBe('auth/clearError');
    expect(resetLoginAttempts().type).toBe('auth/resetLoginAttempts');
    expect(updateUserProfile({}).type).toBe('auth/updateUserProfile');
    expect(setToken('').type).toBe('auth/setToken');
  });

  it('should have correct action types for todos actions', () => {
    expect(setFilter('all').type).toBe('todos/setFilter');
    expect(setSortBy('createdAt').type).toBe('todos/setSortBy');
    expect(setSearchQuery('').type).toBe('todos/setSearchQuery');
    expect(clearTodosError().type).toBe('todos/clearError');
    expect(toggleTodoCompleted('').type).toBe('todos/toggleTodoCompleted');
    expect(addTodoOptimistic({} as Todo).type).toBe('todos/addTodoOptimistic');
    expect(removeTodoOptimistic('').type).toBe('todos/removeTodoOptimistic');
  });

  it('should preserve payload types', () => {
    const userUpdate = { name: 'Test User' };
    const action = updateUserProfile(userUpdate);
    
    // TypeScript should ensure these types match
    expect(action.payload.name).toBe('Test User');
    expect(typeof action.payload.name).toBe('string');
  });
});

πŸ”„ Testing Async Thunks

⚑ Testing Async Action Creators

Async thunks require mocking external dependencies and testing various success/failure scenarios.

// 🌟 Comprehensive async thunk testing

describe('loginUser Async Thunk', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
    localStorage.clear();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  // βœ… Testing successful login
  it('should handle successful login', async () => {
    const mockResponse = {
      user: {
        id: 'user-123',
        name: 'John Doe',
        email: '[email protected]',
        role: 'user' as const,
        isActive: true,
        createdAt: '2023-01-01T00:00:00Z'
      },
      token: 'abc123token'
    };

    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockResponse)
    } as Response);

    const credentials = { email: '[email protected]', password: 'password123' };
    
    const dispatch = jest.fn();
    const getState = jest.fn();
    const extra = {};

    const thunk = loginUser(credentials);
    const result = await thunk(dispatch, getState, extra);

    expect(result.type).toBe('auth/loginUser/fulfilled');
    expect(result.payload).toEqual(mockResponse);
    expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(credentials)
    });
    expect(localStorage.getItem('token')).toBe('abc123token');
  });

  // βœ… Testing login failure with API error
  it('should handle login failure with API error', async () => {
    const errorResponse = {
      message: 'Invalid credentials',
      code: 401,
      field: 'email'
    };

    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: false,
      json: () => Promise.resolve(errorResponse)
    } as Response);

    const credentials = { email: '[email protected]', password: 'wrongpassword' };
    
    const dispatch = jest.fn();
    const getState = jest.fn();
    const extra = {};

    const thunk = loginUser(credentials);
    const result = await thunk(dispatch, getState, extra);

    expect(result.type).toBe('auth/loginUser/rejected');
    expect(result.payload).toEqual(errorResponse);
    expect(localStorage.getItem('token')).toBeNull();
  });

  // βœ… Testing network error
  it('should handle network error', async () => {
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockRejectedValue(new Error('Network error'));

    const credentials = { email: '[email protected]', password: 'password123' };
    
    const dispatch = jest.fn();
    const getState = jest.fn();
    const extra = {};

    const thunk = loginUser(credentials);
    const result = await thunk(dispatch, getState, extra);

    expect(result.type).toBe('auth/loginUser/rejected');
    expect(result.payload).toEqual({
      message: 'Network error occurred',
      code: 0
    });
  });
});

describe('logoutUser Async Thunk', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
    localStorage.setItem('token', 'existing-token');
  });

  afterEach(() => {
    jest.resetAllMocks();
    localStorage.clear();
  });

  it('should handle successful logout', async () => {
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: true
    } as Response);

    const dispatch = jest.fn();
    const getState = jest.fn();
    const extra = {};

    const thunk = logoutUser();
    const result = await thunk(dispatch, getState, extra);

    expect(result.type).toBe('auth/logoutUser/fulfilled');
    expect(result.payload).toBeUndefined();
    expect(mockFetch).toHaveBeenCalledWith('/api/auth/logout', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer existing-token'
      }
    });
    expect(localStorage.getItem('token')).toBeNull();
  });

  it('should handle logout failure', async () => {
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockRejectedValue(new Error('Server error'));

    const dispatch = jest.fn();
    const getState = jest.fn();
    const extra = {};

    const thunk = logoutUser();
    const result = await thunk(dispatch, getState, extra);

    expect(result.type).toBe('auth/logoutUser/rejected');
    expect(result.payload).toEqual({
      message: 'Logout failed',
      code: 500
    });
    expect(localStorage.getItem('token')).toBeNull(); // Token should still be removed
  });
});

describe('Todos Async Thunks', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
    localStorage.setItem('token', 'test-token');
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  // βœ… Testing fetchTodos
  describe('fetchTodos', () => {
    it('should fetch todos successfully', async () => {
      const mockResponse = {
        todos: [
          {
            id: 'todo-1',
            text: 'First todo',
            completed: false,
            priority: 'high' as const
          },
          {
            id: 'todo-2',
            text: 'Second todo',
            completed: true,
            priority: 'low' as const
          }
        ],
        pagination: {
          page: 1,
          limit: 10,
          total: 2,
          totalPages: 1
        }
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockResponse)
      } as Response);

      const params = { page: 1, limit: 10, filter: 'all' };
      
      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = fetchTodos(params);
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/fetchTodos/fulfilled');
      expect(result.payload).toEqual(mockResponse);
      expect(mockFetch).toHaveBeenCalledWith('/api/todos?page=1&limit=10&filter=all');
    });

    it('should handle fetch todos error', async () => {
      const errorResponse = {
        message: 'Failed to fetch todos',
        code: 500
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: false,
        json: () => Promise.resolve(errorResponse)
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = fetchTodos();
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/fetchTodos/rejected');
      expect(result.payload).toEqual(errorResponse);
    });
  });

  // βœ… Testing createTodo
  describe('createTodo', () => {
    it('should create todo successfully', async () => {
      const newTodo = {
        text: 'New todo',
        completed: false,
        priority: 'medium' as const
      };

      const createdTodo = {
        id: 'todo-123',
        ...newTodo
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(createdTodo)
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = createTodo(newTodo);
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/createTodo/fulfilled');
      expect(result.payload).toEqual(createdTodo);
      expect(mockFetch).toHaveBeenCalledWith('/api/todos', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer test-token'
        },
        body: JSON.stringify(newTodo)
      });
    });

    it('should handle create todo validation error', async () => {
      const invalidTodo = {
        text: '',
        completed: false,
        priority: 'medium' as const
      };

      const errorResponse = {
        message: 'Text is required',
        code: 400,
        field: 'text'
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: false,
        json: () => Promise.resolve(errorResponse)
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = createTodo(invalidTodo);
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/createTodo/rejected');
      expect(result.payload).toEqual(errorResponse);
    });
  });

  // βœ… Testing updateTodo
  describe('updateTodo', () => {
    it('should update todo successfully', async () => {
      const todoId = 'todo-123';
      const updates = { text: 'Updated todo text', completed: true };
      const updatedTodo = {
        id: todoId,
        text: 'Updated todo text',
        completed: true,
        priority: 'high' as const
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(updatedTodo)
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = updateTodo({ id: todoId, updates });
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/updateTodo/fulfilled');
      expect(result.payload).toEqual(updatedTodo);
      expect(mockFetch).toHaveBeenCalledWith(`/api/todos/${todoId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer test-token'
        },
        body: JSON.stringify(updates)
      });
    });
  });

  // βœ… Testing deleteTodo
  describe('deleteTodo', () => {
    it('should delete todo successfully', async () => {
      const todoId = 'todo-123';

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = deleteTodo(todoId);
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/deleteTodo/fulfilled');
      expect(result.payload).toBe(todoId);
      expect(mockFetch).toHaveBeenCalledWith(`/api/todos/${todoId}`, {
        method: 'DELETE',
        headers: {
          'Authorization': 'Bearer test-token'
        }
      });
    });

    it('should handle delete todo error', async () => {
      const todoId = 'todo-123';
      const errorResponse = {
        message: 'Todo not found',
        code: 404
      };

      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: false,
        json: () => Promise.resolve(errorResponse)
      } as Response);

      const dispatch = jest.fn();
      const getState = jest.fn();
      const extra = {};

      const thunk = deleteTodo(todoId);
      const result = await thunk(dispatch, getState, extra);

      expect(result.type).toBe('todos/deleteTodo/rejected');
      expect(result.payload).toEqual(errorResponse);
    });
  });
});

πŸ”§ Testing Reducers

πŸ—‚οΈ Testing State Transitions

Reducers are pure functions that should be tested thoroughly for all possible state transitions.

// 🌟 Comprehensive reducer testing

describe('Auth Reducer', () => {
  // βœ… Testing initial state
  it('should return initial state', () => {
    const state = authReducer(undefined, { type: '@@INIT' } as any);
    
    expect(state).toEqual(initialAuthState);
  });

  // βœ… Testing synchronous actions
  describe('Synchronous Actions', () => {
    it('should handle clearError', () => {
      const previousState: AuthState = {
        ...initialAuthState,
        error: 'Some error'
      };

      const state = authReducer(previousState, clearError());

      expect(state.error).toBeNull();
      expect(state).toEqual({
        ...previousState,
        error: null
      });
    });

    it('should handle resetLoginAttempts', () => {
      const previousState: AuthState = {
        ...initialAuthState,
        loginAttempts: 3,
        lastLoginAttempt: '2023-01-01T00:00:00Z'
      };

      const state = authReducer(previousState, resetLoginAttempts());

      expect(state.loginAttempts).toBe(0);
      expect(state.lastLoginAttempt).toBeNull();
    });

    it('should handle updateUserProfile when user exists', () => {
      const user: User = {
        id: 'user-123',
        name: 'John Doe',
        email: '[email protected]',
        role: 'user',
        isActive: true,
        createdAt: '2023-01-01T00:00:00Z'
      };

      const previousState: AuthState = {
        ...initialAuthState,
        user
      };

      const updates = { name: 'Updated Name', email: '[email protected]' };
      const state = authReducer(previousState, updateUserProfile(updates));

      expect(state.user).toEqual({
        ...user,
        ...updates
      });
    });

    it('should not update user profile when user is null', () => {
      const previousState: AuthState = {
        ...initialAuthState,
        user: null
      };

      const updates = { name: 'Updated Name' };
      const state = authReducer(previousState, updateUserProfile(updates));

      expect(state.user).toBeNull();
      expect(state).toEqual(previousState);
    });

    it('should handle setToken', () => {
      const token = 'new-token-123';
      const state = authReducer(initialAuthState, setToken(token));

      expect(state.token).toBe(token);
    });
  });

  // βœ… Testing async action states
  describe('Login User Async Actions', () => {
    it('should handle loginUser.pending', () => {
      const action = { type: loginUser.pending.type };
      const state = authReducer(initialAuthState, action);

      expect(state.loading).toBe(true);
      expect(state.error).toBeNull();
    });

    it('should handle loginUser.fulfilled', () => {
      const payload = {
        user: {
          id: 'user-123',
          name: 'John Doe',
          email: '[email protected]',
          role: 'user' as const,
          isActive: true,
          createdAt: '2023-01-01T00:00:00Z'
        },
        token: 'auth-token-123'
      };

      const previousState: AuthState = {
        ...initialAuthState,
        loading: true,
        loginAttempts: 2,
        lastLoginAttempt: '2023-01-01T10:00:00Z'
      };

      const action = { type: loginUser.fulfilled.type, payload };
      const state = authReducer(previousState, action);

      expect(state).toEqual({
        ...previousState,
        loading: false,
        user: payload.user,
        token: payload.token,
        error: null,
        loginAttempts: 0,
        lastLoginAttempt: null
      });
    });

    it('should handle loginUser.rejected', () => {
      const payload = {
        message: 'Invalid credentials',
        code: 401
      };

      const previousState: AuthState = {
        ...initialAuthState,
        loading: true,
        loginAttempts: 1
      };

      const action = { type: loginUser.rejected.type, payload };
      const state = authReducer(previousState, action);

      expect(state.loading).toBe(false);
      expect(state.error).toBe('Invalid credentials');
      expect(state.loginAttempts).toBe(2);
      expect(state.lastLoginAttempt).toBeDefined();
      expect(new Date(state.lastLoginAttempt!).getTime()).toBeCloseTo(Date.now(), -3);
    });

    it('should handle loginUser.rejected without payload', () => {
      const previousState: AuthState = {
        ...initialAuthState,
        loading: true
      };

      const action = { type: loginUser.rejected.type, payload: undefined };
      const state = authReducer(previousState, action);

      expect(state.loading).toBe(false);
      expect(state.error).toBe('Login failed');
      expect(state.loginAttempts).toBe(1);
    });
  });

  // βœ… Testing logout actions
  describe('Logout User Async Actions', () => {
    const loggedInState: AuthState = {
      ...initialAuthState,
      user: {
        id: 'user-123',
        name: 'John Doe',
        email: '[email protected]',
        role: 'user',
        isActive: true,
        createdAt: '2023-01-01T00:00:00Z'
      },
      token: 'auth-token-123',
      loginAttempts: 1,
      lastLoginAttempt: '2023-01-01T10:00:00Z'
    };

    it('should handle logoutUser.pending', () => {
      const action = { type: logoutUser.pending.type };
      const state = authReducer(loggedInState, action);

      expect(state.loading).toBe(true);
      // Other state should remain unchanged
      expect(state.user).toBe(loggedInState.user);
      expect(state.token).toBe(loggedInState.token);
    });

    it('should handle logoutUser.fulfilled', () => {
      const action = { type: logoutUser.fulfilled.type };
      const state = authReducer(loggedInState, action);

      expect(state).toEqual({
        loading: false,
        user: null,
        token: null,
        error: null,
        loginAttempts: 0,
        lastLoginAttempt: null
      });
    });

    it('should handle logoutUser.rejected', () => {
      const payload = {
        message: 'Logout failed',
        code: 500
      };

      const action = { type: logoutUser.rejected.type, payload };
      const state = authReducer(loggedInState, action);

      expect(state.loading).toBe(false);
      expect(state.error).toBe('Logout failed');
      // User should remain logged in on logout failure
      expect(state.user).toBe(loggedInState.user);
      expect(state.token).toBe(loggedInState.token);
    });
  });
});

describe('Todos Reducer', () => {
  // βœ… Testing initial state
  it('should return initial state', () => {
    const state = todosReducer(undefined, { type: '@@INIT' } as any);
    
    expect(state).toEqual(initialTodosState);
  });

  // βœ… Testing synchronous actions
  describe('Synchronous Actions', () => {
    it('should handle setFilter', () => {
      const filter = 'completed';
      const previousState = {
        ...initialTodosState,
        pagination: { ...initialTodosState.pagination, page: 3 }
      };

      const state = todosReducer(previousState, setFilter(filter));

      expect(state.filter).toBe(filter);
      expect(state.pagination.page).toBe(1); // Should reset to page 1
    });

    it('should handle setSortBy', () => {
      const sortBy = 'priority';
      const state = todosReducer(initialTodosState, setSortBy(sortBy));

      expect(state.sortBy).toBe(sortBy);
    });

    it('should handle setSearchQuery', () => {
      const searchQuery = 'important task';
      const previousState = {
        ...initialTodosState,
        pagination: { ...initialTodosState.pagination, page: 2 }
      };

      const state = todosReducer(previousState, setSearchQuery(searchQuery));

      expect(state.searchQuery).toBe(searchQuery);
      expect(state.pagination.page).toBe(1); // Should reset to page 1
    });

    it('should handle toggleTodoCompleted for existing todo', () => {
      const todos: Todo[] = [
        {
          id: 'todo-1',
          text: 'First todo',
          completed: false,
          priority: 'medium'
        },
        {
          id: 'todo-2',
          text: 'Second todo',
          completed: true,
          priority: 'high'
        }
      ];

      const previousState = {
        ...initialTodosState,
        items: todos
      };

      const state = todosReducer(previousState, toggleTodoCompleted('todo-1'));

      expect(state.items[0].completed).toBe(true);
      expect(state.items[1].completed).toBe(true); // Should remain unchanged
    });

    it('should handle toggleTodoCompleted for non-existing todo', () => {
      const todos: Todo[] = [
        {
          id: 'todo-1',
          text: 'First todo',
          completed: false,
          priority: 'medium'
        }
      ];

      const previousState = {
        ...initialTodosState,
        items: todos
      };

      const state = todosReducer(previousState, toggleTodoCompleted('non-existing'));

      expect(state.items).toEqual(todos); // Should remain unchanged
    });

    it('should handle addTodoOptimistic', () => {
      const newTodo: Todo = {
        id: 'temp-123',
        text: 'Optimistic todo',
        completed: false,
        priority: 'low'
      };

      const state = todosReducer(initialTodosState, addTodoOptimistic(newTodo));

      expect(state.items).toHaveLength(1);
      expect(state.items[0]).toEqual(newTodo);
    });

    it('should handle removeTodoOptimistic', () => {
      const todos: Todo[] = [
        {
          id: 'temp-123',
          text: 'Temp todo',
          completed: false,
          priority: 'medium'
        },
        {
          id: 'real-456',
          text: 'Real todo',
          completed: false,
          priority: 'high'
        }
      ];

      const previousState = {
        ...initialTodosState,
        items: todos
      };

      const state = todosReducer(previousState, removeTodoOptimistic('temp-123'));

      expect(state.items).toHaveLength(1);
      expect(state.items[0].id).toBe('real-456');
    });
  });

  // βœ… Testing async actions
  describe('Fetch Todos Async Actions', () => {
    it('should handle fetchTodos.pending', () => {
      const action = { type: fetchTodos.pending.type };
      const state = todosReducer(initialTodosState, action);

      expect(state.loading).toBe(true);
      expect(state.error).toBeNull();
    });

    it('should handle fetchTodos.fulfilled', () => {
      const payload = {
        todos: [
          {
            id: 'todo-1',
            text: 'First todo',
            completed: false,
            priority: 'high' as const
          },
          {
            id: 'todo-2',
            text: 'Second todo',
            completed: true,
            priority: 'low' as const
          }
        ],
        pagination: {
          page: 1,
          limit: 10,
          total: 2,
          totalPages: 1
        }
      };

      const action = { type: fetchTodos.fulfilled.type, payload };
      const state = todosReducer(initialTodosState, action);

      expect(state.loading).toBe(false);
      expect(state.items).toEqual(payload.todos);
      expect(state.pagination).toEqual(payload.pagination);
      expect(state.error).toBeNull();
    });

    it('should handle fetchTodos.rejected', () => {
      const payload = {
        message: 'Failed to fetch todos',
        code: 500
      };

      const action = { type: fetchTodos.rejected.type, payload };
      const state = todosReducer(initialTodosState, action);

      expect(state.loading).toBe(false);
      expect(state.error).toBe('Failed to fetch todos');
    });
  });

  // βœ… Testing create/update/delete actions
  describe('CRUD Async Actions', () => {
    const existingTodos: Todo[] = [
      {
        id: 'todo-1',
        text: 'Existing todo',
        completed: false,
        priority: 'medium'
      }
    ];

    it('should handle createTodo.fulfilled', () => {
      const newTodo: Todo = {
        id: 'todo-2',
        text: 'New todo',
        completed: false,
        priority: 'high'
      };

      const previousState = {
        ...initialTodosState,
        items: [...existingTodos, { id: 'temp-id', text: 'Temp', completed: false, priority: 'low' as const }],
        loading: true
      };

      const action = { type: createTodo.fulfilled.type, payload: newTodo };
      const state = todosReducer(previousState, action);

      expect(state.loading).toBe(false);
      expect(state.error).toBeNull();
      expect(state.items).toHaveLength(2);
      expect(state.items[0]).toEqual(newTodo); // New todo should be first
      expect(state.items[1]).toEqual(existingTodos[0]);
      expect(state.items.some(item => item.id === 'temp-id')).toBe(false); // Temp should be removed
    });

    it('should handle updateTodo.fulfilled', () => {
      const updatedTodo: Todo = {
        id: 'todo-1',
        text: 'Updated todo',
        completed: true,
        priority: 'high'
      };

      const previousState = {
        ...initialTodosState,
        items: existingTodos
      };

      const action = { type: updateTodo.fulfilled.type, payload: updatedTodo };
      const state = todosReducer(previousState, action);

      expect(state.items).toHaveLength(1);
      expect(state.items[0]).toEqual(updatedTodo);
    });

    it('should handle deleteTodo.fulfilled', () => {
      const previousState = {
        ...initialTodosState,
        items: existingTodos
      };

      const action = { type: deleteTodo.fulfilled.type, payload: 'todo-1' };
      const state = todosReducer(previousState, action);

      expect(state.items).toHaveLength(0);
    });
  });
});

πŸͺ Testing Store Integration

πŸ”— Testing Complete Store Behavior

Store testing verifies that actions, reducers, and selectors work together correctly.

// 🌟 Comprehensive store integration testing

describe('Store Integration', () => {
  let store: ReturnType<typeof createTestStore>;

  beforeEach(() => {
    store = createTestStore();
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  // βœ… Testing initial store state
  it('should have correct initial state', () => {
    const state = store.getState();

    expect(state.auth).toEqual(initialAuthState);
    expect(state.todos).toEqual(initialTodosState);
  });

  // βœ… Testing store with preloaded state
  it('should initialize with preloaded state', () => {
    const preloadedState: Partial<RootState> = {
      auth: {
        ...initialAuthState,
        user: {
          id: 'user-123',
          name: 'John Doe',
          email: '[email protected]',
          role: 'user',
          isActive: true,
          createdAt: '2023-01-01T00:00:00Z'
        },
        token: 'existing-token'
      }
    };

    const storeWithState = createTestStore(preloadedState);
    const state = storeWithState.getState();

    expect(state.auth.user).toEqual(preloadedState.auth!.user);
    expect(state.auth.token).toBe('existing-token');
  });

  // βœ… Testing action dispatching
  it('should dispatch actions and update state', () => {
    // Test auth actions
    store.dispatch(setToken('new-token'));
    expect(store.getState().auth.token).toBe('new-token');

    store.dispatch(clearError());
    expect(store.getState().auth.error).toBeNull();

    // Test todos actions
    store.dispatch(setFilter('completed'));
    expect(store.getState().todos.filter).toBe('completed');

    store.dispatch(setSearchQuery('test query'));
    expect(store.getState().todos.searchQuery).toBe('test query');
  });

  // βœ… Testing async thunk integration
  it('should handle async thunk workflow', async () => {
    const mockUser = {
      id: 'user-123',
      name: 'John Doe',
      email: '[email protected]',
      role: 'user' as const,
      isActive: true,
      createdAt: '2023-01-01T00:00:00Z'
    };

    const mockResponse = {
      user: mockUser,
      token: 'auth-token-123'
    };

    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockResponse)
    } as Response);

    const credentials = { email: '[email protected]', password: 'password123' };

    // Dispatch async action
    const resultAction = await store.dispatch(loginUser(credentials));

    // Check that action was fulfilled
    expect(resultAction.type).toBe('auth/loginUser/fulfilled');
    
    // Check state updates
    const state = store.getState();
    expect(state.auth.loading).toBe(false);
    expect(state.auth.user).toEqual(mockUser);
    expect(state.auth.token).toBe('auth-token-123');
    expect(state.auth.error).toBeNull();
  });

  // βœ… Testing multiple action interactions
  it('should handle complex action sequences', async () => {
    // Start with login
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({
        user: {
          id: 'user-123',
          name: 'John Doe',
          email: '[email protected]',
          role: 'admin',
          isActive: true,
          createdAt: '2023-01-01T00:00:00Z'
        },
        token: 'auth-token-123'
      })
    } as Response);

    await store.dispatch(loginUser({
      email: '[email protected]',
      password: 'password123'
    }));

    // Update user profile
    store.dispatch(updateUserProfile({ name: 'Updated Name' }));

    // Add some todos
    store.dispatch(addTodoOptimistic({
      id: 'temp-1',
      text: 'First todo',
      completed: false,
      priority: 'high'
    }));

    store.dispatch(addTodoOptimistic({
      id: 'temp-2',
      text: 'Second todo',
      completed: true,
      priority: 'low'
    }));

    // Change filter
    store.dispatch(setFilter('active'));

    // Verify final state
    const state = store.getState();
    
    expect(state.auth.user?.name).toBe('Updated Name');
    expect(state.todos.items).toHaveLength(2);
    expect(state.todos.filter).toBe('active');
  });

  // βœ… Testing error scenarios
  it('should handle error states correctly', async () => {
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    mockFetch.mockResolvedValue({
      ok: false,
      json: () => Promise.resolve({
        message: 'Invalid credentials',
        code: 401
      })
    } as Response);

    const credentials = { email: '[email protected]', password: 'wrongpass' };

    await store.dispatch(loginUser(credentials));

    const state = store.getState();
    expect(state.auth.loading).toBe(false);
    expect(state.auth.error).toBe('Invalid credentials');
    expect(state.auth.user).toBeNull();
    expect(state.auth.loginAttempts).toBe(1);
  });

  // βœ… Testing optimistic updates
  it('should handle optimistic updates correctly', async () => {
    const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
    
    // Add optimistic todo
    const optimisticTodo: Todo = {
      id: 'temp-id',
      text: 'Optimistic todo',
      completed: false,
      priority: 'medium'
    };

    store.dispatch(addTodoOptimistic(optimisticTodo));
    expect(store.getState().todos.items).toHaveLength(1);

    // Mock successful creation
    const createdTodo: Todo = {
      id: 'real-id',
      text: 'Optimistic todo',
      completed: false,
      priority: 'medium'
    };

    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(createdTodo)
    } as Response);

    // Dispatch actual create action
    await store.dispatch(createTodo({
      text: 'Optimistic todo',
      completed: false,
      priority: 'medium'
    }));

    const finalState = store.getState();
    expect(finalState.todos.items).toHaveLength(1);
    expect(finalState.todos.items[0].id).toBe('real-id'); // Should have real ID
    expect(finalState.todos.loading).toBe(false);
    expect(finalState.todos.error).toBeNull();
  });

  // βœ… Testing state persistence
  it('should maintain state consistency across actions', () => {
    let stateChanges: RootState[] = [];

    // Subscribe to state changes
    const unsubscribe = store.subscribe(() => {
      stateChanges.push(store.getState());
    });

    // Perform multiple actions
    store.dispatch(setToken('token-1'));
    store.dispatch(setFilter('active'));
    store.dispatch(setSearchQuery('search'));
    store.dispatch(clearError());

    unsubscribe();

    // Verify all state changes were tracked
    expect(stateChanges).toHaveLength(4);
    
    // Verify final state
    const finalState = store.getState();
    expect(finalState.auth.token).toBe('token-1');
    expect(finalState.todos.filter).toBe('active');
    expect(finalState.todos.searchQuery).toBe('search');
  });
});

// βœ… Testing selectors
describe('Selectors', () => {
  let store: ReturnType<typeof createTestStore>;

  beforeEach(() => {
    store = createTestStore();
  });

  it('should select auth state correctly', () => {
    const mockUser: User = {
      id: 'user-123',
      name: 'John Doe',
      email: '[email protected]',
      role: 'user',
      isActive: true,
      createdAt: '2023-01-01T00:00:00Z'
    };

    // Set up state
    store.dispatch(setToken('test-token'));
    store.dispatch(updateUserProfile(mockUser));

    // Test selectors
    const state = store.getState();
    expect(selectUser(state)).toEqual(mockUser);
    expect(selectIsAuthenticated(state)).toBe(true);
    expect(selectAuthLoading(state)).toBe(false);
    expect(selectAuthError(state)).toBeNull();
  });

  it('should select filtered todos correctly', () => {
    const todos: Todo[] = [
      { id: '1', text: 'Active todo 1', completed: false, priority: 'high' },
      { id: '2', text: 'Completed todo', completed: true, priority: 'low' },
      { id: '3', text: 'Active todo 2', completed: false, priority: 'medium' },
      { id: '4', text: 'Another completed', completed: true, priority: 'high' }
    ];

    // Set up todos
    todos.forEach(todo => {
      store.dispatch(addTodoOptimistic(todo));
    });

    const state = store.getState();

    // Test 'all' filter
    expect(selectFilteredTodos(state)).toHaveLength(4);

    // Test 'active' filter
    store.dispatch(setFilter('active'));
    const activeState = store.getState();
    const activeTodos = selectFilteredTodos(activeState);
    expect(activeTodos).toHaveLength(2);
    expect(activeTodos.every(todo => !todo.completed)).toBe(true);

    // Test 'completed' filter
    store.dispatch(setFilter('completed'));
    const completedState = store.getState();
    const completedTodos = selectFilteredTodos(completedState);
    expect(completedTodos).toHaveLength(2);
    expect(completedTodos.every(todo => todo.completed)).toBe(true);
  });

  it('should select todos with search query', () => {
    const todos: Todo[] = [
      { id: '1', text: 'Important task', completed: false, priority: 'high' },
      { id: '2', text: 'Regular task', completed: false, priority: 'low' },
      { id: '3', text: 'Another important item', completed: true, priority: 'medium' }
    ];

    todos.forEach(todo => {
      store.dispatch(addTodoOptimistic(todo));
    });

    // Set search query
    store.dispatch(setSearchQuery('important'));

    const state = store.getState();
    const filteredTodos = selectFilteredTodos(state);

    expect(filteredTodos).toHaveLength(2);
    expect(filteredTodos.every(todo => 
      todo.text.toLowerCase().includes('important')
    )).toBe(true);
  });

  it('should calculate todo stats correctly', () => {
    const todos: Todo[] = [
      { id: '1', text: 'Todo 1', completed: false, priority: 'high' },
      { id: '2', text: 'Todo 2', completed: true, priority: 'low' },
      { id: '3', text: 'Todo 3', completed: false, priority: 'high' },
      { id: '4', text: 'Todo 4', completed: false, priority: 'medium' }
    ];

    todos.forEach(todo => {
      store.dispatch(addTodoOptimistic(todo));
    });

    const state = store.getState();
    const stats = selectTodoStats(state);

    expect(stats).toEqual({
      total: 4,
      completed: 1,
      active: 3,
      highPriority: 2
    });
  });
});

πŸ§ͺ Testing React-Redux Integration

βš›οΈ Testing Components with Redux

Testing React components that use Redux requires proper store setup and component integration testing.

// 🌟 Comprehensive React-Redux integration testing

describe('React-Redux Integration', () => {
  // βœ… Testing connected components
  describe('Auth Components', () => {
    const LoginForm: React.FC = () => {
      const dispatch = useDispatch();
      const { loading, error } = useSelector((state: RootState) => ({
        loading: selectAuthLoading(state),
        error: selectAuthError(state)
      }));

      const [email, setEmail] = React.useState('');
      const [password, setPassword] = React.useState('');

      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        await dispatch(loginUser({ email, password }));
      };

      return (
        <form onSubmit={handleSubmit} data-testid="login-form">
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            data-testid="email-input"
          />
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            data-testid="password-input"
          />
          <button type="submit" disabled={loading} data-testid="submit-button">
            {loading ? 'Logging in...' : 'Login'}
          </button>
          {error && <div data-testid="error-message">{error}</div>}
        </form>
      );
    };

    it('should handle successful login', async () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({
          user: {
            id: 'user-123',
            name: 'John Doe',
            email: '[email protected]',
            role: 'user',
            isActive: true,
            createdAt: '2023-01-01T00:00:00Z'
          },
          token: 'auth-token-123'
        })
      } as Response);

      const { store } = renderWithRedux(<LoginForm />);

      const emailInput = screen.getByTestId('email-input');
      const passwordInput = screen.getByTestId('password-input');
      const submitButton = screen.getByTestId('submit-button');

      // Fill form
      fireEvent.change(emailInput, { target: { value: '[email protected]' } });
      fireEvent.change(passwordInput, { target: { value: 'password123' } });

      // Submit form
      fireEvent.click(submitButton);

      // Should show loading state
      expect(submitButton).toHaveTextContent('Logging in...');
      expect(submitButton).toBeDisabled();

      // Wait for completion
      await waitFor(() => {
        expect(submitButton).toHaveTextContent('Login');
        expect(submitButton).not.toBeDisabled();
      });

      // Verify store state
      const state = store.getState();
      expect(state.auth.user?.email).toBe('[email protected]');
      expect(state.auth.token).toBe('auth-token-123');
    });

    it('should handle login error', async () => {
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      mockFetch.mockResolvedValue({
        ok: false,
        json: () => Promise.resolve({
          message: 'Invalid credentials',
          code: 401
        })
      } as Response);

      renderWithRedux(<LoginForm />);

      const emailInput = screen.getByTestId('email-input');
      const passwordInput = screen.getByTestId('password-input');
      const submitButton = screen.getByTestId('submit-button');

      // Fill form with invalid credentials
      fireEvent.change(emailInput, { target: { value: '[email protected]' } });
      fireEvent.change(passwordInput, { target: { value: 'wrongpass' } });

      // Submit form
      fireEvent.click(submitButton);

      // Wait for error
      await waitFor(() => {
        expect(screen.getByTestId('error-message')).toHaveTextContent('Invalid credentials');
      });

      expect(submitButton).toHaveTextContent('Login');
      expect(submitButton).not.toBeDisabled();
    });
  });

  // βœ… Testing todos components
  describe('Todos Components', () => {
    const TodoList: React.FC = () => {
      const dispatch = useDispatch();
      const { todos, loading, filter } = useSelector((state: RootState) => ({
        todos: selectFilteredTodos(state),
        loading: selectTodosLoading(state),
        filter: selectTodosFilter(state)
      }));

      const handleToggle = (id: string) => {
        dispatch(toggleTodoCompleted(id));
      };

      const handleFilterChange = (newFilter: TodosState['filter']) => {
        dispatch(setFilter(newFilter));
      };

      if (loading) {
        return <div data-testid="loading">Loading todos...</div>;
      }

      return (
        <div data-testid="todo-list">
          <div data-testid="filter-buttons">
            <button
              onClick={() => handleFilterChange('all')}
              data-active={filter === 'all'}
              data-testid="filter-all"
            >
              All
            </button>
            <button
              onClick={() => handleFilterChange('active')}
              data-active={filter === 'active'}
              data-testid="filter-active"
            >
              Active
            </button>
            <button
              onClick={() => handleFilterChange('completed')}
              data-active={filter === 'completed'}
              data-testid="filter-completed"
            >
              Completed
            </button>
          </div>
          
          <div data-testid="todos">
            {todos.map(todo => (
              <div key={todo.id} data-testid={`todo-${todo.id}`}>
                <span className={todo.completed ? 'completed' : ''}>
                  {todo.text}
                </span>
                <button
                  onClick={() => handleToggle(todo.id)}
                  data-testid={`toggle-${todo.id}`}
                >
                  {todo.completed ? 'Undo' : 'Complete'}
                </button>
              </div>
            ))}
          </div>
          
          {todos.length === 0 && (
            <div data-testid="no-todos">No todos found</div>
          )}
        </div>
      );
    };

    it('should render todos and handle filtering', () => {
      const initialState: Partial<RootState> = {
        todos: {
          ...initialTodosState,
          items: [
            { id: '1', text: 'Active todo', completed: false, priority: 'medium' },
            { id: '2', text: 'Completed todo', completed: true, priority: 'low' },
            { id: '3', text: 'Another active', completed: false, priority: 'high' }
          ]
        }
      };

      renderWithRedux(<TodoList />, { store: createMockStore(initialState) });

      // Should show all todos initially
      expect(screen.getByTestId('todo-1')).toBeInTheDocument();
      expect(screen.getByTestId('todo-2')).toBeInTheDocument();
      expect(screen.getByTestId('todo-3')).toBeInTheDocument();

      // Filter to active todos
      fireEvent.click(screen.getByTestId('filter-active'));

      // Should only show active todos
      expect(screen.getByTestId('todo-1')).toBeInTheDocument();
      expect(screen.queryByTestId('todo-2')).not.toBeInTheDocument();
      expect(screen.getByTestId('todo-3')).toBeInTheDocument();

      // Filter to completed todos
      fireEvent.click(screen.getByTestId('filter-completed'));

      // Should only show completed todos
      expect(screen.queryByTestId('todo-1')).not.toBeInTheDocument();
      expect(screen.getByTestId('todo-2')).toBeInTheDocument();
      expect(screen.queryByTestId('todo-3')).not.toBeInTheDocument();
    });

    it('should handle todo toggle', () => {
      const initialState: Partial<RootState> = {
        todos: {
          ...initialTodosState,
          items: [
            { id: '1', text: 'Test todo', completed: false, priority: 'medium' }
          ]
        }
      };

      const { store } = renderWithRedux(<TodoList />, { 
        store: createMockStore(initialState) 
      });

      const toggleButton = screen.getByTestId('toggle-1');
      expect(toggleButton).toHaveTextContent('Complete');

      // Toggle todo
      fireEvent.click(toggleButton);

      // Verify state change
      const state = store.getState();
      expect(state.todos.items[0].completed).toBe(true);
      expect(toggleButton).toHaveTextContent('Undo');
    });

    it('should show loading state', () => {
      const initialState: Partial<RootState> = {
        todos: {
          ...initialTodosState,
          loading: true
        }
      };

      renderWithRedux(<TodoList />, { store: createMockStore(initialState) });

      expect(screen.getByTestId('loading')).toHaveTextContent('Loading todos...');
      expect(screen.queryByTestId('todo-list')).not.toBeInTheDocument();
    });

    it('should show empty state', () => {
      renderWithRedux(<TodoList />);

      expect(screen.getByTestId('no-todos')).toHaveTextContent('No todos found');
    });
  });

  // βœ… Testing custom hooks with Redux
  describe('Custom Hooks with Redux', () => {
    const useAuth = () => {
      const dispatch = useDispatch();
      const authState = useSelector((state: RootState) => state.auth);

      const login = useCallback((credentials: { email: string; password: string }) => {
        return dispatch(loginUser(credentials));
      }, [dispatch]);

      const logout = useCallback(() => {
        return dispatch(logoutUser());
      }, [dispatch]);

      const clearAuthError = useCallback(() => {
        dispatch(clearError());
      }, [dispatch]);

      return {
        ...authState,
        login,
        logout,
        clearAuthError,
        isAuthenticated: !!authState.token
      };
    };

    const TestComponent: React.FC = () => {
      const auth = useAuth();

      return (
        <div>
          <div data-testid="auth-status">
            {auth.isAuthenticated ? 'Authenticated' : 'Not authenticated'}
          </div>
          <div data-testid="user-name">
            {auth.user?.name || 'No user'}
          </div>
          {auth.error && (
            <div data-testid="auth-error">{auth.error}</div>
          )}
          <button onClick={auth.clearAuthError} data-testid="clear-error">
            Clear Error
          </button>
        </div>
      );
    };

    it('should use auth hook correctly', () => {
      const initialState: Partial<RootState> = {
        auth: {
          ...initialAuthState,
          user: {
            id: 'user-123',
            name: 'John Doe',
            email: '[email protected]',
            role: 'user',
            isActive: true,
            createdAt: '2023-01-01T00:00:00Z'
          },
          token: 'auth-token-123'
        }
      };

      renderWithRedux(<TestComponent />, { store: createMockStore(initialState) });

      expect(screen.getByTestId('auth-status')).toHaveTextContent('Authenticated');
      expect(screen.getByTestId('user-name')).toHaveTextContent('John Doe');
    });

    it('should handle error clearing', () => {
      const initialState: Partial<RootState> = {
        auth: {
          ...initialAuthState,
          error: 'Login failed'
        }
      };

      const { store } = renderWithRedux(<TestComponent />, { 
        store: createMockStore(initialState) 
      });

      expect(screen.getByTestId('auth-error')).toHaveTextContent('Login failed');

      // Clear error
      fireEvent.click(screen.getByTestId('clear-error'));

      // Verify error is cleared
      const state = store.getState();
      expect(state.auth.error).toBeNull();
      expect(screen.queryByTestId('auth-error')).not.toBeInTheDocument();
    });
  });
});

πŸŽ‰ Conclusion

Congratulations! You’ve mastered the art of testing Redux in TypeScript! 🎯

πŸ”‘ Key Takeaways

  1. Action Testing: Verify action creators produce correct types and payloads
  2. Async Thunk Testing: Mock external dependencies and test all scenarios
  3. Reducer Testing: Test pure functions for all state transitions
  4. Store Integration: Test complete workflows with real store instances
  5. Selector Testing: Verify computed state derivations
  6. Component Integration: Test React-Redux connections with proper store setup
  7. Error Scenarios: Test failure paths and error handling
  8. Performance: Verify optimistic updates and state consistency

πŸš€ Next Steps

  • API Testing: Master API mocking with Mock Service Worker (MSW)
  • Integration Testing: Build comprehensive integration test suites
  • E2E Testing: Test complete user workflows with Redux state
  • Performance Testing: Optimize Redux performance and test scaling
  • Advanced Patterns: Test Redux middleware and complex state patterns

You now have the skills to test any Redux application with confidence, ensuring your state management is reliable, predictable, and maintainable! 🌟