# 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

## 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

:::
## 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**.
:::

### 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);
});
})
```

#### 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)