본문 바로가기

프로젝트/나만의명언집

[나만의 명언집 프로젝트] 테스트 코드 적용 정리본(계속 추가중)

반응형

오늘의 명언

 

포스트의 목적과 참고 사항

프로젝트를 진행하면서 진행한 단위 테스트 코드를 정리하여, 추후 참고하기 위한 용도로서 정리한다. 테스트를 공부하면서 정리해 나가는 것이기 때문에, 테스트의 완성도는 보장할 수 없으며, 최대한 참고문서를 정리하며 정리할 것이기 때문에, 혹시나 이 포스트를 읽는 분이 있다면 참고문서(링크) 위주로 확인하면 도움이 될 것이라 생각된다. 

 

참고로 개발이 어느 정도 완료된 이후 테스트 코드를 추가 .


참고로, 모든 테스트의 흐름은...

다음 패턴을 따른다.

 

Arrange: 테스트 환경을 설정하고 테스트할 데이터 준비. 테스트할 객체를 생성하고 초기화하며, 테스트 환경을 적절하게 설정.

Act: 테스트 대상에 작용하는 작업을 수행.  테스트하려는 기능을 호출하거나 실행하고, 테스트 대상의 동작 유발.

Assert: 테스트 결과가 예상대로인지 확인. Act 단계에서 발생한 동작의 결과를 검증하고, 예상한 결과와 실제 결과가 일치하는지 확인.

 


next/navigation 을 모의하여 push 메소드가 정상적으로 호출되는지 테스트① | useRouter 의 push 메소드 모의

NextJS(^14.1+) 로 진행한 개인 프로젝트인 나만의 명언집 웹 사이트에 대한 단위 테스트 중  next/navigation 의 useRouter 를 모의하여 테스트하는 경우에 대한 정리이다. 해당 테스트에 사용된 테스트 러너 및 프레임워크는 Vitest 를 사용했고, 라이브러리로는 테스팅 라이브러리 리액트를 사용하였다. 

[1] 현재 테스트 하고자 하는 컴포넌트

(컴포넌트 설명 )

- 해당 컴포넌트의 역할은 특정 페이지에서 문제가 발생했을 때 대체하는 메시지를 보여주는 용도로 사용된다. 

- 사용자에게 의도한 화면을 렌더링하지 못하는 경우 대체되는 메시지 카드로 사용되며, '홈으로' 버튼을 클릭하면 '/' 경로로 이동하는 기능을 내포하고 있다.

- 그 외 모듈 간의 종속성이 없는 독립된 컴포넌트로서 여러 곳에서 재사용 가능하므로 단위테스트 대상으로 적용하기 좋은 케이스라고 판단 하였다.

'use client'

import { useRouter } from 'next/navigation'

export default function ReplaceMessageCard({
  childern,
}: {
  childern: React.ReactNode
}) {
  const router = useRouter()

  return (
    <h2
      className=" ~~"
    >
      {childern}
      <br />
      <button
        className="~~"
        onClick={() => {
          router.push('/')
        }}
      >
        홈으로
      </button>
    </h2>
  )
}

 

[2] 테스트 작성

(테스트 목적1) 해당 컴포넌트는 childeren prop 으로 태그나 텍스트를 상속받아 사용자에게 메시지로 보여주기 때문에, 해당 prop 이 제대로 화면에 표기되는지 테스트하고자 했다.

(테스트 목적2) button 을 클릭했을 때 정상적으로 useRouter 훅의 push 메소드가 동작하는지 테스트 하고자 하였다.

import { screen, render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
import ReplaceMessageCard from './ReplaceMessageCard'
import { afterAll, describe, expect, it, vi } from 'vitest'

// vi.hoisted 는 모든 import 보다 우선적으로 실행된다 즉 호이스팅되어 제일 먼저 사용된다.
// 반환된 useRouter() 의 push 메소드를 호출하면 mockRouterPush 가 호출된다.
const {useRouter, mockedRouterPush} = vi.hoisted(() => {
    const mockedRouterPush = vi.fn(); // useRouter의 push 메소드를 모의한다.
    return {
        useRouter : () =>({push: mockedRouterPush}),
        mockedRouterPush
    }
})

// next/navigation 모듈 중 사용하고자 하는 대상(여기서는 useRouter()의 모킹(대체)을 반환
vi.mock('next/navigation', async () => {
    const origin = await vi.importActual('next/navigation'); // next/navigation 모듈 전체를 import
    
    // 전체 모듈 중에서 useRouter 만 사용할 거니 useRouter 만 명시적으로 반환
    return {
        ...origin, 
        useRouter,
    }
})

describe('ReplaceMessageCard', () => {
    afterAll(() => {
        vi.clearAllMocks()
    })

    it('childeren prop 으로 전달된 "오류가 발생하였습니다." 텍스트가 노출된다.', () => {
        render(<ReplaceMessageCard childern='오류가 발생하였습니다.' />)

        const h2 = screen.getByRole('heading', { level: 2 })
        expect(h2.textContent).toContain('오류가 발생하였습니다.')
    })

    it('"홈으로" 버튼을 클릭하면, "/" 경로로 useRouter 훅의 push 함수가 호출된다.', async () => {
        const user = userEvent.setup();

        render(<ReplaceMessageCard childern='오류가 발생하였습니다.' />)
         
        // '홈으로' 텍스트 노드를 가진 버튼 요소
        const button = screen.getByText('홈으로')
 
        // 가상 JSDOM 에 대한 클릭 이벤트 호출
        await user.click(button);
         
         // toHaveBeenNthCalledWith(호출횟수, 경로)
        expect(mockedRouterPush).toHaveBeenNthCalledWith(1, '/')

    })

})

 

테스트 결과


next/navigation 을 모의하여 push 메소드가 정상적을 호출되는지 테스트 ② |  useRouter 의 인스턴스 자체를 모의

[1] 현재 테스트 하고자 하는 대상 : pageSwitch 함수

(함수 설명)

- pageSwitch 함수를 호출하면 전달되는 경로의 특정 id  값에 따라 다른 콘텐츠를 렌더링한다.

- 범용적인 이름에 비해서 특정 경로에 대한 param 만 다르게 하여 데이터를 렌더링하고 있다.

- true 를 반환하게 한 이유는 반환받은 값이 true 일 때 별도의 동작을 수행하도록 하기 위해서이다.

/**
 * 페이지 이동 함수
 * @param router next/navigation의 useRouter()
 * @param id 페이지 식별자
 * @returns 페이지 전환 유무를 반환
 */
export const pageSwitch = (router: AppRouterInstance, id: number) => {
  router.push(`/quotes-styler/auhtor/${id}`)

  return true
}

 

[2] 테스트 작성

(테스트 목적) 지정된 경로로 push 함수가 정상적으로 호출이 되는지 확인하고자 하였다.

import { describe ,expect,it, vi} from "vitest";
import { pageSwitch } from "./commonFunctions";
import {
    AppRouterInstance,
} from 'next/dist/shared/lib/app-router-context.shared-runtime';


const {useRouter } = vi.hoisted(()=>{
    const mockRouter:AppRouterInstance = {
        push: vi.fn(),
        back: vi.fn(),
        forward: vi.fn(),
        replace: vi.fn(),
        refresh: vi.fn(),
        prefetch: vi.fn(),
    }

    return {
        useRouter :() => ({mockRouter }),
        mockRouter
    }
})


vi.mock('next/navigation',async()=>{
    const origin = await vi.importActual('next/navigation')
    
    return {...origin, useRouter}
})

describe('pageSwisth 함수가 호출된다면', () => {
    
    it(' "/" 경로로 이동하고, result 는 true 를 반환해야 한다. ', () => {
        const router = useRouter().mockRouter
        const result = pageSwitch(router, 123)

        expect(router.push).toHaveBeenNthCalledWith(1,'/')
        expect(result).toBe(true)
    })

})

 

(테스트 설명)

- useRouter 훅은  'next/dist/shared/lib/app-router-context.shared-runtime'  에서 정의된 타입인 AppRouterInstance 를 적용받는 인스턴스를 반환하기 때문에, 이를 모의하기 위해 라우터의 인스턴스를 모의하였다.

const {useRouter } = vi.hoisted(()=>{
    const mockRouter:AppRouterInstance = {
        push: vi.fn(),
        back: vi.fn(),
        forward: vi.fn(),
        replace: vi.fn(),
        refresh: vi.fn(),
        prefetch: vi.fn(),
    }

    return {
        useRouter :() => ({mockRouter }),
        mockRouter
    }
})

 

 

- 그 후 pageSwith 함수에 전달된 router 의 push 메소드를 1번 호출 되었을 때 정상적으로 해당 경로를 렌더링하는지 테스트 하였다.

- (레드 테스트) 그러나 함수 내부에서 미리 정의되어 있는 경로의 형식과는 다르기 때문에 테스트는 실패한다.

describe('pageSwisth 함수가 호출된다면', () => {
    
    it(' "/" 경로로 이동하고, result 는 true 를 반환해야 한다. ', () => {
        const router = useRouter().mockRouter
        const result = pageSwitch(router, 123)

        expect(router.push).toHaveBeenNthCalledWith(1,'/')
        expect(result).toBe(true)
    })

})

 

- 지정한 경로와 예상된 경로가 다르므로 테스트가 실패하였다는 메시지가 반환된다.

 

 

 

- (그린 테스트) 실패한 이유를 확인하였으므로, 테스트가 성공할 수 있도록 경로를 수정하여 다시 테스트를 수행하면 테스트는 성공한다.

 

 

 

이번 테스트를 적용해보면서 느낀 테스트의 유용성

- 생각지도 않은 오타를 발견할 수 있다. 테스트를 적용하기 전에는 발견하지도 못했던 문제를 찾을 수 있다.

 

 

 

참고 문서

https://github.com/vercel/next.js/discussions/42527

https://vitest.dev/api/vi.html#vi-hoisted-0-31-0

https://testing-library.com/docs/user-event/intro/

https://testing-library.com/docs/queries/byrole


 debounce 함수의 비동기적 단위 테스트

[1] 현재 테스트 하고자 하는 대상 |  debounce 함수

(함수 설명)

- 일정 시간 간격 마다 함수가 호출 되는 디바운스 기술이 적용된 함수이다. 

- 일정 시간 이후 기존 상태를 새로운 상태로 업데이트한다.

- state 가 null 일 때, null이 아닐 때 서로 다른 동작을 수행한다.(이 부분은 향후 통합 테스트에서 진행 예정) 

- onChange 를 사용하여 잦은 변경이 발생하는 이벤트 핸들러에서 함수 호출 빈도를 조정할 때 사용된다.

// 디바운스
function debounce() {
  let timerId: NodeJS.Timeout

  return function (
    newValue: number,
    targetName: string,
    state: any,
    setState: any,
    delayTime: number,
  ) {
    clearTimeout(timerId)

    timerId = setTimeout(() => {
      if (state === null) {
        setState(newValue)
      }
      if (state !== null) {
        setState({ ...state, [targetName]: newValue })
      }
    }, delayTime)
  }
}

// 디바운스 함수 호출 : debonce 함수의 클로저 반환하고 이를 활용.
/**
 * @argument newValue 새롭게 업데이트할 상태 값
 * @argument targetName 상태값의 타입 (타입이 필요 없으면 빈문자열<''> 을 전달한다.)
 * @argument state 기존 상태(기존상태가 필요없으면 null 을 전달한다.)
 * @argument setState 상태를 업데이트하는 함수
 * @argument delayTime 지연시간
 * @returns
 * @example  예를들어, debounceCloser(50, 'height',size, setSize,300) 와 같이 호출한다.
 */
export const debounceCloser = debounce()

 

[2] 테스트 코드 작성

(테스트 목적)

- 일정 시간 후에 정상적으로 함수가 호출되는지 테스트하고자 하였다.

- 해당 함수는 여러 컴포넌트에서 재사용되기 때문에, 향후 통합 테스트를 진행할 때 주요 로직의 변경 여부를 살펴볼 예정이다. 함수 단위의 단위 테스트에서는 매개변수를 전달했을 때, 정상적으로 함수가 호출되는지 검증하고자 한다.

 

(레드 테스트) 만일 500ms 가 지나기 전에 함수가 호출되는지 측정하기 위해 vi.advanceTimerByTIme(400) 을 호출하여 모킹 타이머가 400ms 가 지난 직후 호출되는지 테스트하였다.

describe('debunce function', () => {
    beforeEach(() => {
        vi.useFakeTimers(); // 각 테스트 실행 전에 타이머를 모킹한다(vitest 에게 타이머 모킹을 적용할 것임을 알림 ).
    })

    afterEach(() => {
        vi.useRealTimers(); // 각 테스트 마다 타이머 모킹을 실행 전 상태로 초기화 한다.
    })

    vi.useFakeTimers(); // 타이머 모킹(이후 useRealTimers 호출 전 까지 모든 타이퍼를 래핑)

    it('특정 시간이 지나면, 함수가 호출된다.', () => {
        const spySetState = vi.fn()
        debounceCloser(42, 'width', null, spySetState, 500)

        vi.advanceTimersByTime(400)
        expect(spySetState).toHaveBeenCalled()
    })
})

 

 

적어도 한 번은 스파이 함수가 호출되어야 함에도 호출되지 않았다고 에러가 뜬다.

 

 

 

(그린 테스트) 그 후 500ms 가 전달되었을 때 모킹 타이머가 정상적으로 호출되는지 체크 하였다.

    it('특정 시간이 지나면, 함수가 호출된다.', () => {
        const spySetState = vi.fn()
        debounceCloser(42, 'width', null, spySetState, 500)

        vi.advanceTimersByTime(500)
        expect(spySetState).toHaveBeenCalled()
    })

 

 

의도한 대로 500ms 이후에 함수가 호출되어 정상적으로 테스트를 통과하였다.

 

 

 

타이머를 활용한 비동기 테스트 시 주의점

하나의 타이머 모킹에 대한 테스트가 완료되고 난 후에는 vi.useRealTimers() 를  호출해야 한다. 해당 메서드는 이전에 구현된 모든 가짜 타이머(useFakeTimers 로 랩핑되어 있는 모든 타이머)가 실행되기 전으로 초기화시켜주므로, 이전 테스트의 모킹 타이머가 다음 테스트에 영향을 미치는 것을 방지한다.

 

참고 메서드

vi.useFakeTimers() ;
모의 타이머를 활성화 해준다. vi.useRealTimers() 가 호출될 때 까지 타이머에 대한 모든 추가 호출을 래핑한다.
vi.useRealTimers();
이전에 구현된 모든 가짜 타이머(useFakeTimers 로 랩핑되어 있는 모든 타이머)가 실행되기 전으로 초기화된다.
vi.advanceTimersByTime(number ms)
시작된 모든 타이머들을 지정된 밀리초가 지나거나 대기열이 비어 있을 때까지 호출한다.
vi.isFakeTImers()
가짜 타이머를 활성화할 수 있다면 true를 반환

 

 

[커스텀 훅]  사용자 정의 hooks 단위 테스트

① 테스트 대상 | useHasToken()

커스텀 훅 설명

사용자의 로그인 유무 판단 및 접근 토큰이 존재하는지 유무를 검증하여  api 요청 등에 사용하기 위해 만든 커스텀 훅이다. 해당 훅을 호출하는 경우 sessionStorage 에 토큰 정보가 존재한다면 true 를 반환하고, 그렇지 않으면 false 을 반환한다.

'use client'
import { useEffect, useState } from 'react'

export default function useHasToken() {
  const [validToken, setValidToken] = useState(false)

  useEffect(() => {
    if (sessionStorage.getItem('token')) {
      setValidToken(true)
    }

    if (!sessionStorage.getItem('token')) {
      setValidToken(false)
    }
  }, [])

  return validToken
}

테스트 코드 작성

(테스트 목적)

- sessionStorage 에서 정상적으로 토큰의 존재 유무가 검증되고 있는지 테스트 하고자 하였다. 

 

(테스트 환경 설정)

해당 테스트에서는 renderHook 을 사용한다. renderHook 은 리액트 테스팅 라이브러리 에서 제공하는 훅으로 실제 클라이언트 컴포넌트 내에서 커스텀 훅을 호출할 필요 없이 테스트 환경 내에서  가상으로 훅을 호출할 수 있도록 해준다.

 

 

(테스트 1) 토큰이 존재하는 경우 true 를 반환하는가?

만일 세션 스토로지에 토큰이 존재하는 경우 true을 반환하는지 간단하게 테스트 한다. 간단한 훅이지만, 해당 단위테스트를 진행하는 이유는 간혹 session 스토로지를 인지 못하고 에러를 던지는 경우가 있었는데, 해당 문제가 개선된 이후에 동일한 에러가 발생할 수 있는 상황을 체크하기 위해서이다.

 

실제 sessionStorage 의 setItem 메소드를 사용하여 토큰을 저장하고, renderHook 을 사용하여 가상의 돔 환경에서 useHasHook 을 호출한다.

 

그리고 useHasHook 이 return 하는 값을 result 객체에 담기는데 result.current 로 접근하면 해당 커스텀 훅이 반환하는 값에 접근할 수 있다.

    it('sessionStorage 에 토큰이 존재한다면, validToken 의 상태가 true로 설정된다. ', () => {
        sessionStorage.setItem('token', 'Token') // 가상의 토큰 데이터 

        const { result } = renderHook(() => useHasToken())

		// result.current => {current: boolean}
        expect(result.current).toBeTruthy()
    })

 

 

이렇게 테스트 코드를 작성하고 테스트를 실행하면 성공한다.

 

 

(테스트 2) 토큰이 삭제되어 존재하지 않는다면, false 를 반환하는가?

만일 토큰이 만료되어 삭제되는 경우 혹은 로그인 상태가 아닌 경우에 false 를 정상적으로 반환하는지 테스트하고자 하였다. 테스트 구조는 테스트 1과 동일하며, 차이점이 있다면 sessionStorage 의 removeItem 메서드를 사용하여 기존 token 값을 삭제해 주었다.

 

참고로, 테스트 1 에서 지정한 setItem 의 값은 테스트2의 결과에도 영향을 준다. 만일 각각의 독립된 테스트를 실행한다면 각 테스트 실행 전이나 후에 초기화시켜 주어야 하지만, 해당 테스트는 연계되어 진행되어도 상관없었기에 그대로 진행하였다.

describe('useHasToken', () => {

    it('sessionStorage 에 토큰이 존재하지 않는다면, validToken 의 상태는 false 로 설정된다.', () => {
        sessionStorage.removeItem('token')
        
        const { result } = renderHook(() => useHasToken())

        expect(result.current).toBeFalsy()
    })

})

 

복잡한 테스트가 아니기에 큰 문제 없이 나머지 테스트도 성공한 것을 볼 수 있다. 즉, 정상적으로 세션 스토로지에 아이템이 삭제되었고, 삭제되어 존재하지 않는 경우에는 예상한 값인 false 을 반환한 것이 증명되었다.

 

 

참고 메서드

- renderHook https://testing-library.com/docs/react-testing-library/api/#renderhook

 

테스트 대상 | useTTS() 

커스텀 훅 설명

명언 내용(text)을 useTTS 훅에서 반환하는 setText() 메서드로 설정한다. 이 때, 설정된  text 는 speakText () 의 매개변수로 전달되고, 해당 text 에 대한 TTS 를 생성하여 음성을 재생한다. 즉, 텍스트가 합성된 음성으로 재생되도록 하는 커스텀 훅이다.

'use client'
import { useCallback, useEffect, useState } from 'react'

/**
 * TTS API 를 호출하는 커스텀 훅
 * @example
 * const [setText] = useTTL();
 * <button onClick={()=> setText('듣기할 텍스트를 전달하면 됩니다.')}>
 */

export default function useTTS() {
  const [text, setText] = useState('')

  const speakText = useCallback((text: string) => {
    const synth = window.speechSynthesis 
    if (synth !== null && text.length > 1) {
      const utterance = new SpeechSynthesisUtterance(text)
      synth.speak(utterance)
    }
  }, [])

  useEffect(() => {
    // if(window['speechSynthesis'] === undefined) return
    speakText(text)
  }, [text, speakText])

  return {text, setText}
}

 

테스트 코드 작성

(테스트의 목적)

- useTTS 훅을 호출하는 경우 setText 메서드가 정상적으로 text 상태를 업데이트 하는지 테스트하고자 하였다.

- 그리고 text 가 speakText 함수에 전달되고, 해당 함수가 정상적으로 호출되는지 까지도 검증하고 한다.

 

(태스트 환경 설정)

vi.resetAllmocks() 메서드를 모든 테스트가 완료된 이후에 한 번 호출하여 다른 단위테스트에 영향을 미치지 않도록 정리한다.

 

이번에는 추가적으로 act ()  메서드를 사용한다. 사용하는 이유는 React 컴포넌트의 라이프사이클 메서드, useEffect 훅 등과 같은 비동기 작업이 있는 부분은 jest  나 vitest 가 해당 작업이 완료될 때까지 기다리지 않고 테스트를 종료할 수 있는데, . 이러한 경우에 act 함수를 사용하여 해당 작업을 동기적으로 수행하여 테스트가 정확하게 실행되도록 보장할 수 있다.

 

renderHook() 메서드를 사용한다. 이 메서드를 사용하면, 사용자가 정의한 커스텀 훅을 실제 컴포넌트에서 실행하지 않고도 가상의 환경에서 실제 환경과 유사하게 커스텀 훅을 호출할 수 있다.

 

(테스트 ①) setText 가 정상적으로 호출될 시 text 에 임의의 값이 할당되는가?

useTTS 는 객체 리터럴 형태로 {text, setText } 를 반환한다. 따라서 사용자가 TTS 버튼을 클릭하면, 임의의 텍스트가 setText(텍스트)  로 호출되고, 커스텀 훅 내부적으로 기존 text의 상태를 전달받은 값으로 업데이트 한다. 

 

즉, 테스트 ① 에서는 useTTS 가 정상적인 setText 메서드를 반환하는지, 해당 메서드가 정상적으로 값을 업데이트하는지를 검증한다.

 

    it('setText() 가 정상적으로 호출된다면, text 에 임의의 텍스트가 할당된다. ', () => {
        const { result } = renderHook(useTTS)

       // result.current 는 내부적으로{ text: '가는 말이 고와야 오늘 말도 곱다.', setText: [Function: bound dispatchSetState] } 를 가진다.
       // act 내부에 있는 함수가 종료될 때 까지 테스트는 종료되지 않고 대기한다.
        act(() => {
            result.current.setText('가는 말이 고와야 오늘 말도 곱다.')
        })
     
        expect(result.current.text).toBe('가는 말이 고와야 오늘 말도 곱다.')
    })

 

여기서 act 메서드에 전달된 콜백함수가 실행되기 전까지 vitest 는 expect 를 호출하지 않고 대기한다. 만약에 act 메서드가 없이  result.current.setText('가는 말이 고와야 오늘 말도 곱다.') 만 호출한다면, vitest 는 해당 메서드의 동작이 완료할 때 까지 기다리지 않고 테스트를 종료시킨다.

 

해당 테스트를 실행하면, 테스트가 성공하는 것을 확인할 수 있다. 

 

 

(테스트② ) setText 가 호출되고 나서, TTS API 도 호출되는지?

useTTS() 훅은 내부적으로 speakText 함수가 useEffect 에 의해 호출되고 있는데, 이 때 매개변수로 text 를 전달받는다. 그리고 내부적으로는  브라우저에서 제공하는  window.speechSynthesis 을 사용하고 있다. 하지만 해당 브라우저의 메서드는 외부에 의존성을 가지기 때문에, 이에 대한 모의 객체를 생성하여 해당 함수의 호출을 흉내 내어 호출 여부를 테스트할 수 있도록 해야 한다.

 

이제, 테스트에 앞서 우선적으로 함수 호출 여부 판별에 필요한 speakText 함수의 모의 함수인 mockSpeakTextSpy 함수를 생성한다.

     const mockSpeakTextSpy = vi.fn()

 

 

그리고 해당 모의함수의 호출과 함께 window.speechSynthesis 객체를 모의하여,  speechSynthesis 의 동작을 모의한다.

 

이 때 주의할 점은 NodeJS 환경에서 window 객체에 접근할 수 없기 때문에이른 반환을 사용하여 undefined 가 아닌 경우에만 모의 객체에 접근할 수 있도록 처리해야 한다.

 

그리고 앞서 모의 함수(mockSpeakTextSpy) 를 speak 객체에 할당하여 speak 메서드가 정상적으로 호출되는지에 대해서만 모의하도록 설정한다.

        if(window['speechSynthesis'] === undefined) return
        window.speechSynthesis = {
            speak: mockSpeakTextSpy,
            onvoiceschanged: vi.fn(),
            paused: false,
            pending: false,
            speaking: false,
            cancel: vi.fn(),
            getVoices: vi.fn(),
            pause: vi.fn(),
            resume: vi.fn(),
            addEventListener,
            removeEventListener, 
            dispatchEvent
        }

 

 

앞서 모의 대상을 한정한 이유

 

 

speechSynthesis 가 어떤 프로퍼티를 상속받고 있는지 확인하고자 한다면, 아래 영상을 참고하자( 참고로, 윈도우 환경에서 Ctrl + 마우스 클릭 으로 링크 이동이 가능하다.)

 

 

그 후 테스트 ① 에서 동일한 형태로 코드를 작성한다. 아래는 위에서 설명한 코드를 포함한 전체 코드이다.

    it('setText 함수를 호출할 때 TTS API가 호출되는지 확인한다.', () => {
        const mockSpeakTextSpy = vi.fn()

        if(window['speechSynthesis'] === undefined) return
        window.speechSynthesis = {
            speak: mockSpeakTextSpy,
            onvoiceschanged: vi.fn(),
            paused: false,
            pending: false,
            speaking: false,
            cancel: vi.fn(),
            getVoices: vi.fn(),
            pause: vi.fn(),
            resume: vi.fn(),
            addEventListener,
            removeEventListener, 
            dispatchEvent
        }

        const { result } = renderHook(() => useTTS())

        act(() => {
            result.current.setText('테스트할 텍스트')
        })

        expect(mockSpeakTextSpy).toHaveBeenCalledWith('테스트할 텍스트')
    })

 

테스트 결과 정상적으로 함수가 호출되는 것을 확인할 수 있었다.

 

반응형