Unit Test: 비동기 테스트

비동기 테스트

  • example.js

    export function asynFn() {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve('Done!')
        }, 2000)
      })
    }
    

    실제 주는 값: Done!

example.test.js

  • 비정상적인 테스트 결과

    import { asynFn } from './example';
      
    describe('비동기 테스트', () => {
      test('done', () => {
        asynFn().then(res => {
          expect(res).toBe('Done?')
        })
      })
    })
    

    일반적으로 우리가 아는 비동기 패턴으로 테스트를 했을 때, 테스트가 정상적으로 동작하지 않는다. 실제 주는 값은 Done!이고 기대되는 값은 Done?인데 테스트가 패스됐다. 즉, 테스트가 정상적으로 동작하지 않는 것이다.

    특정한 test 내부에 비동기 코드를 작성했는데, 해당 내용을 테스트 입장에서는 얼마나 기다려야 하는지 알수가 없기 때문에 기다리지 않고 바로 테스트를 진행해서 올바르지 않음에도 테스트가 패스되었다.

  • 정상적인 비동기 코드: done

    import { asynFn } from './example';
      
    describe('비동기 테스트', () => {
      test('done', (done) => {
        asynFn().then(res => {
          expect(res).toBe('Done?')
          done()
        })
      })
    })
    

    비동기 테스트를 진행할 수 있는 매개변수 done을 사용한다.
    done은 비동기 테스트가 언제 종료되는지 명시해준다. 마치 함수처럼 실행할 수 있다.
    test의 콜백의 done이라는 매개변수를 비동기 코드가 종료되는 다음 부분에 함수처럼 작성해준다. 해당 test가 done 매개변수가 호출돼서 동작하기 전까지는 무작정 기다리게 된다. asynFn 함수가 2초뒤에 실행이 되고 그 다음에 done 매개변수가 실행되면서 test가 종료된다.

  • 정상적인 비동기 코드: then

    test('then', () => {
        return asynFn().then(res => {
          expect(res).toBe('Done?')
        })
      })
    

    하나의 비동기 코드를 return키워드로 반환해준다. 반환해주게 되면서 해당 코드 부분들이 test에서는 비동기로 동작하게 된다는 것을 인지하게 된다.
    해당 비동기 코드가 동작할때까지 기다리고 test를 동작시킨다.

  • 정상적인 비동기 코드: resolves

    test('resolves', () => {
        return expect(asyncFn()).resolves.toBe('Done!')
      })
    

    expect 안에 asyncFn 비동기 함수를 실행해서 여기서 나온 값을 바로 toBe로 비교를 한다.
    expect와 toBe 사이에 resolves 브릿지를 추가한다. 해당 브릿지로 비동기코드가 완료될때까지 기다리게 해준다.

    return 없이 사용해도 되지만, 그렇게 되면 테스트 환경에서 얼마나 기다려야하는지 알 수가 없기 때문에 콜백 밖으로 반환해준다.

  • 정상적인 비동기 코드: async/await

    test('async/await', async () => {
        const res = await asyncFn()
        expect(res).toBe('Done!')
      })
    

    async/await를 사용하여 비동기 코드를 작성한다.
    비동기 함수 앞에 await를 작성해주고 await 키워드가 붙어있는 함수 부분에 async를 붙여준다.

    await 키워드가 있는 부분에서 resolve의 값이 반환된다. 해당 값을 res 변수에 넣는다. res를 expect의 인수로 받아서 사용한다.

    async와 await를 사용해서 비동기 코드를 작성하는 것을 가장 추천한다.

  • test의 세번째 인수

    만약 비동기 함수를 2초가 아닌 6초를 기다리게 한 후에 작동하게 만든다면 어떤 일이 발생할까? 테스트 fail이 발생한다.
    테스트는 최대 5초까지만 가능하다. 따라서 5초가 되어도 테스트가 종료되지 않았기에 test fail이 발생한 것이다.

    그렇다면, 6초 뒤에 동작하게는 어떻게 할까? test의 세번째 인수를 추가해준다.

    test('async/await', async () => {
      const res = await asyncFn()
      expect(res).toBe('Done!')
    },7000)
    

    세번째 인수에는 우리에게는 안 보이게 기본값으로 5000(5초)가 설정되어있다. 따라서 세번째 인수에 원하는 숫자를 작성하면 해당 시간만큼 더 기다릴 수 있게 된다.

Unit Test: Jest Globals, Matcher

첫 테스트


  • package.json

    "scripts": {
      "dev": "webpack-dev-server --mode development",
      "build": "webpack --mode production",
      "lint": "eslint --fix --ext .js,.vue",
      "test:unit": "jest --watchAll"
    }
    

    test 스크립트를 추가한다. 단, 우리가 현재 진행할 테스트는 unit 테스트이기에 뒤에 unit 테스트임을 명시한다.

    jest 명령을 사용하는데 테스트가 감시돼서 반복적으로 사용될 수 있도록 한다.
    모든 테스트 파일의 변경사항을 감시해서 변경사항 발생시, 테스트 환경을 재동작 시킨다.

  • test 실행

    npm run test:unit
    

    테스트가 통과됐을 때, 나타나는 화면

    테스트 통과

    테스트가 실패할 때, 나타나는 화면

    테스트 실패

    expect는 실제 받는 값(Received), toBe는 받을 것으로 기대되는 값(Expected)이다.
    기대되는 값은 문자 123인데, 실제 받은 값은 숫자 123이기에, 일치하지 않아서 테스트가 실패한 것이다.

Jest Globals


Globals · Jest

Globals Methods

import { double } from './example'

describe('Group1', () => {
  beforeAll(() => {
    console.log('beforeAll!')
  })
  afterAll(() => {
    console.log('afterAll!')
  })

  beforeEach(() => {
    console.log('beforeEach!')
  })
  afterEach(() => {
    console.log('afterEach!')
  })

  test('첫 테스트', () => {
    console.log('첫 테스트!')
    expect(123).toBe(123)
  })
  
  test('인수가 숫자 데이터입니다', () => {
    console.log('인수가 숫자 데이터입니다!')
    expect(double(3)).toBe(6)
    expect(double(10)).toBe(20)
  })
  
  test('인수가 없습니다', () => {
    console.log('인수가 없습니다!')
    expect(double()).toBe(0)
  })
})
  • test()

    우리가 만들어내는 각각의 테스트를 구분해주는 용도로 사용한다.

    첫번째 인수로는 테스트의 이름, 두번째 인수로는 콜백 함수를 작성하여 콜백 내부에서 실제 테스트를 진행한다.

  • describe()

    여러개의 test 함수들을 describe 함수로 묶을 수 있다. 일종의 test 그룹이라고 생각하면 된다.

    첫번째 인수로는 해당 그룹의 이름, 두번째 인수로는 콜백함수를 작성하여 내부에는 여러가지 test 함수들을 넣어줄 수 있다.

    그룹으로 묶는 이유는 before나 after 전역함수를 사용하기 위함이다.

  • beforeAll() afterAll()

    before, after를 가지는 전역함수로는 별도의 이름(첫번째 인수)을 작성해주지 않는다. 대신 첫번째 인수로 콜백을 명시한다.

    • beforeAll()

      그룹안에 있는 모든 테스트가 실행하기 전에, 단 한번만 실행이 된다.

    • afterAll()

      그룹안에 있는 모든 테스트가 실행된 후에, 단 한번만 실행이 된다.

  • beforeEach() afterEach()

    • beforeEach()

      각각의 테스트가 동작하기 직전에, 해당 메소드가 한 번 실행된다.

      테스트가 3개라면, 각 테스트가 동작하기 전에 실행되므로 총 3번 실행된다.

    • afterEach()

      각각의 테스트가 동작한 직후에, 해당 메소드가 한 번 실행된다.

      테스트가 3개라면, 각 테스트가 동작한 후에 실행되므로 총 3번 실행된다.

Jest Matcher 이해


Expect · Jest

Expect Methods

기대값과 실제 주어진 값을 비교해주는 메소드들을 Matcher라고 부른다. 한글로는 일치도구라고 할 수 있다.

const userA = {
  name: 'Soha',
  age: 39
}
const userB = {
  name: 'Momo',
  age: 23
}

test('데이터가 일치해야 합니다', () => {
  expect(userA.age).toBe(39)
  expect(userA).toEqual({
    name: 'Soha',
    age: 39
  })
})

test('데이터가 일치하지 않아야 합니다', () => {
  expect(userB.name).not.toBe('Soha')
  expect(userB).not.toBe(userA)
})
  • .toBe()

    값이 원시형데이터(string, number, boolean 등)인 것을 비교한다.

    expect와 함께 사용되며, expect의 인수에는 실제 받은 값, toBe의 인수에는 기대되는 값을 작성한다.

    두 값이 일치하는지 비교하고 일치하면 pass, 일치하지 않으면 fail이 뜬다.

  • .toEqual()

    참조형 데이터(객체, 배열데이터 등)를 비교할 때 사용한다.
    데이터가 가지는 실제 값만 동일한지 확인 할때 사용한다. 실제 객체 데이터, 배열 데이터의 내부적인 구조만 비교해서 같은지 비교하는 것이다.

    사용 방법은 toBe와 동일하다.

  • .not

    expect 함수 뒤에 not 속성을 추가하여 기대값을 일치시키는 메소드들의 반대값을 명시할 수 있다.

    중간에 not이라는 키워드를 사용해서 부정된 결과를 확인할 수 있다.
    expect(can1).not.toBe(can2)

Unit Test: 테스트 환경 설정

  • 패키지 설치

    npm i -D jest @vue/test-utils@next vue-jest@next babel-jest
    
  • jest.config.js

    module.exports = {
      moduleFileExtensions: [
        'js',
        'vue'
      ],
      moduleNameMapper: {
        '^~/(.*)$': '<rootDir>/src/$1'
      },
      modulePathIgnorePatterns: [
        '<rootDir>/node_modules',
        '<rootDir>/dist'
      ],
      testURL: 'http://localhost/',
      transform: {
        '^.+\\.vue$': 'vue-jest',
        '^.+\\.js$': 'babel-jest'
      }
    }
    
    • moduleFileExtensions

      파일 확장자를 지정하지 않은 경우 Jest가 검색할 확장자 목록이다.
      일반적으로 많이 사용되는 모듈의 확장자를 지정한다.

    • moduleNameMapper

      틸드(~) 같은 경로 별칭을 매핑한다.
      <rootDir> 토큰을 사용해 루토 경로를 참조할 수 있다.

      '^~/(.*)$': '<rootDir>/src/$1’
      틸드(~/)의 슬래쉬로 시작하는 경로별칭은 뒤쪽에 있는 값으로 매핑한다. 뒤쪽의 값은 <rootDir>은 무시하고 src 디렉토리 안에 있는 모든 경로($1)를 매핑한다는 의미이다.

    • modulePathIgnorePatterns

      테스트 환경에서 무시할 경로들을 불러온다. 즉, 테스틀 하지 않는 것들이다.

    • testURL

      jsdom은 html 환경이라고 이해하면 된다. 환경에서 문제가 생기지 않도록 localhost의 주소를 명시하여 해결한다.

    • transform

      정규 표현식을 통해서 vue,js 확장자를 가지는 파일을 발견하면 테스트 환경에서 동작할 수 있는 새로운 코드로 변환해준다.
      vue 확장자는 vue-jest 패키지를 통해 변환, js 확장자는 babel-jest 패키지를 통해 변환한다.

  • example.test.js

    해당 파일에 test() 코드를 작성해준다. 그러면 에러가 발생한다.
    해당 함수는 전역이라 오류가 발생하면 안되는데 eslint로 인해 에러가 발생한다.

    • .eslintrc.js 수정

      env: {
        browser: true,
        node: true,
        jest: true
      }
      

      eslint를 통해서 jest라는 테스트 환경에서 기본적인 문법들이 lint 에러가 발생하지 않도록 옵션을 추가해준 것이다.

      해당 수정으로 인해 test 전역 함수의 에러가 발생하지 않는다.

테스트 개요

Unit Test


단위(Unit) 테스트란 데이터(상태), 함수(메소드), 컴포넌트 등의 정의된 프로그램 최소 단위들이 독립적으로 정상 동작하는지 확인하는 방법이다.

작성한 로직이 정상적으로 동작하는지 코드 위주로 테스트를 진행한다. 비교적 가볍고 빠르고 단순하게 테스트를 진행하는 것을 선호하여 코드를 작성한다.

사용하는 프레임워크 및 라이브러리: Jest, Vue Test Utils

E2E Test

E2E(End to End) 테스트란 애플리케이션의 처음부터 끝까지의 실제 사용자의 관점에서 사용 흐름을 테스트하는 방법이다.

실제 브라우저 화면에서 만든 애플리케이션(사이트)를 직접 사용하면서 테스트를 진행한다. 화면에서 값을 입력해보거나 직접 버튼을 클릭하면서 정상 작동하는지 확인한다.

사용하는 도구: Cypress

SPA 개요

  • Traditional Web Application

    Traditional Web Application

    패스트 캠퍼스 강의 중 사진 발췌

    클라이언트에서 최초 페이지를 요청하면 서버가 요청한 페이지에 해당하는 html을 클라이언트에게 보내주는 방식이다.

    클라이언트가 about이라는 새로운 페이지를 요청하면 서버는 이에 해당하는 html 파일을 클라이언트에게 보내준다.

  • SPA (Single Page Application)

    SPA

    패스트 캠퍼스 강의 중 사진 발췌

    클라이언트가 최초의 페이지를 요청하면 서버는 해당하는 html 파일을 보내주는 것까지는 위와 동일하다.

    클라이언트가 about 페이지에 접속하게 되면 SPA는 별도의 페이지를 요청하는 것이 아닌 about 페이지에서 기존(최초)의 페이지와 다른 부분을 서버에 요청한다. 이것을 AJAX 요청이라고 한다.
    서버는 클라이언트에게 여러가지 데이터를 내어줄 수 있겠고, 클라이언트의 컴퓨터에서는 처음에 떴던 최초 페이지에서 about 페이지로 달라졌을 때 새롭게 랜더링을 해야 하는 데이터들만 다시 화면에 그려준다.
    따라서 페이지 로드 없이 콘텐츠만 새로고침 된다.

    SPA는 전통적인 web 애플리케이션과 다르게 페이지가 변경되더라도 동일한 컨텐츠는 다시 불러 올 필요가 없다는 장점이 있다.

  • SPA 장점
    • 빠르고 자연스러운 전환으로 훌륭한 사용자 경험 제공
    • 서버에 더 적게 요청해 빠르게 랜더링 가능
    • 컴포넌트 단위 개발로 생산성 향상
    • 쉬운 분업화
  • SPA 단점

    • 최초 페이지 로드 느림
      단 하나의 페이지(index.html)만 사용하기 때문에 가져와야 할 데이터가 많아서, 최초 로드가 느리다.
      ⇒ Lazy loading(사용자에게 당장 보이는 페이지 먼저 로딩), 브라우저 캐싱(필요한 부분을 저장후, 사용자에게 추후에 보여줌)

    • 어려운 검색 엔진 최적화(SEO)
      세부적으로 분리된 페이지에 대한 상세 내용을 검색엔진이 가져가기에는 구조적으로 조금 문제가 있다.
      ⇒ SSR(Server Side Randering이 가능한 방식으로 수정/보완), Serverless Functions을 사용하여 보완

    • 모든 데이터 노출
      모든 파일이 사용자에게 넘어가지기 때문에 중요 내용 또한 넘어가게 된다.
      ⇒ 비즈니스 로직 최소화 (최대한 프론트엔드에서 비즈니스 로직을 작성하지 않도록 주의해야 함)