Skip to content

Commit

Permalink
feat(unit-tests): test examples for Redux and Saga
Browse files Browse the repository at this point in the history
Added redux and saga tests examples

Installed dependencies:
yarn add redux-saga-test-plan --dev
  • Loading branch information
rbiedrawa committed Mar 15, 2022
1 parent bb71c61 commit 50ae5e5
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 414 deletions.
9 changes: 7 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ module.exports = {
],
},
},
"rules": {
'rules': {
'jest/expect-expect': [
'error',
{
assertFunctionNames: ['expect', 'testSaga', 'expectSaga'],
},
],
'import/order': [
'error',
{
Expand Down Expand Up @@ -66,7 +72,6 @@ module.exports = {
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error",
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
},
parserOptions: {
sourceType: 'module',
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
* [commitlint](https://commitlint.js.org/#/) - Lint commit messages
* [Standard Version](https://github.com/conventional-changelog/standard-version) - A utility for versioning using semver and CHANGELOG generation powered by Conventional Commits.

* [Redux Saga Test Plan](https://github.com/jfairbank/redux-saga-test-plan) - Redux Saga Test Plan aims to embrace both integration testing and unit testing approaches to make testing your sagas easy.

## Additional Links

* [React+TypeScript Cheatsheets](https://github.com/typescript-cheatsheets/react) - Cheatsheets for experienced React developers getting started with TypeScript
Expand All @@ -128,4 +130,5 @@ npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
* [Redux Style Guide](https://redux.js.org/style-guide/style-guide#write-action-types-as-domaineventname)
* [MUI - theme switcher](https://mui.com/customization/dark-mode/)
* [Level up your CSS linting using Stylelint](https://blog.logrocket.com/using-stylelint-improve-lint-css-scss-sass/)
* [Create React App: A quick setup guide](https://blog.logrocket.com/create-react-app-a-quick-setup-guide-b812f0aad03c/)
* [Create React App: A quick setup guide](https://blog.logrocket.com/create-react-app-a-quick-setup-guide-b812f0aad03c/)
* [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/)
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@
"@mui/material": "^5.5.0",
"@redux-saga/core": "^1.1.3",
"@reduxjs/toolkit": "^1.8.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9",
"@types/redux-logger": "^3.0.9",
"history": "^5.3.0",
"i18next": "^21.6.14",
"json-server": "^0.17.0",
Expand Down Expand Up @@ -76,6 +68,14 @@
"@commitlint/config-conventional": "16.2.1",
"@typescript-eslint/eslint-plugin": "5.14.0",
"@typescript-eslint/parser": "5.14.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9",
"@types/redux-logger": "^3.0.9",
"eslint": "8.11.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "16.1.2",
Expand All @@ -90,6 +90,7 @@
"lint-staged": "12.3.5",
"postcss-scss": "4.0.3",
"prettier": "2.5.1",
"redux-saga-test-plan": "^4.0.4",
"standard-version": "9.3.2",
"stylelint": "14.5.3",
"stylelint-config-prettier": "9.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Headers/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
import { NavLink as RouterLink } from 'react-router-dom'

type HeaderProps = {
currentThemeMode: string
currentThemeMode: 'light' | 'dark'
onChangeThemeClick: () => void
onChangeLanguage: (lang: string) => void
}
Expand Down
36 changes: 36 additions & 0 deletions src/features/posts/store/__tests__/posts.sagas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { nanoid } from '@reduxjs/toolkit'
import { expectSaga, testSaga } from 'redux-saga-test-plan'
import { call } from 'redux-saga-test-plan/matchers'

import { getPosts } from '../../api'
import { Post } from '../../types'
import { onGetPosts } from '../posts.sagas'
import postsReducer, { postsActions } from '../posts.slice'

const expectedSagaPosts: Post[] = [{ id: '1', title: 'saga-test-example', body: nanoid() }]

describe('Saga - test examples', () => {
it('should execute commands in exact order with redux-saga-test-plan', async () =>
testSaga(onGetPosts)
.next()
.call(getPosts)
.next(expectedSagaPosts)
.put(postsActions.fetchAllSucceeded(expectedSagaPosts))
.next()
.isDone())

it('should mock external api with .provide()', () =>
expectSaga(onGetPosts)
.provide([[call(getPosts), expectedSagaPosts]])
.put(postsActions.fetchAllSucceeded(expectedSagaPosts))
.run())

test('integration test with withReducer', () =>
expectSaga(onGetPosts)
.withReducer(postsReducer)
.provide([[call(getPosts), expectedSagaPosts]])
.hasFinalState({
posts: expectedSagaPosts,
})
.run())
})
81 changes: 81 additions & 0 deletions src/features/posts/store/__tests__/posts.slice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { RootState, store } from '../../../../store/store'
import { Post } from '../../types'
import reducer, { postsActions, selectPosts } from '../posts.slice'

const expectedPosts = [
{ id: '1', body: 'post1', title: 'post1' },
{ id: '2', body: 'post2', title: 'post2' },
]

describe('State tests', () => {
it('should initially set post to an empty array', () => {
const state = store.getState().posts
expect(state.posts.length).toEqual(0)
})
})

describe('Reducer tests', () => {
it('should return the initial state when passed an empty action', () => {
// Given
const initialState = undefined

const action = { type: '' }

// When
const result = reducer(initialState, action)

// Then
expect(result).toEqual({ posts: [] })
})

it('should add received posts', () => {
// Given
const initialState = undefined

const action = postsActions.fetchAllSucceeded(expectedPosts)

// When
const result = reducer(initialState, action)

// Then
expect(Object.keys(result.posts).length).toEqual(expectedPosts.length)
expect(result.posts).toEqual(expectedPosts)
})
})

describe('Selectors tests', () => {
it('should return empty posts', () => {
// Given
const state: RootState = {
posts: {
posts: [],
},
router: {},
}

// When
const result = selectPosts(state)

// Then
expect(result).toEqual([])
})
})


const expectedSagaPosts: Post[] = [{ id: '1', title: 'saga', body: 'saga' }]

jest.mock('../../api/index', () => ({
async getPosts() {
return expectedSagaPosts
},
}))

describe('Saga tests', () => {
it('should return all posts when fetchAll dispatched (using mocked Rest API)', async () => {
// When
await store.dispatch(postsActions.fetchAll())

// Then
expect(store.getState().posts.posts).toEqual(expectedSagaPosts)
})
})
36 changes: 26 additions & 10 deletions src/features/posts/store/posts.sagas.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import { put, takeEvery } from 'redux-saga/effects'
import { SagaIterator } from '@redux-saga/core'
import { call, put, takeEvery } from 'redux-saga/effects'

import { createPost, deletePost, getPosts, updatePost } from '../api'
import { Post } from '../types'

import { postsActions } from './posts.slice'

// Worker Sagas
function* onGetPosts() {
const posts: Post[] = yield getPosts()
export function* onGetPosts(): SagaIterator {
const posts: Post[] = yield call(getPosts)
yield put(postsActions.fetchAllSucceeded(posts))
}

function* onCreatePost({ payload }: { type: typeof postsActions.create; payload: Post }) {
yield createPost(payload)
function* onCreatePost({
payload,
}: {
type: typeof postsActions.create
payload: Post
}): SagaIterator {
yield call(createPost, payload)
yield put(postsActions.fetchAll())
}

function* onUpdatePost({ payload }: { type: typeof postsActions.update; payload: Post }) {
yield updatePost(payload)
function* onUpdatePost({
payload,
}: {
type: typeof postsActions.update
payload: Post
}): SagaIterator {
yield call(updatePost, payload)
yield put(postsActions.fetchAll())
}

function* onDeletePost({ payload }: { type: typeof postsActions.delete; payload: Post }) {
yield deletePost(payload)
function* onDeletePost({
payload,
}: {
type: typeof postsActions.delete
payload: Post
}): SagaIterator {
yield call(deletePost, payload)
yield put(postsActions.fetchAll())
}

// Watcher Saga
export function* postsWatcherSaga() {
export function* postsWatcherSaga(): SagaIterator {
yield takeEvery(postsActions.fetchAll.type, onGetPosts)
yield takeEvery(postsActions.update.type, onUpdatePost)
yield takeEvery(postsActions.delete.type, onDeletePost)
Expand Down
2 changes: 1 addition & 1 deletion src/features/posts/store/posts.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createAction, createSlice, nanoid, PayloadAction } from '@reduxjs/toolk
import type { RootState } from '../../../store/store'
import { Post } from '../types'

interface PostsState {
export interface PostsState {
posts: Post[]
}

Expand Down
Loading

0 comments on commit 50ae5e5

Please sign in to comment.