Episode ini membahas strategi testing Redux: menguji reducer secara murni, menguji async thunk dengan mock fetch, merender komponen dengan test store memakai testing-library, serta mengintegrasikan MSW untuk mem-mock API secara realistis tanpa server nyata.

Redux dirancang agar mudah diuji: reducer adalah fungsi murni, dan seluruh logika async tinggal di thunk. Episode 18 mengubah keunggulan itu menjadi praktik — kalian akan belajar menulis test untuk slice, thunk, dan komponen yang memakai Redux, serta mem-mock API dengan MSW sehingga test berjalan cepat dan deterministik.
Strategi pengujian berlapis: unit test untuk reducer yang murni, integration test untuk thunk dengan fetch dimock, lalu component test dengan renderer + store uji. Lapisan ini bersama-sama menjaga agar setiap perubahan kode tidak diam-diam memecahkan perilaku state.
Sebelum menulis test, siapkan tooling dan store uji yang dibangun dari slice yang sama dengan aplikasi:
npm install -D vitest @testing-library/react @testing-library/jest-dom mswimport { configureStore } from "@reduxjs/toolkit"
import { authSlice } from "../features/auth/authSlice"
import { postsSlice } from "../features/posts/postsSlice"
export const createTestStore = (preloaded = {}) =>
configureStore({
reducer: {
auth: authSlice.reducer,
posts: postsSlice.reducer,
},
preloadedState: preloaded,
})createTestStore adalah factory test store dengan state awal yang bisa disuntikkan. Setiap test membuat store baru sehingga tidak ada state bocor antar test.
Reducer hanyalah fungsi: beri state dan action, periksa hasilnya. postsReducer(initialState, postAdded(...)) adalah panggilan fungsi murni biasa:
import { expect, it } from "vitest"
import postsReducer, { postAdded } from "./postsSlice"
it("menambahkan post baru ke state", () => {
const initialState = { ids: [], entities: {} }
const next = postsReducer(
initialState,
postAdded({ id: 1, title: "Halo" }),
)
expect(next.entities[1]?.title).toBe("Halo")
expect(next.ids).toContain(1)
})Thunk diuji dengan mem-mock fetch global dan store uji. Dispatch thunk, lalu tunggu hingga selesai:
import { expect, it, vi } from "vitest"
import { createTestStore } from "../../test/store"
import { fetchPosts } from "./postsSlice"
it("menyimpan posts saat berhasil", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => [{ id: 1, title: "Dari API" }],
}))
const store = createTestStore()
await store.dispatch(fetchPosts())
expect(store.getState().posts.items).toHaveLength(1)
expect(store.getState().posts.status).toBe("succeeded")
vi.unstubAllGlobals()
})store.dispatch(fetchPosts()) mengembalikan promise thunk. Setelah resolve, state bisa diperiksa langsung. Tambahkan skenario kedua dengan mockRejectedValue untuk memastikan status failed pada lifecycle rejected — keduanya mencakup seluruh lifecycle pending, fulfilled, dan rejected.
Komponen Redux diuji dengan membungkusnya dalam Provider berisi test store:
import { render, screen, waitFor } from "@testing-library/react"
import { Provider } from "react-redux"
import { createTestStore } from "../../test/store"
import PostList from "./PostList"
it("menampilkan daftar post dari store", () => {
const store = createTestStore({
posts: { items: [{ id: 1, title: "Post Satu" }], status: "succeeded" },
})
render(
<Provider store={store}>
<PostList />
</Provider>,
)
expect(screen.getByText("Post Satu")).toBeInTheDocument()
})Karena createTestStore menerima preloaded state, test bisa menanamkan data langsung tanpa menunggu fetch. Untuk komponen yang memakai hook query, gunakan waitFor menunggu cache terisi; store uji tetap memakai middleware default RTK — termasuk middleware RTK Query — sehingga hook query berjalan persis seperti di produksi.
MSW mem-mock di level jaringan: request /api/... ditangkap dan dijawab handler, bukan memblokir fetch per test. Definisikan handler sekali untuk seluruh suite:
import { http, HttpResponse } from "msw"
export const handlers = [
http.get("*/api/posts", () => {
return HttpResponse.json([
{ id: 1, title: "Post MSW" },
{ id: 2, title: "Post Kedua" },
])
}),
http.post("*/api/posts", async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 3, ...body }, { status: 201 })
}),
]Handler berjalan untuk seluruh URL yang cocok — tak perlu menyentuh kode aplikasi sama sekali. MSW bekerja untuk fetch, axios, maupun graphql-request.
Aktifkan server di setup test:
import { setupServer } from "msw/node"
import { handlers } from "./mocks/handlers"
export const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())Setelah setup ini, thunk maupun hook query yang melakukan request /api/... akan mendapatkan respons MSW. Kombinasikan dengan component test, maka seluruh lapisan — dari komponen sampai store — teruji tanpa server nyata.
Warning
Jangan campur mock fetch manual dan MSW dalam satu test file. Pilih satu strategi per lapisan: mock fetch untuk unit test thunk, MSW untuk integration dan component test agar perilaku jaringan lebih realistis.
Testing Redux terasa ringan karena arsitekturnya yang murni. Reducer diuji sebagai fungsi biasa, thunk diuji dengan mem-mock fetch terhadap test store, dan komponen diuji dengan renderer yang dibungkus Provider. MSW menutup lapisan terluar dengan mem-mock API secara realistis, sehingga seluruh stack — action, reducer, store, komponen — dapat diverifikasi tanpa server.
Inti yang harus dibawa pulang:
createTestStore agar tiap test memiliki state isolasi.dispatch selesai.waitFor untuk query.Episode 19 berikutnya membahas debugging — kalian akan memanfaatkan Redux DevTools untuk time-travel debugging, action trace, dan state diff, lalu memecahkan masalah umum seperti selector yang membuat object baru, Immer error, stale state, dan masalah hydration SSR.