공부

jest

Jest 강좌

출처 - 유튜브 코딩앙마

소개, 설치, 간단한 테스트

  • JEST : 페북에서 만듬, Zero config 철학을 가지고 있어서 별개의 설정없이 빠르게 테스트할 수 있는 장점이 있다.
  • js진영에서 테스트 도구로 제일 많이 쓴다.
  • npm i jest -D 설치
  • package.json에서 스크립트에 test : 'jest'로 수정.
  • npm jest 명령어를 치면 *.test.js 파일이나 tests폴더에 테스트 파일들을 모두 테스트한다.
const { add } = require("./fn")

test("2 + 3 = 5", () => {
  expect(add(2, 3)).toBe(5)
})
  • 테스트할 함수를 불러오고 expect에 검증할 값을, toBe에 인자로 예상값을 넣고 테스트를 돌리면 expect값이 예상값과 일치한지 출력해준다.
  • 여기에 쓰인 toBe를 Matcher 라고 한다.
  • toBe는 숫자나 문자 같은 기본 타입형을 검사할때 쓰고, toBe 외에도 다양한 Matcher가 있음.

다양한 Matchers

toBe와 toEqual의 차이

const { makeUser } = require("./fn")

test("Jake toBe", () => {
  expect(makeUser('Jake', 20)).toBe({name: 'Jake', age: 20})
}) // 실패

test("Jake toEqual", () => {
  expect(makeUser('Jake', 20)).toEqual({name: 'Jake', age: 20})
}) // 성공
  • toBe는 객체나 배열의 내부를 재귀적으로 탐색하면서 비교하지 않지만, toEqual은 재귀적으로 돌면서 속성이 같은지 비교함.
    test("Jake toBe", () => {
    const jake = {name : 'Jake'}
    expect(jake).toBe(jake)
    })
  • 이건 통과한다. toBe는 딱 js에서 '===' 비교랑 같은듯하다. 객체나 배열은 레퍼런스값으로만 비교한다.

toStrictEqual

const fn = {
  add : (a, b) => a + b,
  makeUser: (name, age) => ({name, age, gender: undefined})
}

test("Jake toStrictEqual", () => {
  expect(makeUser('Jake', 20)).toStrictEqual({name: 'Jake', age: 20})
}) // 실패

test("Jake toEqual", () => {
  expect(makeUser('Jake', 20)).toEqual({name: 'Jake', age: 20})
}) // 성공
  • toStrictEqual은 toEqual보다 좀 더 엄격하게 비교연산을 수행한다.
  • toEqual에서는 gender: undefined와 gender속성이 아예없는것을 같다고 판별하지만 toStrictEqual은 다르다고 봄.

그외

  • toBeNull
  • toBeUndefined
  • toBeDefined
  • toBeTruthy
  • toBeFalsy
  • toBeGreaterThan - 크다
  • toBeGreaterThanOrEqual - 크거나같다
  • toBeLessThan - 작다
  • toBeLessThanOrEqual - 작거나같다
  • toMatch - 정규표현식
    test("match", () => {
    expect("hello world").toMatch(/hello/)
    }) //성공
  • toContain - 배열에서 특정요소가 있는지 확인
  • toThrow - 에러 발생 여부. 인자에 에러 메시지 넣어서 메시지검증까지도 됨.

비동기 코드 테스트

getName: (callback) => {
    const name = "Mike"
    setTimeout(() => {
      callback(name)
    }, 3000)
  }

test("3초 후에 받아온 이름은 Mike", () => {
  const callback = (name) => {
    expect(name).toBe("Tom")
  }
  fn.getName(callback)
})//성공
  • 위 코드는 성공한다.
  • 테스트를 돌려보면 3초를 기다리지 않고 success를 출력하는데, jest는 test함수의 끝에 도달하면 그대로 종료된다.
  • 비동기의 콜백을 실행되기 전에 종료되서 success를 출력하는 것임.
  • 이럴때는 테스트함수에 done이라고 하는 콜백함수를 전달해주면된다.
test("3초 후에 받아온 이름은 Mike", (done) => {
  const callback = (name) => {
    expect(name).toBe("Tom")
    done()
  }
  fn.getName(callback)
}) //3초 후 실패
  • 콜백내의 done함수가 실행될 때까지 테스트가 종료되지 않아 비동기함수를 수행할 수 있음.
  • done을 전달은 받았는데 호출하지 않으면 테스트가 실패한다.
    getAge: (age) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(age)
        }, 3000)
      })
    }
    test("3초 후에 받아온 age는 30", () => {
    return getAge(30).then((age) => {
      expect(age).toBe(30)
    })
    })
  • promise의 경우 done을 안써도 된다. 프로미스를 return 시켜주면 됨.
    test("3초 후에 받아온 age는 30", () => {
    return expect(getAge(30)).resolves.toBe(30)
    })
  • 이렇게 사용할 수도 있음.
  • expect(getAge(30)).rejects.toMatch("error")이런식으로 reject도 테스트가능함.
test("async await",async () => {
  const age = await getAge(30)
  expect(age).toBe(30)
})
  • async await은 그냥 평소에 쓰던대로 하면 됨.
    test("3초 후에 받아온 age는 30", async () => {
    await getAge(30).then((age) => {
      expect(age).toBe(30)
    })
    })
    

test("3초 후에 받아온 age는 30", async () => {
await expect(getAge(30)).resolves.toBe(30)
})

- 위에 코드중 return 대신 await을 써도 돌아간다.


### 테스트 전 후 작업
- beforeEach(() => {}) : 각 테스트 실행전에 이 함수가 실행됨. 값을 초기화하기 좋음
- afterEach : 이건 각 테스트가 끝날때마다 실행된다.
- beforeAll / afterAll : 각 테스트 케이스마다가 실행되는게 아니라 전체 테스트가 시작하기 전 / 끝난 후에 실행됨.
- describe를 써서 여러 테스트를 한 블록에 묶을 수 있다. 
- 이때 describe내의 before / after 함수들은 describe 내부에서만 실행 됨.
- 근데 describe내의 테스트들은 describe 밖에서 정의한 before/after함수들도 적용된다.
- describe 밖에있는 beforeEach가 내부의 beforeEach보다 먼저 실행됨.
- describe 안에있는 afterEach가 외부의 afterEach보다 먼저 실행됨.
- 밖 beforeEach -> 안 beforeEach -> 테스트 -> 안 afterEach -> 밖 afterEach
```javascript=
test.only("0 + 1 = 1", () => {
  expect(add(0, 1)).toBe(1)
})
  • test.only는 해당 테스트만 실행되도록 한다.
  • 여러 테스트가 있는데 하나만 실패한 경우 외부의 요인인지 코드자체의 문제인지 파악할때 유용
  • test.skip은 해당 테스트만 건너뛰도록 한다.

mock 함수

  • 테스트 하기 위해 흉내만 내는 함수.
  • 유저 db에 접근해서 user list를 select 해오는 작업이 필요하다면 작성해야할 코드가 상당히 많아진다.
  • 외부요인의 영향을 받기도 함(네트워크나 db환경)
  • 직접 코드를 짜는 대신 목함수로 대체함.

calls

const mockFn = jest.fn()

mockFn()
mockFn(1)

test('dd', () => {
  console.log(mockFn.mock.calls) // [ [], [ 1 ] ] 출력
  expect('dd').toBe('dd')
})
  • jest.fn()으로 목함수 생성
  • mockFn.mock.calls는 목함수가 실행되었을 때의 인자를 가진다.
  • calls.length로 호출된 횟수를 테스트하거나 인자들의 값으로 테스트함

예시


const mockFn = jest.fn()

const forEachAdd1 = (arr) => {
  arr.forEach(num => {
    //fn(num + 1)
    mockFn(num + 1)
  })
}

forEachAdd1([10, 20, 30]);
test('함수 호출은 3번 됩니다.', () => {
  expect(mockFn.mock.calls.length).toBe(3)
})
test('전달된 값은 11, 21, 31 입니다.', () => {
  expect(mockFn.mock.calls[0][0]).toBe(11)
  expect(mockFn.mock.calls[1][0]).toBe(21)
  expect(mockFn.mock.calls[2][0]).toBe(31)
})
  • 배열을 순회하면서 배열값 + 1을 인자로 함수 fn을 실행시키는 forEachAdd1이라는 함수가 있고, 아직 fn이라는 함수는 만들지 않은 상태이다.
  • fn을 만들기 전에 목함수로 호출 횟수와 전달한 인자를 테스트해서

results


const mockFn = jest.fn(num => num + 1)

mockFn(10)
mockFn(20)
mockFn(30)
console.log(mockFn.mock.results)
/*[
      { type: 'return', value: 11 },
      { type: 'return', value: 21 },
      { type: 'return', value: 31 }
    ]
*/
  • results는 결과값을 리턴한다.
    const mockFn = jest.fn()
    

mockFn
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValue(true)

const result = [1, 2, 3, 4, 5].filter((num) => mockFn(num))

test("홀수는 1, 3, 5", () => {
expect(result).toStrictEqual([1, 3, 5])
})

- mockReturnValueOnce는 목함수의 리턴값을 테스트 중간에 주입 가능하다.
- 연속적으로 값을 전달하는 형태의 코드에서 효율적으로 사용 가능
- mockResolvedValue는 비동기의 resolve를 흉내냄

#### 목킹 모듈
```javascript=
const { createUser } = require("./fn")

test('유저를 만든다.', () => {
  const user = createUser('Mike')
  expect(user.name).toBe('Mike')
})
  • 실제로 db에 유저를 생성하는 함수인 createUser 함수가 있다고 하자.
  • 테스트를 할 때마다 테스트유저를 db에 만들면 이후 직접 삭제해줘야하는 귀찮음이 있다.
  • createUser를 mocking module로 만들어서 실제 함수를 대체한다.
const { createUser } = require("./fn")

jest.mock('./fn')
createUser.mockReturnValue({name : 'Mike'})

test('유저를 만든다.', () => {
  const user = createUser()
  expect(user.name).toBe('Mike')
})
  • 위와 같이 createUser를 모킹 모듈로 만들면 실제 createUser코드는 실행되지 않고 mockReturnValue에 지정한 리턴값을 반환한다.

리액트 컴포넌트 + 스냅샷 테스트

  • @testing-library/react 라이브러리로 테스트내에서 렌더링하고, 해당 스크린에 문자열을 검색하는 방식으로 테스트 할 수 있다.
  • 스냅샷은 성공하는 케이스를 저장해두고 비교하면서 테스트하는 방식이다.
  • toMatchSnapshot() 메소드로 스냅샷을 생성하고 비교함
  • 스냅샷 업데이트는 npm test후 터미널에서 업데이트 명령어를 치면 됨
  • 만약 버그가 있어서 실패한 것인데 꼼꼼이 살펴보지않고 스냅샷을 업데이트해버리면 다음부턴 버그가 있는 스냅샷으로 테스트를 하게 되니 신중하게 선택해야 한다
  • 타이머 같은 컴포넌트는 매번 출력되는 시간이 바뀌므로 스냅샷 테스트를 할때마다 fail이 뜰 것이다.
  • Date.now를 목함수를 써서 고정된 값으로 만든다.

'공부' 카테고리의 다른 글

코드가 얼마나 복잡한가에 대한 근거  (0) 2021.01.12
CORS  (0) 2020.12.29
리액트 Docs - 고급 안내서  (0) 2020.12.16
리액트 Docs - 주요개념  (0) 2020.12.15
실용적인 프론트엔드 전략 3  (0) 2020.12.10