테스팅

테스트 주도 개발하기

테스트

테스트는 말 그대로 작성한 코드가 잘 작동하는것을 검증하는 작업이다. 잘 작동하는지 확인하려면 구현한 기능을 직접 사용해보는 방법이 있다. 직접 클릭해보고 키보드로 입력해보고 잘 작동하는지 확인하는것. 그러나 프로젝트 규모가 커진다면 수동으로 일일이 확인하는 것은 번거로운 일이다. 그래서 테스트 자동화라는 작업을 하여 사람이 직접 확인을 하는것이 아니라 테스트 코드를 작성해서 테스트 시스템이 자동으로 확인을 해줄 수 있게 해준다. 이것이 테스트 자동화

장점은?

다른사람들과 협업을 하게 되는 경우 테스트 코드를 작성하면 큰 도움을 준다 테스트 코드를 사용하면 우리가 프로젝트를 개발하는 과정에서 써내려가는 코드가 기존의 기능들이 실수로 망가뜨리게 되는일을 효과적으로 방지할 수 있다. 또한 리팩토링을 할 때 테스트 코드가 존재한다면 리팩토링 후 코드가 이전과 똑같이 작동하는지 검증하는게 매우 쉬워진다.

유닛 테스트

아주 조그마한 단위로 작성되는 테스트, 예시

  • 컴포넌트가 잘 렌더링 된다.
  • 컴포넌트의 특정 함수를 실행하면 상태가 원하는 형태로 바뀐다.
  • 리덕스의 액션 생성 함수가 액션 객체를 잘 만들어낸다.
  • 리덕스의 리듀서에 상태와 액션객체를 넣어서 호출하면 새로운 상태를 잘 만든다.

프로젝트의 기능을 잘게 쪼개서 테스트를 하면 각 기능이 모두 잘 작동하는지 확인 할 수 있다.

통합 테스트

기능들이 전체적으로 잘 작동하는지 확인하기 위해서 사용하는 것이 통합테스트, 예시

  • 여러 컴포넌트들을 렌더링하고 서로 상호 작용을 잘 한다
  • DOM이벤트를 발생 시켰을 때 우리의 UI에 원하는 변화가 잘 발생한다.ㅣ
  • 리덕스와 연동된 컨테이너 컴포넌트의 DOM에 특정 이벤트를 발생시켰을 때 원하는 액션이 디스패치 된다.

유닛테스트는 하나에 초점을 둔다면, 통합테스트는 여러 요소들을 고려하여 작성한다.

TDD(Test Driven Development)테스트 주도 개발

테스트가 개발을 이끌어나가는 형태의 개발론이며 선 테스트코드 작성 후 구현이다.

3가지 절차

  • 실패 첫번째 절차는 실패이다. 실패하는 테스트 케이스를 먼저 만듬 실패 케이스를 만들 때는 프로젝트의 전체 기능에 대하여 처음부터 모든 케이스를 작성하는것이 아니라, 먼저 구현할 기능 하나씩 테스트 케이스를 작성한다.
  • 성공 두번째 절차는 성공 작성하는 실패케이스를 통과시키기 위하여 코드를 작성하여 테스트를 통과 시키는것.
  • 리팩토링 세번쨰 절차는 리팩토링이다. 구현한 코드에 중복되는 코드가 있거나, 더 개선시킬 방법이 있다면 리팩토링을 진행한다. 리팩토링을 하고난 후에도 테스트 케이스가 잘 성공하는지 확인한다.

TDD의 장점

TDD를 진행하면서 테스트 케이스를 작성할때 주로 작은 단위로 만들어서 코드를 작성할 때 코드가 방대해지지 않고, 코드의 모듈화가 자연스럽게 잘 이루어지면서 개발이 진행된다.
TDD를 하면 테스트 커버리지가 자연스럽게 높아지고 결국 리팩토링도 쉬워지고 유지보수도 쉬워진다. 결국 프로젝트의 퀄리티가 높아짐

TDD 연습하기

배열의 최댓값, 최소값, 평균, 중앙값, 최빈값을 구하는 함수들을 구현해보자
stats.js 와 stats.test.js를 만든다.

최댓값 구하기

테스트케이스를 작성한다.

// stats.test.js
const stats = require('./stats')

describe('stats', () => {
  it('gets maximum value', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4)
  })
})

max 함수 구현

// stats.js
exports.max = numbers => {
  let result = numbers[0]
  numbers.forEach(n => {
    if (n > result) {
      result = n
    }
  })
  return result
}

이렇게 하면 테스트가 통과한다. 그 다음 어떻게 리팩토링을 할지 고민을 해본다 Math.max 함수사용

//stats.js
exports.max = numbers => Math.max(...numbers)

최솟값 구하기

실패 케이스 작성

// stats.test.js
const stats = require('./stats')

describe('stats', () => {
  it('gets maximum value', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4)
  })
  it('gets minimum value', () => {
    expect(stats.min([1, 2, 3, 4])).toBe(1)
  })
})

테스트 케이스 통과시키기

exports.max = numbers => Math.max(...numbers)
exports.min = numbers => Math.min(...numbers)

평균값 구하는 테스트 케이스 작성

const stats = require('./stats')

describe('stats', () => {
  it('gets maximum value', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4)
  })
  it('gets minimum value', () => {
    expect(stats.min([1, 2, 3, 4])).toBe(1)
  })
  it('gets average vale', () => {
    expect(stats.avg([1, 2, 3, 4, 5])).toBe(3)
  })
})

함수 구현

exports.max = numbers => Math.max(...numbers)
exports.min = numbers => Math.min(...numbers)
exports.avg = numbers => {
  const sum = numbers.reduce((acc, current) => acc + current, 0)
  return sum / numbers.length
}

중앙값 구하기 중앙값을 구현하기 전에 우선 배열을 정렬해야한다. 우선 정렬하는 함수를 위한 테스트 케이스 작성.

const stats = require('./stats')

describe('stats', () => {
  it('gets maximum value', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4)
  })
  it('gets minimum value', () => {
    expect(stats.min([1, 2, 3, 4])).toBe(1)
  })
  it('gets average vale', () => {
    expect(stats.avg([1, 2, 3, 4, 5])).toBe(3)
  })
  describe('median', () => {
    it('sorts the array', () => {
      expect(stats.sort([5, 4, 1, 2, 3])).toEqual([1, 2, 3, 4, 5])
    })
  })
})

describe 내부에서 또 describe를 쓸 수 있다. 단 it 내부에 또다른 it 이나 describe는 쓸 수 없다. 위 케이스에서 toBe가 아닌 toEqual을 사용했는데 이는 객체 또는 배열을 비교해야하는 상황에서 사용한다.

exports.max = numbers => Math.max(...numbers)
exports.min = numbers => Math.min(...numbers)
exports.avg = numbers => {
  const sum = numbers.reduce((acc, current) => acc + current, 0)
  return sum / numbers.length
}
exports.sort = numbers => numbers.sort((a, b) => a - b)
exports.median = numbers => {
  const middle = Math.floor(numbers.length / 2)

  if (numbers.length % 2) {
    // 홀수
    return numbers[middle]
  }
  return (numbers[middle - 1] + numbers[middle]) / 2
}

작성 후 리팩토링

exports.median = numbers => {
  const { length } = numbers
  const middle = Math.floor(length / 2)
  return length % 2
    ? numbers[middle]
    : numbers[middle - 1] + numbers[middle] / 2
}

최빈값 구하기

최빈값은 배열에서 빈도가 가장 높은값. 이 값은 배열 안에 어떤 숫자들이 있느냐에 따라 형태가 다르다.

  1. 주어진 값들 중에서 가장 자주 나타난 값이 결과가 된다. [1,2,2,2,3] => 2
  2. 모든 값들의 빈도가 같다면 최빈값은 없다. [1,2,3,] => null
  3. 빈도가 똑같은 값이 여러개라면 결과값도 여러개다. [1,2,2,3,3,4] => [2,3]

테스트 케이스 작성

describe('mode', () => {
  it('has one mode', () => {
    expect(stats.mode([1, 2, 2, 2, 3])).toBe(2)
  })
  it('has no mode', () => {
    expect(stats.mode([1, 2, 3])).tobe(null)
  })
  it('has multiple mode', () => {
    expect(stats.mode([1, 2, 2, 3, 3, 4])).toEqual([2, 3])
  })
})

기능 구현

exports.mode = numbers => {
  const counts = new Map()
  numbers.forEach(n => {
    const count = counts.get(n) || 0
    counts.set(n, count + 1)
  })
  const maxCount = Math.max(...count.values())
  const result = [...counts.keys()].find(
    number => counts.get(number) === maxCount
  )
  return result
}

첫번째 테스트 케이스 통과

exports.mode = numbers => {
  const counts = new Map()
  numbers.forEach(n => {
    const count = counts.get(n) || 0
    counts.set(n, count + 1)
  })
  const maxCount = Math.max(...counts.values())
  const modes = [...counts.keys()].filter(
    number => counts.get(number) === maxCount
  )

  if (modes.length === numbers.length) {
    // 최빈값이 없다
    return null
  }

  if (modes.length > 1) {
    // 최빈값이 여러개
    return modes
  }
  // 최빈값이 하나
  return modes[0]
}

모든 케이스 통과 참고 :https://velog.io/@velopert/TDD%EC%9D%98-%EC%86%8C%EA%B0%9C


Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub