Jest, React-Testing-Library

🍠 react-testing-library, Jest

React μ»΄ν¬λ„ŒνŠΈμ˜ κ΅¬μ„±μš”μ†ŒκΉŒμ§€ DOM으둜 λ§Œλ“€μ–΄μ„œ ν…ŒμŠ€νŠΈλ₯Ό ν•  수 있게 λ„μ™€μ£ΌλŠ” 라이브러리 react-testing-library와 λ³„λ„μ˜ μ—¬λŸ¬ 라이브러리λ₯Ό λ°›μ•„μ˜¬ ν•„μš” 없이 ν•˜λ‚˜λ‘œ 합쳐진 Jestλ₯Ό 톡해 ν…ŒμŠ€νŠΈ μ½”λ“œ μž‘μ„± 즉, μ„œλ‘œκ°€ λ‹€λ₯Έ 역할을 κ°–κ³  있고 λ„μ™€μ£ΌλŠ” ν˜•νƒœ λ¬Όλ‘  react-testing-library만 κ°–κ³  ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ Mocha, Expect 같은 라이브러리λ₯Ό μˆ˜λ™μœΌλ‘œ λ°›μ•„μ„œ μž‘μ—…ν•  수 μžˆλ‹€κ³  함. ν•˜μ§€λ§Œ Jest 만 λ°›μœΌλ©΄ λͺ¨λ“ κ²Œ 됨 create-react-app은 두가지가 μ„€μΉ˜λœ μƒνƒœλ‘œ λ„˜μ–΄μ˜΄

πŸ₯œ Jest

ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ μ—¬λŸ¬ 라이브러리λ₯Ό ν•©μΉœ μƒνƒœλ‘œ λ“±μž₯ν•˜λ©΄μ„œ ν•˜λ‚˜μ˜ ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬λΌ 뢈리고 있음.

🍯 Matchers

Jestμ—μ„œ MatchersλŠ” 결과값을 ν…ŒμŠ€νŠΈν•  수 μžˆλŠ” μ—¬λŸ¬ API듀을 μ˜λ―Έν•œλ‹€. 기본적으둜 expect().Matcher APIs ν˜•μ‹μœΌλ‘œ μ§„ν–‰λ˜λŠ” λŠλ‚Œμ΄λ‹€. expect()둜 ν…ŒμŠ€νŠΈ μ§„ν–‰μ˜ λŒ€μƒμ΄ λ˜λŠ” μž‘μ—…(λ©”μ†Œλ“œ, μ»΄ν¬λ„ŒνŠΈ)듀이 λ“€μ–΄κ°€κ³  ν•΄λ‹Ή λ©”μ†Œλ“œλŠ” μ‹€μ œ 결과물인 expection 객체λ₯Ό λ°˜ν™˜ν•œλ‹€κ³  ν•œλ‹€. μ΄ν›„μ˜ Matcher APIsλ₯Ό 톡해 λ‚΄κ°€ μ˜ˆμƒν•˜κ³  μ›ν•˜λŠ” κ²°κ³Όλ¬Όκ³Ό λΉ„κ΅ν•˜λŠ”κ²ƒ κ°™λ‹€.

μ—¬λŸ¬κ°€μ§€ 비ꡐ API듀이 있고 promise, async await κ³Ό 같은 λΉ„λ™κΈ°μž‘μ—…μ˜ ν…ŒμŠ€νŠΈλ„ κ°€λŠ₯ν•˜λ‹€.

React의 라이프사이클과 같은 μˆœμ„œκ°€ ν…ŒμŠ€νŠΈ νŒŒμΌμ—λ„ μ‘΄μž¬ν•˜λŠ”κ²ƒ κ°™λ‹€.

  • beforeAll : ν•΄λ‹Ή ν…ŒμŠ€νŠΈ 파일 λ‚΄ λͺ¨λ“  ν…ŒμŠ€νŠΈ μž‘μ—… 전에 ν•œλ²ˆλ§Œ μ‹€ν–‰λ˜λŠ” λ©”μ†Œλ“œ
  • beforeEach : 각각의 λͺ¨λ“  ν…ŒμŠ€νŠΈ μž‘μ—…μ΄ 전에 μ‹€ν–‰λ˜λŠ” λ©”μ†Œλ“œ
  • afterAll : ν•΄λ‹Ή ν…ŒμŠ€νŠΈ 파일 λ‚΄ λͺ¨λ“  ν…ŒμŠ€νŠΈ μž‘μ—… μ’…λ£Œ 후에 ν•œλ²ˆλ§Œ μ‹€ν–‰λ˜λŠ” λ©”μ†Œλ“œ
  • beforeAll : 각각의 λͺ¨λ“  ν…ŒμŠ€νŠΈ μž‘μ—… μ’…λ£Œ 후에 μ‹€ν–‰λ˜λŠ” λ©”μ†Œλ“œ

λ˜ν•œ, ν…ŒμŠ€νŠΈ 내에 μŠ€μ½”ν”„(μ˜μ—­)을 지정해 쀄 수 μžˆλŠ” λ©”μ†Œλ“œλ„ μ‘΄μž¬ν–ˆλ‹€. describeλ₯Ό 톡해, μŠ€μ½”ν”„λ₯Ό 지정해주고 μœ„μ˜ 라이프사이클도 λ‚΄λΆ€μ—μ„œ λ³„λ„λ‘œ λ™μž‘ν•  수 μžˆμ—ˆλ‹€,

beforeAll(() => console.log('1 - beforeAll'))
afterAll(() => console.log('1 - afterAll'))
beforeEach(() => console.log('1 - beforeEach'))
afterEach(() => console.log('1 - afterEach'))
test('', () => console.log('1 - test'))
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'))
  afterAll(() => console.log('2 - afterAll'))
  beforeEach(() => console.log('2 - beforeEach'))
  afterEach(() => console.log('2 - afterEach'))
  test('', () => console.log('2 - test'))
})

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

🍞 Mock

mocking은 λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•  λ•Œ, ν•΄λ‹Ή μ½”λ“œκ°€ μ˜μ‘΄ν•˜λŠ” 뢀뢄을 κ°€μ§œ(mock)둜 λŒ€μ²΄ν•˜λŠ” 기법이라고 ν•œλ‹€. μ•„λ¬΄λž˜λ„ ν…ŒμŠ€νŠΈ ν•˜λ €λŠ” μ½”λ“œκ°€ μ˜μ‘΄ν•˜λŠ” 뢀뢄을 직접 μƒμ„±ν•˜λŠ”κ²ƒμ€ λΆ€λ‹΄μŠ€λŸ½κΈ° λ•Œλ¬Έμ— mocking을 많이 μ‚¬μš©ν•œλ‹€κ³  ν•œλ‹€. 예둜, λ°μ΄ν„°λ² μ΄μŠ€μ˜ crud μž‘μ—…μ— λŒ€ν•œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•  λ•Œ μ‹€μ œ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ‚¬μš©ν•˜κ²Œλœλ‹€λ©΄ μ—¬λŸ¬ λΆˆνŽΈν•¨κ³Ό λ¬Έμ œκ°€ μžˆμ„ 수 μžˆλ‹€. λ˜ν•œ Unit Test(λ‹¨μœ„ν…ŒμŠ€νŠΈ)λŠ” μ™ΈλΆ€ν™˜κ²½μ— μ˜μ‘΄ν•˜μ§€ μ•Šκ³  λ…λ¦½μ μœΌλ‘œ μ‹€ν–‰λ˜μ•Όν•œλ‹€λŠ” μ μ—μ„œ μœ„λ°°λœλ‹€. 이럴 λ•Œ, κ°€μ§œ 객체λ₯Ό μƒμ„±ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜λŠ”κ²ƒμ΄λ‹€.

μ»΄ν¬λ„ŒνŠΈμ˜ props둜 λ°›μ•„μ˜¨ λ©”μ†Œλ“œ λ˜ν•œ, 초기 ν…ŒμŠ€νŠΈ render λ•Œ mockingν•¨μˆ˜λ₯Ό 보내어 μ‹€ν–‰λ˜λŠ”μ§€ 확인할 수 μžˆμ—ˆλ‹€.

// Link
const Link = ({ page, setState, children }: Props) => {
  const [status, setStatus] = useState(STATUS.NORMAL)

  const onMouseEnter = useCallback(() => {
    setStatus(STATUS.HOVERED)
    setState('μ‹€ν–‰λ˜μ—ˆλ‹Ή')
  }, [setStatus, STATUS, setState])

  const onMouseLeave = useCallback(() => {
    setStatus(STATUS.NORMAL)
  }, [setStatus, STATUS])

  return (
    <a
      className={status}
      href={page || '#'}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
    >
      {children}
    </a>
  )
}

// test
describe('Link', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('Link component mount', () => {
    // screen.debug();
  })

  it('onMouse Toggle', () => {
    const setState = jest.fn()
    render(
      <Link page="http://www.facebook.com" setState={setState}>
        Facebook
      </Link>
    )

    const target = screen.getByText('Facebook')
    fireEvent.mouseEnter(target)
    console.log(target.className)

    expect(setState).toBeCalledTimes(1)

    fireEvent.mouseLeave(target)
    console.log(target.className)
  })
})

μƒμ„±λœ mockingν•¨μˆ˜ μΈμŠ€ν„΄μŠ€μ—λŠ” .mock 속성이 μ‘΄μž¬ν•œλ‹€. ν•΄λ‹Ή μΈμŠ€ν„΄μŠ€λŠ” μ•„λž˜μ™€ 같은 속성듀을 κ°–κ³ μžˆλ‹€.

mock = {
  calls, // μ‹€ν–‰λ λ•Œλ§ˆλ‹€ 받은 λ³€μˆ˜λ₯Ό 담은 이쀑배열
  instances, // λ³€μˆ˜κ°€ μ•„λ‹Œ λ°”μΈλ”©λœ κ°μ²΄λ“±μ˜ μΈμŠ€ν„΄μŠ€
  invocationCallOrder, // ν•΄λ‹Ή λͺ¨μ˜ν•¨μˆ˜(mock) μΈμŠ€ν„΄μŠ€κ°€ μ‹€ν–‰λœ 횟수
  results, // 초기 λͺ¨μ˜ν•¨μˆ˜κ°€ 생성될 λ•Œ 보내진 μž‘μ—…μ˜ κ²°κ³Όκ°’
}

πŸ₯ jest.fn(), jest.spyOn()

jest.fn()을 톡해 mocking ν•¨μˆ˜λ₯Ό λ§Œλ“€ 수 μžˆλ‹€. μƒμ„±λœ mocking ν•¨μˆ˜μ˜ 결과값을 지정해 쀄 수 μžˆλ‹€.

describe('Mock fn', () => {
  let mockFn = null
  beforeEach(() => {
    mockFn.mockClear()
    mockFn = jest.fn()
  })

  it('mockReturnValue', () => {
    mockFn.mockReturnValue('Mock ν•¨μˆ˜ μž…λ‹ˆλ‹€.')
    console.log(mockFn()) // 'Mock ν•¨μˆ˜ μž…λ‹ˆλ‹€.'
  })

  it('mockReturnValueOnce', () => {
    mockFn.mockReturnValueOnce(false).mockReturnValueOnce(true)
    const result = [11, 12].filter(num => mockFn(num))
    console.log(result) // 12
    console.log(mockFn.mock)
    //   {
    //    calls: [ [ 11 ], [ 12 ] ],
    //    instances: [ undefined, undefined ],
    //    invocationCallOrder: [ 1, 2 ],
    //    results: [ { type: 'return', value: false }, { type: 'return', value: true } ]
    // }
  })

  it('mockResolvedValue', () => {
    mockFn.mockResolvedValue('비동기 Mock ν•¨μˆ˜ μž…λ‹ˆλ‹€.')
    mockFn().then(res => console.log(res)) // '비동기 Mock ν•¨μˆ˜ μž…λ‹ˆλ‹€.'
  })

  it('mockImplementation', () => {
    mockFn.mockImplementation(name => `I am ${name}`)
    console.log(mockFn('상민')) // 상민
  })

  it('mockFn props', () => {
    mockFn('a')
    mockFn(['b', 'c'])

    expect(mockFn).toBeCalledTimes(2)
    expect(mockFn).toBeCalledWith('a')
    expect(mockFn).toBeCalledWith(['b', 'c'])
  })
})

jest.spyOn의 경우, μ™ΈλΆ€ λͺ¨λ“ˆμ— κ°μ§€ν•˜λŠ” 슀파이λ₯Ό λΆ™μ—¬λ†“λŠ” κ°œλ…μ΄κΈ° λ•Œλ¬Έμ— μƒν˜Έ μ˜μ‘΄μ„±μ΄ 생길 수 μžˆμ–΄ jest.fn()으둜 μƒˆλ‘œμš΄ κ°€μƒμ˜ mocking λͺ¨λ“ˆμ„ λ§Œλ“œλŠ” 것이 더 μ’‹λ‹€κ³  ν•œλ‹€.

fetch, axios λ“±μ˜ κΈ°λ³Έ μžλ°”μŠ€ν¬λ¦½νŠΈ api에도 μ‚¬μš© κ°€λŠ₯ν•˜λ‹€.

ν•΄λ‹Ή λͺ¨λ“ˆμ˜ 속성에 μ ‘κ·Όν•˜λŠ” 방식

const calculator = {
  add: (a, b) => a + b,
}

describe('Mock spy', () => {
  it('spy fn', () => {
    const mockSpy = jest.spyOn(calculator, 'add')
    const result = calculator.add(2, 3)
    console.log(mockSpy.mock)

    expect(mockSpy).toBeCalledTimes(1)
    expect(mockSpy).toBeCalledWith(2, 3)
    expect(result).toBe(5)
  })
})

비동기 APIλ₯Ό ν…ŒμŠ€νŠΈ ν•˜λŠ” μ˜ˆμ œμ΄λ‹€. spyOn을 μ‚¬μš©ν•˜μ—¬ 기쑴의 λ©”μ†Œλ“œλ₯Ό κ°μ§€ν•˜λŠ” 방식이기 λ•Œλ¬Έμ—, μ„œλ²„ μ’…λ£Œλ‚˜ 잘λͺ»λœ urlλ“± μ—λŸ¬κ°€ λ°œμƒν•˜λ©΄ ν…ŒμŠ€νŠΈμ½”λ“œκ°€ μ§„ν–‰λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, fn으둜 μƒˆλ‘œμš΄ mocking ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•˜λŠ”κ²ƒμ΄ 독립성이 μ€‘μš”ν•œ μœ λ‹›ν…ŒμŠ€νŠΈμ— μ ν•©ν•˜λ‹€.

describe('Mock function example', () => {
  it('get some async data with axios', async () => {
    axios.get = jest.fn().mockResolvedValue({ data: { id: 1, name: '상민' } })
    const user = await axios.get('some url').then(res => res.data)
    console.log(user) // { id: 1, name: '상민' }

    axios.get.mockClear()
  })

  it('get some async data with fetch', async () => {
    // console.log(global.fetch); -> [Function: fetch]
    global.fetch = jest
      .fn()
      .mockResolvedValue({ data: { id: 1, name: '상민' } })
    global.fetch.mockClear
    // console.log(global.fetch); ->  [Function: mockConstructor]
    const { data } = await fetch('url')
    console.log(data) // { id: 1, name: '상민' }

    global.fetch.mockClear()
  })
})

mocking ν•¨μˆ˜λ₯Ό λ§Œλ“€κΈ° μ „μ—λŠ” μ›λ³Έμ˜ λ©”μ†Œλ“œλ‘œ μ‘΄μž¬ν•˜μ§€λ§Œ, mocking ν•¨μˆ˜λ₯Ό λ§Œλ“€κ³  λ‚œ 뒀에 ν•΄λ‹Ή 원본 λ©”μ†Œλ“œλ₯Ό κ²€μƒ‰ν•˜λ©΄ [Function: mockConstructor]λΌλŠ” 결과값이 λ‚˜μ˜¨λ‹€.

즉, 이 ν…ŒμŠ€νŠΈ μ—μ„œλŠ” ν•΄λ‹Ή λ©”μ†Œλ“œκ°€ mocking ν•¨μˆ˜λ‘œ λ³€κ²½λ˜μ—ˆλ‹€λŠ”κ²ƒμ„ μ˜λ―Έν•œλ‹€κ³  ν•œλ‹€.

λ”°λΌμ„œ, μž‘μ—…μ΄ λλ‚œ λ‹€μŒμ—λŠ” ν•΄λ‹Ή mocking μΈμŠ€ν„΄μŠ€λ₯Ό μ΄ˆκΈ°ν™”μ‹œμΌœμ£Όλ„λ‘ ν•˜μž. jest.clearAllMocks()둜 λͺ¨λ‘ μ΄ˆκΈ°ν™”ν•˜κ±°λ‚˜, global.fetch.mockClear()둜 κ°œλ³„ μ΄ˆκΈ°ν™”λ„ κ°€λŠ₯ν•˜λ‹€.

// μ΄ˆκΈ°ν™” μ „
global.fetch = {
  calls: [['url']],
  instances: [undefined],
  invocationCallOrder: [2],
  results: [{ type: 'return', value: [Promise] }],
}

// μ΄ˆκΈ°ν™” ν›„
global.fetch = {
  calls: [],
  instances: [],
  invocationCallOrder: [],
  results: [],
}

πŸ₯– jest.mock()

μœ„μ—μ„œ fetch, axiosλ₯Ό mock ν•¨μˆ˜λ‘œ λ§Œλ“€μ–΄μ„œ μ‚¬μš©μ„ ν–ˆλ‹€. 즉, λͺ¨λ“ˆ 내뢀에 μžˆλŠ” λ©”μ†Œλ“œκ°€ μ œλŒ€λ‘œ μ •μ˜λ˜μ–΄μžˆμ§€ μ•Šκ±°λ‚˜ λͺ¨λ₯΄λ”라도 μ›ν•˜λŠ” 결과값을 μ§„ν–‰ν•˜μ—¬ μ΄ν›„μ˜ 과정을 ν…ŒμŠ€νŠΈ ν•  수 μžˆμ—ˆλ‹€.

ν•˜μ§€λ§Œ, λ§Œμ•½ λͺ¨λ“ˆ 내뢀에 μžˆλŠ” μ—¬λŸ¬κ°œ 의 λ©”μ†Œλ“œλ₯Ό mock ν•΄μ•Όν•œλ‹€λ©΄ jest.fn()을 μ—¬λŸ¬λ²ˆ λ°˜λ³΅ν•˜μ—¬ λ§Œλ“€μ–΄μ€˜μ•Ό ν•  것이닀.

이λ₯Ό 도와주기 μœ„ν•œ λ©”μ†Œλ“œκ°€ jest.mock(λͺ¨λ“ˆ)이닀. 인자둜 λ³΄λ‚΄μ§€λŠ” λͺ¨λ“ˆ 전체λ₯Ό mocking ν•¨μˆ˜λ‘œ λ§Œλ“€μ–΄μ£ΌλŠ”κ²ƒμ΄λ‹€.

jest.mock(axios), jset.mock(../utils.ts) ν•΄λ‹Ή λͺ¨λ“ˆ λ‚΄λΆ€μ˜ λͺ¨λ“  λ©”μ†Œλ“œκ°€ mock μΈμŠ€ν„΄μŠ€ν™” λœλ‹€.

이전에 axios.get = jest.fn()... λ°©μ‹μœΌλ‘œ mocking ν•œ 경우, axiosμ—μ„œ get μ†μ„±λ§Œ mock μΈμŠ€ν„΄μŠ€κ°€ λ˜μ—ˆλŠ”λ°, 이 λ°©μ‹μœΌλ‘œ ν•˜κ²Œλ  경우 axios λ‚΄λΆ€μ˜ λͺ¨λ“  속성듀이 λͺ¨λ‘ mock μΈμŠ€ν„΄μŠ€ν™” λœλ‹€.

axios = {
  delete: [Function: wrap] {
    _isMockFunction: true,
    getMockImplementation: [Function (anonymous)],
    mock: [Getter/Setter],
    mockClear: [Function (anonymous)],
    mockReset: [Function (anonymous)],
    mockRestore: [Function (anonymous)],
    mockReturnValueOnce: [Function (anonymous)],
    mockResolvedValueOnce: [Function (anonymous)],
    mockRejectedValueOnce: [Function (anonymous)],
    mockReturnValue: [Function (anonymous)],
    mockResolvedValue: [Function (anonymous)],
    mockRejectedValue: [Function (anonymous)],
    mockImplementationOnce: [Function (anonymous)],
    mockImplementation: [Function (anonymous)],
    mockReturnThis: [Function (anonymous)],
    mockName: [Function (anonymous)],
    getMockName: [Function (anonymous)]
  },
  get: [Function: wrap] {
    _isMockFunction: true,
    getMockImplementation: [Function (anonymous)],
    mock: [Getter/Setter],
    mockClear: [Function (anonymous)],
    mockReset: [Function (anonymous)],
    mockRestore: [Function (anonymous)],
    mockReturnValueOnce: [Function (anonymous)],
    mockResolvedValueOnce: [Function (anonymous)],
    mockRejectedValueOnce: [Function (anonymous)],
    mockReturnValue: [Function (anonymous)],
    mockResolvedValue: [Function (anonymous)],
    mockRejectedValue: [Function (anonymous)],
    mockImplementationOnce: [Function (anonymous)],
    mockImplementation: [Function (anonymous)],
    mockReturnThis: [Function (anonymous)],
    mockName: [Function (anonymous)],
    getMockName: [Function (anonymous)]
  },
}

πŸ₯¨ 이후

이전에 λ§Œλ“€μ—ˆλ˜ ν”„λ‘œμ νŠΈλ“€μ— ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό ν•œλ²ˆ μž…ν˜€λ³Ό 생각이닀. λ˜ν•œ, μ‹€μ œ μž‘μ—… 이전에 ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό λ¨Όμ € μž‘μ„±ν•˜κ³ , μ‹€νŒ¨λ˜λŠ” ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό ν•˜λ‚˜ν•˜λ‚˜ μ„±κ³΅μœΌλ‘œ λ§žμΆ°λ‚˜κ°€λŠ” TDD κ°œλ°œλ‘ μ— λŒ€ν•΄ ν•œλ²ˆ μ•Œμ•„λ΄μ•Όν•  것 κ°™λ‹€.

πŸ₯― μ°Έκ³ 


@SangMin
πŸ‘† H'e'story

πŸš€GitHub