01.Unit Test (단위 테스트)

Updated:


1.테스트 개요

Unit Test

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

  • 즉 주관적으로 정의된 것에 대한 하나하나를 테스트 하는 개념임.

  • 나중에 수정할 부분이 있으면 테스트 한 항목을 기반으로 더 안정적으로 수정을 할 수 있습니다.

  • 작성한 로직이 코드위주로 테스트를 하기 때문에 가볍고, 더 빠르게 테스트를 진행 할 수 있습니다.

  • 대표적인 Unit Test tool 로는 Jest 가 있습니다. (JavaScript test 용)

E2E Test

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

  • 실제 브라우저 화면에서 만들어진 사이트를 직접 사용하면서 사용하는 테스트입니다.

  • 대표적인 E2E test tool 로는 cypress 가 있습니다.

2.테스트 환경설정

  • 예제 App test 파일로는 Jacob’s DevLog 의 portfolio 에 있는 Book-Search App 을 통해 진행 하겠습니다. (Vue.js 기반)

  • Vue.js 에서 unit test 를 진행하기 위해선 아래와 같은 package 를 설치 해야 합니다.

    • jest

    • vue test-utils

    • vue-jest

    • babel-jest

  • package 설치

$ npm i -D jest @vue/test-utils@next vue-jest@next babel-jest

  • root 경로에 jest.config.js 파일 생성 -> 테스트 할 환경설정 설정
// in jest.config.js

module.exports = {
  // 파일 확장자를 지정하지 않은 경우, Jest 가 검색할 확장자 목록입니다.
  // 일반적으로 많이 사용되는 모듈의 확장자를 지정합니다.
  // e.g `import HellowWorld from '~/components/HelloWorld';`
  moduleFileExtensions: [
    'js',
    'vue'
  ],

  // `~` 와 같은 경로 별칭을 매핑합니다.
  // `< rootDir>` 토큰을 사용해 루트 경로를 참조할 수 있습니다.
  moduleNameMapper: {
    '^~/(.*)$': '<rootDir>/src/$1' // regexp 로 ~/ 로 시작되는 모든 문자열은 root 경로의 src 폴더안의 모든 경로를 말함
  },

  // 일치하는 경로에서는 모듈을 가져오지 않습니다.
  // `<rootDir>` 토큰을 사용해 루트 경로를 참조할 수 있습니다.
  modulePathIgnorePatterns: [
    '<rootDir>/node_modules',
    '<rootDir>/dist'
  ],

  // jsdom 환경에 대한 URL 을 설정합니다.
  // https://github.com/facebook/jest/issues/6766
  testURL: 'https://localhost',

  // 정규식과 일치하는 파일의 변환 모듈을 지정합니다.
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.js$': 'babel-jest'
  }
}

  • eslint 가 설치 되어 있으면 test 시 충돌 할 수 있기 때문에 사전에 정의된 전역 변수를 미리 설정 해주어야 합니다. root 경로에 있는 .eslintrc.js 에서..
// in .eslintrc.js
module.exports = {
  // 사전에 정의된 전역 변수 설정
  env: {
    browser: true,
    node: true,
    jest: true
  }

3.초기 테스트 실행

  • 예를 들어 하나의 js 파일을 test 하기 위해선 데이터(함수) 등을 export 할 수 있는 통로를 만들어 줘야지 test 작동 파일로 데이터를 내보내야 합니다. 다음과 같이 export 를 해주어야 합니다.
// in example.js

export function double(num) {
  return num * 2
}
  • 즉, vue 나 js 파일안에 있는 파일의 데이터나, 함수는 export 를 하지 않으면 결국 테스트를 할 수 없습니다.

  • script 상에서 jest 를 사용할 수 있게 packge.json 파일에서 script 등록을 다음과 같이 해줍니다.

  "scripts": {
    "test:unit": "jest --watchAl" // watchAll flag  변경사항이 발생  경우 자동으로 감지해서 test 환경을 다시한번 동작 시키는 flag 입니다
  }
  • test 할 부분 작성
// in example.test.js

import { expect, test } from '@jest/globals'
import { double } from './example'

test('Sample test', () => {
  expect(123).toBe(123)
})
  • terminal 에서 $ npm run test:unit 을 실행하면 다음과 같음 같은 메세지가 출력됩니다.

image

  • 만약 test 에서 fail 할 경우 다음과 같이 출력 됩니다.
// in example.test.js

import { expect, test } from '@jest/globals'
import { double } from './example'

test('Sample test', () => {// expect 는 실제 들어오는 값이고, tobe 는 함수 실행 시, 출력이 예상되는 값을 가리킴 
  expect(123).toBe('123') // 일부로 123을 str 로 변환해서 test 진행 -> fail 유도
})

image

  • received 는 받은 값을 의미하고, expected 는 기대되는 값을 가리키는데 두개의 값이 일치 하지 않기 때문에 test 에서 fail 이 나타나게 됨

  • 마찬가지로 다른 test 를 실행합니다. 일단 tobe() 에 임의의 값을 입력하고, test() 를 실행하면 fail 이 나오는데 expected 값을 received 값으로 바꿔서 넣어주면 pass 가 됩니다

import { expect, test } from '@jest/globals'
import { double } from './example'

test('parameter is a number', () => {
  expect(double(3)).toBe(7)
})

image

image

4.Jest Globals

Jest 전역 함수 자세히 보기

test()

  • test 라는 전역 함수를 사용해서 각각의 용도로 구분해줌. 첫 번째 parameter 는 test 의 이름을 명시하고, 두번째 parameter로는 callBack 함수를 사용해서 실제 테스트를 진행 해주면 되는거임

  • 예)

    test('did not rain', () => {
    expect(inchesOfRain()).toBe(0);
    });
    

describe()

  • 테스트 함수들을 묶어 내는 함수. 일종의 test() 를 group 화 해준다는 것

  • 형식도 test() 와 마찬가지로 처음 parameter 는 이름, 두번째는 callback 함수를 사용함

  • describe() 를 사용하는 이유는, before(), after() 함수를 사용하기 위해서 사용합니다

beforeAll()

  • 위와 마찬가지로 이름, callback 형태로 사용

  • describe() 안에 모든 test 가 진행되기 전에 한번만 실행 합니다

afterAll()

  • 모든 test 가 진행되고 나서 다음에 시행될 것을 명시함

beforeEach()

  • 각각의 test 가 실행되기 직전에 한번씩 동작할 수 있는 구조

afterEach()

  • 각각의 test 가 끝나고 나서 한번씩 동작할 수 있는 구조
import { expect, test } from '@jest/globals'
import { double } from './example'


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

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

  test('Sample test', () => {
    console.log('sample test')
    expect(123).toBe(123) // expect 는 실제 들어오는 값이고, tobe 는 함수 실행 시, 출력이 예상되는 값을 가리킴 
  })

  test('parameter is a number', () => {
    console.log('parameter is a number')
    expect(double(3)).toBe(6)
    expect(double(10)).toBe(20)
  })

  test('No Parameter', () => {
    console.log('No parameter')
    expect(double()).toBe(0)
  })
})

image

5.Jest Marchers

Jest expect 자세히 보기..

expect(), .tobe()

  • 실제 값 (expect) 와 기대되는 값 (tobe) 를 서로 비교해서 test 를 비교하는 것
const can = {
  name: 'pamplemousse',
  ounces: 12,
};

describe('the can', () => {
  test('has 12 ounces', () => {
    expect(can.ounces).toBe(12);
  });

  test('has a sophisticated name', () => {
    expect(can.name).toBe('pamplemousse');
  });
});
  • excpect(), tobe() 의 비교 되는 값은 원시형 데이터 (문자, 숫자 , boolean 데이터) 경우에 사용되며, 참조형 데이터 (배열, 객체) 등은 toEqual() 로 해주어야 합니다.

toEqul()

const can1 = {
  flavor: 'grapefruit',
  ounces: 12,
};
const can2 = {
  flavor: 'grapefruit',
  ounces: 12,
};

describe('the La Croix cans on my desk', () => {
  test('have all the same properties', () => {
    expect(can1).toEqual(can2);
  });
  test('are not the exact same can', () => {
    expect(can1).not.toBe(can2);
  });
});
  • 위의 코드와 같이 객체 데이터의 재귀를 통해서 먼저 두개의 속성의 name 이 같은지 비교 하고, 그 다음에 실제 value 부분이 같은지 비교해서 같을 경우 pass 를 시키는 것입니다

  • 변수가 다르기 때문에 can1 과 can2 는 다른 객체 데이터 임 (다른 메모리에 저장 되있는것) 내부값이 같더라도 code 상에서 can1 === can2 을 하게 되면 false 가 됨. toEqual() 을 사용해서 다른 메모리에 저장 되어 있지만 실제 안에 들어 있는 값만 비교해서 test 를 합니다

  • .not 을 사용해서 마치 ! 와 같이 부정 연산자를 사용해서 비교 test 할 수 있습니다.

  • test 예시 (toBe(), toEqual())


const userA = {
  name: 'Jacob',
  age: 80
}

const userB = {
  name: 'Emma',
  age: 22
}

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

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

6.비동기 테스트

  • 특정한 코드에 비동기 코드를 작성할 경우, 실제 test 의 입장에서는 얼마나 기다려야 되는지 모르기 때문에, 별도로 기다리지 않고 바로 test를 진행 합니다. 그래서 마지막 부분에 비동기 함수를 호출을 시켜 줘야지 test 입장에서 비동기 함수의 작동이 끝날때 까지 기다 립니다.

  • 먼저 통과하는 테스트 code 부터 작성하는 것이 아니라, 실패하는 test 부터 작성을 해서 실제 어떠한 값이 제공이 되었고, 어떠한 값이 기대가 되서 test 가 실패 했는지 항상 확인을 한 다음에 코드를 수정해서 test 를 pass 하게 끔 만들어야 합니다.

// in example.js

export function asyncFn() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('Done!')
    }, 2000)
  })
}
// in example.test.js

import { describe, expect, test } from '@jest/globals'
import { asyncFn } from './example'

describe('비동기 테스트', () => {
  test('done', (done) => {
    asyncFn().then(res => {
      expect(res).toBe('Done!')
      done()
    })
  })

  test('then', () => {
    asyncFn().then(res => {
      expect(res).toBe('Done!')
    })
  })

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

  // 주로 비동기 test 를 할때는 async awit 을 사용해서 직관적인 code 를 작성하는 게 좋음
  test('async/await', async () => {
    const res = await asyncFn()
    expect(res).toBe('Done!')
  })
})
  • 비동기 함수에서 setTimeout() 을 6초로 설정하면 아래와 같이 fail 이 나오게 됩니다. 왜냐면 timeout 의 최대 시간이 기본값으로 5초 (5000ms) 로 설정 되어 있기 때문입니다.

image

  • 만약 6초 후까지 기다린 후에 실행하게 하려면, test code 에서 3번째 parameter 로써 시간을 바꿔서 명시 하면 됩니다. (첫번째는 test 의 이름, 두번째는 실제 test 의 내용을 명시하는 콜백함수, 세번째는 기본값으로 5000 이라는 값이 생략 되어 있는 것임)
import { describe, expect, test } from '@jest/globals'
import { asyncFn } from './example'

describe('비동기 테스트', () => {
  // 주로 비동기 test 를 할때는 async awit 을 사용해서 직관적인 code 를 작성하는 게 좋음
  test('async/await', async () => {
    const res = await asyncFn()
    expect(res).toBe('Done!')
  }, 7000)
})

7.모의 (Mock)함수

  • 단위 test 라는 것은 최소한의 단위 (함수의 로직, 데이터) 를 test 하는 것이기 때문에 걸리는 시간도 최소한 시켜 줘야 합니다. 최대한 시간이 걸리는 logic 을 단순화 시켜 줄 필요가 있습니다. 그때, 사용되는것이 모의 함수 라는 것입니다.

  • 테스트에 방해되는 개념 (지연시간 등) 을 무시하고, 가짜(모의)의 개념으로 함수를 만들어서 value 값만 확인할 수 있게 test 를 진행 할 수 있습니다.

// 예시로 이전의 code 에서 지연시간이 7000으로 설정 되어 있지만, 모의 함수를 만들어서 지연시간을 무시하고 빠른 시간에 test를 진행할 수 있음

import { describe, expect, jest, test } from '@jest/globals'
import * as example from './example'

describe('비동기 테스트', () => {
  test('async/await', async () => {
    jest.spyOn(example, 'asyncFn').mockResolvedValue('Done!') // 모의 함수 실행
    const res = await example.asyncFn()
    expect(res).toBe('Done!')
  }, 7000)
})

image

  • 비동기 테스트를 진행 할 경우 외부요인들의 영향을 받지 않고 test 가 pass 될 수 있게 모의 함수를 잘 사용해야 합니다. (예 인터넷 연결이 안될 경우 외부 API 호출이 안되서 test 가 실패하지만, 모의 함수를 만들어 놓고 test 를 진행하면 외부 요인에 영향을 받지 않습니다.)

  • 예시로 ombd api 에서 title Frozen II 를 Frozen ii 로 변환 하는 test 를 모의함수를 통해서 test 를 진행하는 code 예시 입니다.

// in example.js

import axios from 'axios';
import _upperFirst from 'lodash/upperFirst'
import _toLower from 'lodash/toLower'

export async function fetchMovieTitle() {
  const res = await axios.get('https://omdbapi.com?apikey=7035c60c&i=tt4520988')
  return _upperFirst(_toLower(res.data.Title)) // Frozen II => Frozen ii 영화 제목 뒤에 대문자 I 를 소문자 i 로 바꿔서 출력하기
}
// in example.test.js

import { describe, expect, jest, test } from '@jest/globals';
import { fetchMovieTitle } from './example';
import axios from 'axios'

describe('비동기 테스트', () => {
  // omdb 에서 호출하는 결과 기대 값을 임의로 모의 함수를 만들어서 가지고 오는 db runtime 에 상과 없이 테스트를 진행 할 수 있음
  // 서버에서 데이터를 가지고 올 수 없어도 test 를 할 수 있는거임 (즉, 인터넷이 연결이 안된 상태에서도 test 는 된다는 의미임)
  axios.get = jest.fn(() => {
    return new Promise(resolve => {
      resolve({
        data: {
          Title: 'Frozen II'
        }
      })
    })
  })
  test('영화제목 변환', async () => {
    const title = await fetchMovieTitle()
    expect(title).toBe('Frozen ii')
  })
})

🔶 🔷 📌 🔑

Reference

Categories:

Updated:

Leave a comment