{"id":13450216,"url":"https://github.com/antoinejaussoin/jooks","last_synced_at":"2025-08-26T14:27:39.254Z","repository":{"id":37623050,"uuid":"175285904","full_name":"antoinejaussoin/jooks","owner":"antoinejaussoin","description":"Testing hooks with Jest","archived":false,"fork":false,"pushed_at":"2023-03-15T02:31:25.000Z","size":1901,"stargazers_count":84,"open_issues_count":6,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-13T01:13:45.340Z","etag":null,"topics":["hooks","react","reacthooks","testing","typescript","unit-testing"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/antoinejaussoin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-03-12T19:49:06.000Z","updated_at":"2023-05-02T18:26:19.000Z","dependencies_parsed_at":"2024-01-07T18:03:59.065Z","dependency_job_id":"7ccb71b1-513f-43c2-8d99-603615fc2f85","html_url":"https://github.com/antoinejaussoin/jooks","commit_stats":{"total_commits":30,"total_committers":9,"mean_commits":"3.3333333333333335","dds":0.6666666666666667,"last_synced_commit":"70e3c5d51c1daf4f7275a8c179abad94369b9a2b"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/antoinejaussoin%2Fjooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/antoinejaussoin%2Fjooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/antoinejaussoin%2Fjooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/antoinejaussoin%2Fjooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/antoinejaussoin","download_url":"https://codeload.github.com/antoinejaussoin/jooks/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248650754,"owners_count":21139681,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hooks","react","reacthooks","testing","typescript","unit-testing"],"created_at":"2024-07-31T07:00:32.392Z","updated_at":"2025-04-13T01:13:49.794Z","avatar_url":"https://github.com/antoinejaussoin.png","language":"TypeScript","funding_links":[],"categories":["Tools","工具"],"sub_categories":[],"readme":"# Jooks (Jest ❤ + Hooks 🤘🏻)\n\n\u003e If you're going through hell testing React Hooks, keep going.\n\u003e (_Churchill_)\n\n[![Version](https://img.shields.io/npm/v/jooks.svg)]()\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](https://github.com/antoinejaussoin/jooks/pulls)\n\n## What are Custom React Hooks\n\n[React Hooks](https://reactjs.org/docs/hooks-faq.html) are a new API added to React from version 16.8.\n\nThey are great, and make proper separation of concern and re-using logic across components very easy and enjoyable.\n\n**One problem**: they are f\\*ing hard to test.\n\nLet's start with a definition first: Custom React Hooks (CRH) are functions, starting with `use` (by convention), that are themselves using React's Hooks (`useState`, `useEffect` and so on). They are standalone, and not part of a component.\n\n## Why this library?\n\nCustom React Hooks are very hard to test. There are a [few](https://medium.com/@dawchihliou/testing-react-hooks-6d3ae95cd838) [articles](https://blog.logrocket.com/a-quick-guide-to-testing-react-hooks-fa584c415407) [dedicated](https://medium.com/@rossirsa/testing-react-hooks-%EF%B8%8F-f0466fd41cbf) to this, but they all come down to the same solution: **instantiating a fake component** that is using the hook, and testing that the hook is working through that component.\n\nThe premise of this testing library is that this is **slow**, **hard to setup**, and fails when you use `useEffect` in an asynchronous manner.\n\n## The solution\n\nSo the solution to that, that this library brings, is to **completely mock the React Hooks API**, and replace it by fake implementations (that you control).\n\nThat way you can **test your hooks in isolation**, and you don't need the overhead of instantiating React Components and a DOM just to test a small function.\n\n## Prerequisites\n\nThis library works with Jest.\n\n## Current capabilities\n\nCurrently, the library supports most of React's basic hooks:\n\n- [useState](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useStateExample.test.ts)\n- [useEffect](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useEffectExample.test.ts)\n- [useContext](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useContextExample.test.ts)\n- [useReducer](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useReducerExample.test.ts)\n- useCallback\n- [useMemo](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useMemoExample.test.ts)\n- [useRef](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useRefExample.test.ts)\n- ~~useImperativeHandle~~ (Coming Soon!)\n- [useLayoutEffect](https://github.com/antoinejaussoin/jooks/blob/master/src/__tests__/useLayoutEffectWithDependencyExample.test.ts)\n- useDebugValue\n\n## Installation\n\n`yarn add jooks`\n\n## Examples\n\n### Simple example with useState only\n\nLet's take this very simple hook:\n\n```javascript\nimport { useState } from 'react';\n\nexport default function useStateExample() {\n  const [first, setFirst] = useState('alpha');\n  const [second, setSecond] = useState('beta');\n  const [third, setThird] = useState(() =\u003e 'charlie'); // Notice the delayed execution\n  const update = () =\u003e {\n    setFirst(first + 'a');\n    setSecond(second + 'b');\n    setThird(third + 'c');\n  };\n  return { first, second, third, update };\n}\n\n```\n\nTo test this (mostly useless) CRH, here is what you need to do:\n\n```javascript\nimport 'jest';\nimport init from 'jooks';\nimport useStateOnlyExample from '../useStateExample';\n\ndescribe('Testing useState hook', () =\u003e {\n  // Initialising the Jooks wrapper\n  const jooks = init(() =\u003e useStateOnlyExample());\n\n  it('It should give the correct initial values', () =\u003e {\n    // Run your Hook function\n    const { first, second } = jooks.run();\n\n    // And then test the result\n    expect(first).toBe('alpha');\n    expect(second).toBe('beta');\n  });\n\n  it('It should update the values properly', () =\u003e {\n    // Run your Hook function\n    let { first, second, third, update } = jooks.run();\n    expect(first).toBe('alpha');\n    expect(second).toBe('beta');\n    expect(third).toBe('charlie');\n\n    // Call the callback\n    update();\n\n    // Run the Hook again to get the new values\n    ({ first, second, third } = jooks.run());\n    expect(first).toBe('alphaa');\n    expect(second).toBe('betab');\n    expect(third).toBe('charliec');\n  });\n});\n```\n\nAs you can see, testing the hook was mostly painless.\n\n### A more complicated example involving useEffect\n\nThis hook is a more real-world example. It fetches data (once), stores it into the state, and returns that data.\n\nIt also provides a callback to fetch another set of data.\n\nThe example is using TypeScript, but this would of course work with vanilla JavaScript.\n\n```javascript\nimport { useEffect, useState } from 'react';\n\ninterface Activity {\n  activity: string;\n  accessibility: number;\n  type: string;\n  participants: number;\n  price: number;\n  key: string;\n}\n\nexport default function useLoadActivity() {\n  const [activity, setActivity] = useState\u003cActivity | null\u003e(null);\n\n  const fetchData = async () =\u003e {\n    const result = await fetch('https://www.boredapi.com/api/activity');\n    if (result.ok) {\n      const content = (await result.json()) as Activity;\n      setActivity(content);\n    }\n  };\n\n  useEffect(() =\u003e {\n    fetchData();\n  }, []);\n\n  return { activity, next: fetchData };\n}\n```\n\nThe problem with that example, is that it would be impossible to test with other techniques, because of the asynchronous content of the `useEffect` function.\nIt acts in a \"fire and forget\" way, [which means that wrapping the call in `act()`](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks) is not going to work: `act()` is synchronous, and the asynchronous function inside will resolve outside of it at a later point, resulting in an error in the console.\n\nSo how do I test that with Jooks?\n\n```javascript\nimport 'jest';\nimport init from 'jooks';\nimport useLoadActivity from '../useLoadActivity';\n// This library helps you with testing fetch, but you can use other methods to achieve the same thing\nimport { GlobalWithFetchMock } from 'jest-fetch-mock';\n\n// This is a TypeScript specific way of mocking the fetch function.\n// See the jest-fetch-mock documentation for more information.\nconst customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;\ncustomGlobal.fetch = require('jest-fetch-mock');\ncustomGlobal.fetchMock = customGlobal.fetch;\n\ndescribe('Testing a custom hook', () =\u003e {\n  const jooks = init(() =\u003e useLoadActivity());\n  beforeEach(() =\u003e {\n    customGlobal.fetch.mockResponses(\n      // On the first call, the endpoint will return Foo\n      [JSON.stringify({ activity: 'Foo' }), { status: 200 }],\n      // And Bar on the second call\n      [JSON.stringify({ activity: 'Bar' }), { status: 200 }]\n    );\n  });\n  afterEach(() =\u003e {\n    customGlobal.fetch.mockClear();\n  });\n\n  it('Should load activities properly', async () =\u003e {\n    // By default, before it has a change to load anything, the hook\n    // will return a null\n    expect(jooks.run().activity).toBeNull();\n    // Then we wait for the component to \"mount\", essentially giving\n    // it time to resolve the effect and load the data\n    await jooks.mount();\n    expect(jooks.run().activity).not.toBeNull();\n    expect(jooks.run().activity!.activity).toBe('Foo');\n    // Then we call the next() callback\n    await jooks.run().next();\n    expect(jooks.run().activity).not.toBeNull();\n    expect(jooks.run().activity!.activity).toBe('Bar');\n  });\n});\n\n```\n\n### How to work with useContext\n\nWhen working with `useContext`, you need to set that context first, in your test.\n\nSince Hooks rely on the order they are called, you can safely call `setContext` on your jooks wrapper, once per `useContext` call, in the same order.\n\nLet's see an example:\n\n```javascript\nimport { useContext } from 'react';\nimport { Context1, Context2 } from './ExampleContext';\n\nexport default function useContextExample() {\n  const { foo } = useContext(Context1);\n  const { ping } = useContext(Context2);\n  return foo + ':' + ping;\n}\n```\n\nAnd the test:\n\n```javascript\nimport 'jest';\nimport useContextExample from '../useContextExample';\nimport { Context1, Context2 } from '../ExampleContext';\nimport init from 'jooks';\n\ndescribe('Testing useContext hook', () =\u003e {\n  const jooks = init(() =\u003e useContextExample());\n\n  beforeEach(() =\u003e {\n    // First you need to set all contexts, in the same order they are called, for every test.\n    jooks.setContext(Context1, { foo: 'baz' });\n    jooks.setContext(Context2, { ping: 'pung' });\n  });\n\n  it('It should give the correct values', () =\u003e {\n    // Then you can run your hook normally and expect that `useContext`\n    // will return the value you set in the beforeEach\n    const foo = jooks.run();\n    expect(foo).toBe('baz:pung');\n  });\n\n  it('It should give the correct values when set within the test', () =\u003e {\n    // If you want to change that context for a specific test, you can always reset the context values\n    // and set new ones.\n    jooks.resetContext();\n    jooks.setContext(Context1, { foo: 'buz' });\n    jooks.setContext(Context2, { ping: 'pang' });\n    const foo = jooks.run();\n    expect(foo).toBe('buz:pang');\n  });\n});\n```\n\n### Custom hooks with arguments\n\nHooks often rely on passed-in arguments to compute a value, for example:\n\n```javascript\nimport { useContext } from 'react';\nimport { Context1 } from './ExampleContext';\n\nexport default function useContextWithArgsExample(bar: string) {\n  const { foo } = useContext(Context1);\n  return foo + ':' + bar;\n}\n```\n\nTo test these, we can simply pass in the argument when we call `.run()`:\n\n```javascript\nimport 'jest';\nimport useContextWithArgsExample from '../useContextWithArgsExample';\nimport { Context1 } from '../ExampleContext';\nimport init from 'jooks';\n\ndescribe('Testing useContextWithArgsExample hook', () =\u003e {\n  const jooks = init(useContextWithArgsExample);\n\n  beforeEach(() =\u003e {\n    jooks.setContext(Context1, { foo: 'baz' });\n  });\n\n  it('It should give the correct values', () =\u003e {\n    // Here it should compute the hook's return value based on what you passed in\n    const foo = jooks.run('bar');\n    expect(foo).toBe('baz:bar');\n  });\n});\n```\n\n## API\n\nThe library exposes 3 things:\n\n- The default export is an initialisation function, as shown in the examples above, hidding the complexity away. This is Jest-specific.\n- `Jooks` class: this is the class that contains the logic and wraps your hook. It is independent from any testing framework so it could be used with other testing frameworks.\n\n### `function init\u003cT\u003e(hook: (...args: any[]) =\u003e T, verbose?: boolean)` (default export)\n\nThis function is meant to be called within your test's `describe` function.\nIt takes one compulsory argument, and one optional flag:\n\n- `hook`: This is a function that calls your hook, or the hook function itself\n- `verbose` (optional): Whether to enable the verbose mode, logging information to the console, for debugging purpose.\n\n```javascript\nconst jooks = init(() =\u003e useContextExample(someVariable));\n// or\nconst jooks = init(useContextExample);\n```\n\nor, to enable the verbose mode;\n\n```javascript\nconst jooks = init(() =\u003e useContextExample(), true);\n// or\nconst jooks = init(useContextExample, true);\n```\n\n#### Two ways to initialise Jooks\n\nAs see above, you have two ways of initialising a hook :\n\nIn the first one, you instantiate your hook on initialisation, with function arguments that are not going to change for the rest of the test:\n\n```javascript\nconst jooks = init(() =\u003e useContextExample(someVariable));\njooks.run();\n```\n\nAlternatively, you can specify the Hook function directly, and provide its argument on each `run()` call, like so:\n\n```javascript\nconst jooks = init(useContextExample);\njooks.run(someVariable);\n```\n\n### Jooks\n\nThis is the object you get on the third parameter of the describe function.\n\nIt exposes 3 methods that you should care about:\n\n- `async mount(wait: number = 1): Promise\u003cR\u003e`: ensure your hook ran all its effect \"on mount\"\n- `run(...hooksParams?)`: this runs your hook function and returns the result. This is usually what you are testing.\n- `async wait(wait: number = 1)`: if you are expecting an asynchronous effect to fire, call this to wait until it's resolved\n\nAn additional 2 methods are dedicated to Context:\n\n- `setContext\u003cT\u003e(context: React.Context\u003cT\u003e, value: T)`: Allows you to set the context before it's used with `useContext`.\n- `resetContext()`: Resets all previously set contexts\n\nThere are 2 other public method that you shouldn't use if you are using the init function as described above.\n\n- `setup`: this is to be run before every test. This is done automatically if you are using the init function.\n- `cleanup`: this is to be run after every test. This is done automatically if you are using the init function.\n\n#### async mount(wait: number = 1): Promise\u003cvoid\u003e\n\nUse this function at the beginning of a test to wait for `useEffect` to fire.\n\nA classic way of using this is: testing that the default values are fine, then wait for the API call that will populates with real data from the backend.\n\nIf for some reason the effect doesn't resolve quickly enough, you can increase the timeout time using the optional parameter: `await mount(5);`.\n\n```javascript\nit(\"Should load activities properly\", async () =\u003e {\n  // By default, before it has a change to load anything, the hook\n  // will return a null\n  expect(jooks.run().activity).toBeNull();\n  // Then we wait for the component to \"mount\", essentially giving it time to resolve the\n  // effect and load the data\n  await jooks.mount();\n  expect(jooks.run().activity).not.toBeNull();\n  expect(jooks.run().activity!.activity).toBe(\"Foo\");\n});\n```\n\n#### run(...hooksParams?)\n\nThis function runs your hook function. Use this to test the hook output.\n\n#### async wait(wait: number = 1)\n\nIf you know that a change you make (say calling one of the hook callbacks) is going to result in an async callback or useEffect to be called, use this to wait until the effect has resolved. You can extend the wait time, but the default (1) should be enough if you properly mock your API calls. It will fire all effects and then wait.\n\n#### setContext\u003cT\u003e(context: React.Context\u003cT\u003e, value: T)\n\nThis is only necessary when your Hook uses `useContext`. You need to call this as many times you have a `useContext` call, and in the same order. First, provide your context object, and then it's value.\n\n## Future\n\n- Making Jooks compatible with other testing frameworks\n- Allowing a better control on the Hook's internal state\n\n## Change Log\n\n### Version 2.0.2\n\n- Upgrading dependencies and fixing the React peer dependency so it works with any version greater than 16.8\n\n### Version 2.0.1\n\n- Upgrading dependencies for security reasons\n\n### Version 2.0.0\n\n- **Breaking change**: context values don't have to be set in a specific order anymore. They don't have to be set multiple times either.\n- Remove dependency to `lodash`. Only `lodash.isequal` is now a dependency.\n- Fixed some mistakes in the examples.\n\n### Version 1.2.0\n\n- Updating dependencies\n- Adding link to GitHub Repository (courtesy of Microsoft)\n\n## FAQ\n\n- Why Jooks? it's a mix of Jest and Hooks.\n- Can I use this? Since it's a testing library (and not a piece of code that's going to production), yes go ahead. The API is likely to change a bit before being stable though, so you might want to pin down your version.\n- There must be a Medium article about that? [Yes, there is.](https://medium.com/@jantoine/another-take-on-testing-custom-react-hooks-4461458935d4)\n\n## Thanks\n\n- Thanks to [@bronter](https://github.com/bronter) for suggesting and implementing the ability to specify a hook function parameters on `run()` instead of just during initialisation.\n- Thanks to [@jjd314](https://github.com/jjd314) for [implementing a fix](https://github.com/antoinejaussoin/jooks/pull/6) around saving run() arguments\n- Thanks to [@hjvvoorthuijsen](https://github.com/hjvvoorthuijsen) for improving the `setState` mock and [allowing the use of a callback function](https://github.com/antoinejaussoin/jooks/pull/9).\n\n## Can I contribute?\n\nYes please! MR welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fantoinejaussoin%2Fjooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fantoinejaussoin%2Fjooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fantoinejaussoin%2Fjooks/lists"}