# Unit test #1 ###### tags: `Test` ## Quick review :::success **Reference** https://hackmd.io/iZTRq6kXTwOawxnA_HNKUA ::: ### AAA - Arrange - Act - Assert :::spoiler Example ```javascript= // Credit: https://github.com/nielsen-oss/docs/tree/master/javascript/testing it('allows the user to login successfully', async () => { // Assign jest.spyOn(window, 'login').mockImplementationOnce(() => { return Promise.resolve({}) }) const {getByLabelText, getByText, findByRole} = render(<Login />) // Act userEvent.type(getByLabelText(/username/i), 'chuck') userEvent.type(getByLabelText(/password/i), 'norris') userEvent.click(getByText(/submit/i)) // Assert // wait for the side effect to finish before selecting elements on the dom const alert = await findByRole('alert') expect(alert).toHaveTextContent(/congrats/i) }) ``` ::: ### Testing setup - Assertion library - Test runner ![](https://i.imgur.com/Fg2Bj8j.png) ## Test behavior but not implementation :::success **Reference** [Testing Implementation Details](https://kentcdodds.com/blog/testing-implementation-details) [Follow the user](https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#follow-the-user) [We strongly encourage testing components' behavior rather than testing their implementation details.](https://github.com/nielsen-oss/docs/tree/master/javascript/testing#hooks-component) ::: :bulb: Testing behavior ensure you don't need to update test when you revise your code :::spoiler Example ![](https://i.imgur.com/djongJ1.jpg) ::: ## Assert: matcher :::success **Reference** [Jest official: Expect](https://jestjs.io/docs/expect) ::: :bulb: Make good use of ESLint :::spoiler Example: [eslint-plugin-jest](https://www.npmjs.com/package/eslint-plugin-jest) ```javascript= // .eslintrc { "extends": [ ... "plugin:jest/recommended", "plugin:jest/style" ], "plugins": ["jest"] } ``` ```javascript= // foo.test.ts expect(null).toBe(null); // ESLint error: use `toBeNull` instead (eslintjest/prefer-to-be) ``` ::: ## Arrange: mocking :::warning :apple: Why we need to **mock** something? :pear: When we want to **controll the behavior of the dependencies**. ::: ![](https://i.imgur.com/U1ktrBH.png) ### Make a `stub` : `jest.fn()` :::success **Reference** [Jest official: Mock Functions](https://jestjs.io/docs/mock-function-api) ::: If a function become a stub, you could ... 1. Rewrite / eliminate its behavior - `<mockFn>.mockImplement()`, `<mockFn>.mockImplementOnce()`, `<mockFn>.mockReturnValue()` ... - `<mockFn>.mockReset()`, `<mockFn>.mockRestore()` 2. Record of its behavior / reset the record between test cases - `<mockFn>.mock.calls` - `<mockFn>.mockClear()` ```javascript= // The function was called twice expect(someMockFunction.mock.calls.length).toBe(2); // The first arg of the first call to the function was 'first arg' expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // The second arg of the first call to the function was 'second arg' expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); // The first arg of the second call to the function was 'first arg' expect(someMockFunction.mock.calls[1][0]).toBe('first arg of the second call'); // The return value of the first call to the function was 'return value' expect(someMockFunction.mock.results[0].value).toBe('return value'); ... ``` ### Two way to mock: `jest.spyOn()` vs `jest.mock()` :::success **Reference** [Jest official: jest.spyOn()](https://jestjs.io/docs/jest-object#jestspyonobject-methodname) [Jest official: jest.mock()](https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options) [Understanding Jest Mocks](https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c) [Jest.Mock vs Jest.SpyOn - What to use for Mocking](https://www.developright.co.uk/posts/jest-mock-vs-jest-spyon-what-to-use-for-mocking.html) [How to spy on a default exported function with Jest?](https://stackoverflow.com/questions/54245654/how-to-spy-on-a-default-exported-function-with-jest) [Mocks and Spies with Jest](https://dev.to/qmenoret/mocks-and-spies-with-jest-32gf) [What is the difference between jest.fn() and jest.spyOn() methods in jest?](https://stackoverflow.com/questions/57643808/what-is-the-difference-between-jest-fn-and-jest-spyon-methods-in-jest) ::: #### Examples :::spoiler Target to test: `foo()` <br /> ```javascript= // foo.utils.js // import { isNullish } from '../isNullish/isNullish.utils'; import isNullish from '../isNullish/isNullish.utils'; const foo = (value) => isNullish(value); export default foo; ``` ```javascript= // isNullish.utils.js export const isNullish = (value) => [null, undefined].some((each) => each === value); export default isNullish; ``` ::: :::spoiler Test `foo()` without `jest.mock()` <br /> ```javascript= import foo from './foo.utils'; import isNullish from '../isNullish/isNullish.utils'; test('foo()', () => { expect(jest.isMockFunction(isNullish)).toBe(false); expect(foo(null)).toBe(true); expect(foo(0)).toBe(false); }); ``` ::: :::spoiler Test `foo()` with `jest.mock()` <br /> > [Note] If you import the module you mock in test file, the variables you get are also the mocked version. This behavior is the result of `jest.mock()` hoisting behavior <br /> ```javascript= import foo from './foo.utils'; // import { isNullish } from '../isNullish/isNullish.utils'; import isNullish from '../isNullish/isNullish.utils'; jest.mock('../isNullish/isNullish.utils', () => ({ __esModule: true, isNullish: jest.fn(() => true), default: jest.fn(() => true), })) test('foo()', () => { expect(jest.isMockFunction(isNullish)).toBe(true); expect(foo(null)).toBe(true); expect(foo(0)).toBe(true); expect(isNullish).toBeCalledTimes(2); }); ``` ::: :::spoiler Test `foo()` without `jest.spyOn()` <br /> ```javascript= import foo from './foo.utils'; import * as isNullishModule from '../isNullish/isNullish.utils'; test('foo()', () => { expect(jest.isMockFunction(isNullishModule.default)).toBe(false); expect(foo(null)).toBe(true); expect(foo(0)).toBe(false); }); ``` ::: :::spoiler Test `foo()` with `jest.spyOn()` <br /> ```javascript= import foo from './foo.utils'; import * as isNullishModule from '../isNullish/isNullish.utils'; jest.spyOn(isNullishModule, 'default'); test('foo()', () => { expect(jest.isMockFunction(isNullishModule.default)).toBe(true); jest.mocked(isNullishModule.default).mockImplementation(() => true); expect(foo(null)).toBe(true); expect(foo(0)).toBe(true); }); ``` ::: <br /> #### Default export, property `__esModule` and `jest.mock()` :::success **Reference** [深入聊一聊__esModule](https://juejin.cn/post/7063002055308214302) ::: :::spoiler Example <br /> :apple: What if we use `jest.mock()` without `__esModule` to mock what is default exported from module ? :pear: The variable imported via default export `import <variables> from '...'` would be a whole module following the import/export rules of `cjs (commonjs)` but not `esm (ECMAScript)`. <br /> ```javascript= import foo from './foo.utils'; // import { isNullish } from '../isNullish/isNullish.utils'; import isNullish from '../isNullish/isNullish.utils'; jest.mock('../isNullish/isNullish.utils', () => ({ __esModule: true, isNullish: jest.fn(() => true), default: jest.fn(() => true), })) test('foo()', () => { expect(jest.isMockFunction(isNullish)).toBe(true); expect(foo(null)).toBe(true); expect(foo(0)).toBe(true); expect(isNullish).toBeCalledTimes(2); }); ``` ::: <br /> #### Difference | | `jest.spyOn()` | `jest.mock()` | | -------- | -------- | -------- | | behave as actual | default | `<mockFn>.mockImplement(jest.requireActual())` | | behave as mock |`<spyFn>.mockImplement()`| default | |clean up "mock implementation" | `<spyFn>.mockRestore()` | `<mockFn>.mockReset()` | |timing to use 1 | for specific test case | among test cases (test file) | |timing to use 2 | mock part of module | mock all the module | > [Note] `<spyFn>.mockReset()` has same effect as `<spyFn>.mockImplement(() => undefined)` <br /> #### `jest.mock()` hoisting behavior :::success **Reference** [How Jest Mocking Works](https://github.com/kentcdodds/how-jest-mocking-works) ::: :bulb: Get mock function indirectly via module imported, or you might get `can't access before initialization` error. ```javascript= // DO import { isNullish as mockIsNullish } from "./foo.constants"; jest.mock('./foo.constants', () => ({ ...jest.requireActual('./foo.constants'), isNullish: jest.fn(), })); describe('foo()', () => { it('is called with `null` and return `false`', () => { ... expect(mockIsNullish).toBeCalledTimes(1); }); }) ``` ```javascript= // DON'T const mockIsNullish = jest.fn(); jest.mock('./foo.constants', () => ({ ...jest.requireActual('./foo.constants'), isNullish: mockIsNullish, })); describe('foo()', () => { it('is called with `null` and return `false`', () => { ... expect(mockIsNullish).toBeCalledTimes(1); }); }) ``` ![](https://i.imgur.com/9v4XYcz.png) #### Exception: returning a function ### Clean-up functions | All mocks | Specific mock | | -------- | -------- | `jest.clearAllMocks()` | `<mockFn>.mockClear()` | | `jest.resetAllMocks()` | `<mockFn>.mockReset()` | | `jest.restoreAllMocks()` | `<mockFn>.mockRestore()` | ### Misc. - `jest.requireActual()` - `jest.createMockFromModule()` ## Testing Asynchronous Code :::success **Reference** [Jest official: Testing Asynchronous Code](https://jestjs.io/docs/asynchronous) ::: :bulb: Use `async/await` instead of `returning promise` :::spoiler Example ```javascript= // DO test('the data is peanut butter', async () => { const data = await fetchData(); expect(data).toBe('peanut butter'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toMatch('error'); } }); ``` ```javascript= // DON'T test('the data is peanut butter', () => { return fetchData().then(data => { expect(data).toBe('peanut butter'); }); }); ``` ::: ## Jest: watch mode :::success **Reference** [Day14 實戰 Jest 配置:增進 Development Experience](https://ithelp.ithome.com.tw/articles/10246627) ::: `npm test --watch` ```javascript Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press q to quit watch mode. › Press Enter to trigger a test run. ``` ## Misc. [Jest mock/spy returns undefined even when set up](https://github.com/facebook/jest/issues/9131#issuecomment-668790615)