테스트코드 - KimTaebin-ai/study_posts GitHub Wiki
테스트가 필요한 부분
테스트를 수행할 자원은 유한하므로 완벽한 테스트는 현실적으로 불가능하다. 그렇다면 위험이 높은 기능 및 비기능 요구사항을 집중적으로 테스트해야 한다.
테스트 케이스에 작성해야 하는 것
-
가장 중요한 것
-
실패 가능성이 있는 것
-
결함 발생 시 파급효과가 심각하고 이로 인한 막대한 손실이 발생하는 기능
-
위험 요소가 있는 것
그렇다면 위험 요소란 무엇이 있을까
위험 요소
- 장애 발생 가능성
- 소스코드의 복잡도
- 구현 난이도
- 구현 크기(Line of Code)
- 개발자 등급
- 장애 발생 시 비즈니스 영향도
- 기능 요구 사항을 구현한 후 장애가 발생했을 때, 비즈니스 적으로 미치는 정도
명세 기반 테스트 기법 종류
명세 기반 테스트는 요구사항 정의 문서, 설계서 등 명세를 바탕으로 테스트 케이스를 도출, 실행하여 중대한 결함을 없음을 보장하는 기법이다.
테스트 기법을 정확히 알고 있어야 원하는 테스트와 테스트 코드의 목적을 명확히 할 수 있다.
테스트 코드에는 테스트 기법 하나 씩 사용하여 읽기 쉬운 코드를 만들도록 해야 한다.
1. 동등 분할 Equivalence Partitioning
테스트 대상 데이터의 구간을 일정 간격으로 분할하여 케이스를 구성
보통 데이터의 구간에 중간 값을 대표 값으로 사용
const convertToGrade = score => {
switch (true) {
case score <= 100 && score > 80 :
return 'A'
case score <= 80 && score > 60 :
return 'B'
case score <= 60 && score > 40 :
return 'C'
case score <= 40 && score > 20 :
return 'D'
case score <= 20 && score > 0 :
return 'E'
default :
return null
}
}
describe('convertToGrade', () => {
it('A 학점', () => {
// Given
const score = 90
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
it('B 학점', () => {
// Given
const score = 70
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('B')
})
})
2. 경계값 분석 Boundary Value Analysis
경계 주변 값을 테스트 데이터로 선정하는 기법
분기 또는 반복 구문의 경계 값을 기준으로 케이스를 구성
min, min+, normal, max-, max 다섯 경우의 케이스로 구성
const convertToGrade = score => {
switch (true) {
case score <= 100 && score > 80 :
return 'A'
case score <= 80 && score > 60 :
return 'B'
case score <= 60 && score > 40 :
return 'C'
case score <= 40 && score > 20 :
return 'D'
case score <= 20 && score > 0 :
return 'E'
default :
return null
}
}
describe('convertToGrade', () => {
it('A 학점 - min', () => {
// Given
const score = 81
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
it('A 학점 - min+', () => {
// Given
const score = 82
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
it('A 학점 - normal', () => {
// Given
const score = 90
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
it('A 학점 - max-', () => {
// Given
const score = 99
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
it('A 학점 - max', () => {
// Given
const score = 100
// When
const grade = convertToGrade(score)
// Then
expect(grade).toEqual('A')
})
})
3. 결정 테이블 테스팅 Decision Table Testing
조건(입력 값)과 행위(결과 값)를 테이블로 구성하여 케이스 및 절차를 구성
프로세스 수행 중 요구되는 결정 또는 조건과 프로세스와 관련된 모든 동작을 기술
const login = (email, password) => {
if (!email) {
return INVALID_EMAIL
}
if (!password) {
return INVALID_PASSWORD
}
return LOGIN
}
describe('login', () => {
const EMAIL = 'EMAIL'
const PASSWORD = 'PASSWORD'
it('이메일/비밀번호 유효하지 않을 때', () => {
// Given
const email = ''
const password = ''
// When
const status = login(email, password)
// Then
expect(status).toEqual(INVALID_EMAIL)
})
it('이메일 유효하고, 비밀번호 유효하지 않을 때', () => {
// Given
const email = EMAIL
const password = ''
// When
const status = login(email, password)
// Then
expect(status).toEqual(INVALID_PASSWORD)
})
it('이메일 유효하지 않고, 비밀번호 유효할 때', () => {
// Given
const email = ''
const password = PASSWORD
// When
const status = login(email, password)
// Then
expect(status).toEqual(INVALID_EMAIL)
})
it('이메일/비밀번호 유효할 때', () => {
// Given
const email = EMAIL
const password = PASSWORD
// When
const status = login(email, password)
// Then
expect(status).toEqual(LOGIN)
})
})
4. 조합 Pair-wise
테스트하는 데 필요한 값이 다른 파라미터의 값과 최소한 한 번씩은 조합을하여 케이스를 구성
대부분의 결함이 두 개 요소의 상호작용에 기인한다는 것에 착안하여, 두 개 요소의 모든 조합을 다룸
const convertToTitle = ({repeat: boolean, base: boolean, eq: boolean}) => {
return {
repeat: repeat ? '전체반복' : '한곡반복',
base: base ? '설정' : '해제',
eq: eq ? '설정' : '해제',
}
}
describe('convertToTitle', () => {
it('repeat: 전체반복, base: 설정, EQ: 설정', () => {
// Given
const repeat = true
const base = true
const eq = true
// When
const result = convertToTitle({repeat, base, eq})
// Then
expect(result.repeat).toEqual('전체반복')
expect(result.base).toEqual('설정')
expect(result.eq).toEqual('설정')
})
it('repeat: 전체반복, base: 해제, EQ: 해제', () => {
// Given
const repeat = true
const base = false
const eq = false
// When
const result = convertToTitle({repeat, base, eq})
// Then
expect(result.repeat).toEqual('전체반복')
expect(result.base).toEqual('해제')
expect(result.eq).toEqual('해제')
})
it('repeat: 한곡반복, base: 설정, EQ: 해제', () => {
// Given
const repeat = false
const base = true
const eq = false
// When
const result = convertToTitle({repeat, base, eq})
// Then
expect(result.repeat).toEqual('한곡반복')
expect(result.base).toEqual('설정')
expect(result.eq).toEqual('해제')
})
it('repeat: 한곡반복, base: 해제, EQ: 설정', () => {
// Given
const repeat = false
const base = false
const eq = true
// When
const result = convertToTitle({repeat, base, eq})
// Then
expect(result.repeat).toEqual('한곡반복')
expect(result.base).toEqual('해제')
expect(result.eq).toEqual('설정')
})
})
5. 상태전이 State Transtion
시스템의 각 상태를 중심으로 케이스를 도출하고 전이 상태를 절차로 구성
객체의 상태를 구분하고 이벤트에 의해 어느 한 상태에서 다른 상태로 전이되는 경우의 수를 테스트 케이스로 구성
상태 머신을 사용하는 UI 컴포넌트 테스트 시 유용
describe('MovieComponent', () => {
it('영화관을 선택할 때', () => {
// Given
const component = mount(MovieComponent)
// When
component.trigger('click')
// Then
expect(component.hasClass('active')).toBe(true)
})
})
테스트는 왜 해야 하는가?
"Too little testing is a crime, but too much testing is a sin"
(너무 적은 테스트는 범죄지만, 너무 많은 테스트는 죄이다.)
테스팅이 필요한 이유는 결함이 해결되지 않은 상태에서
시스템이 운영 단계 또는 사용자 사용 단계로 넘어간다면 장애가 발생하여 사용자들이 손실을 입고, 나아가 회사 전체 비즈니스에 영향을 줄 수 있기 때문이다
따라서 이를 방지하기 위해 테스팅을 꼭 해야만 한다.
테스트 코드를 작성해야 하는 이유는...
사실 너무나도 간단하다.
웹 어플리케이션을 테스트한다고 예를 들어보자
-
코드를 수정한다.
-
서버를 동작시킨다.
-
필요에 따라 테스트에 필요한 데이터를 DB에 입력한다.
-
브라우저를 통해 서버에 접속하고, 테스트 대상 메소드를 동작시키는 요청을 한다.
-
테스트를 마치고 DB의 데이터들을 정리한다.
-
이 과정을 죽어라 반복한다.
한 두번 정도는 할 수 있지만 간단한 오류라면 시간낭비이고, 알 수 없는 오류가 발생했다면 위 과정을 계속 반복하며 확인해야 한다.
그러나 테스트를 작성한다면
-
코드를 수정한다.
-
테스트 코드를 작성한다.
-
테스트 코드를 실행한다.
-
결과를 확인한다.
만약 결과에서 실패해도 코드 수정 후 테스트 코드를 실행하는 것 뿐이다.
그렇기에 테스트 코드는 정말 많은 시간을 아낄 수 있다.
테스트 종류
유닛 테스트
유닛 테스트는 코드의 특정 모듈이 의도된 대로 정확히 동작하는지 검증하는 방법이다. 모든 함수와 메서드에 대한 테스트 케이스를 작성하게 된다.
이상적으로 각 테스트 케이스는 서로 분리되어야 하며, 이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우, 빠른 시간 내에 문제를 파악하고 해결할 수 있도록 도와준다. 이를 위해서 Mock Object를 생성하는 것도 좋은 방법이다
유닛 테스트는 개발자뿐만 아니라 테스터에 의해서 수행되기도 하고, 테스터가 유닛 테스트를 작성하면 개발자가 맞춰서 개발하는 사례도 있다.
정적 테스트
정적 테스트는 소프트웨어 실행 없이 소프트웨어를 분석하는 것을 의미한다. eslint, prettier, 오픈 소스 라이센스 검증, 테스트 커버리지 측정 등이 정적 테스트에 해당된다.
통합 테스트
통합 테스트는 유닛 테스트보다 좀 더 큰 단위로 구성된 테스트이다. 구체적으로는 클라이언트, 서버, DB 등과 같이 모든 시스템들을 완전히 통합해서 구축된 상태에서 모두 검사하는 것을 말한다.
통합 테스트에서는 통합된 각 모듈들이 원래 계획했던 대로 작동하는지, 시스템의 실제 동작과 원래 의도했던 요구사항과는 차이가 없는지 등을 판단하게 된다. 수행 시간, 파일 저장 및 처리 능력, 최대 부하, 복구 및 재시동 능력, 수작업 절차 등을 점검한다. 시스템의 내부적인 구현 방식이나 설계에 대한 지식에 관계없이 테스트를 수행하는 블랙박스 테스트의 일종으로 분류된다.
회귀 테스트
버그를 찾는 모든 테스트 방식은 회귀 테스트라고 할 수 있는데, 회귀 테스트는 해결한 버그/이슈가 재현 되는지 검사하는 방법이다.