1. Test 분류
- 단위 테스트, 통합 테스트, 인수 테스트
- 블랙 박스 테스트 & 화이트 박스 테스트
- 알파 테스트 & 베타 테스트
2. TDD (Test Driven Development)
TDD는 소프트웨어 개발 방법론 중 하나이다.
기존의 테스트 코드 작성은 프로덕션 코드를 작성한 이후에 이루어졌지만, TDD를 적용하면 프로덕션 코드보다 실패하는 코드를 먼저 작성한다. 그리고 나서 테스트를 통과하기 위해 코드를 최소한으로 개선한 후, 테스트를 통과한 코드를 프로덕션 코드로 리팩토링하는 방식이다.
TDD는 테스트를 위한 기술이 아니라 소프트웨어 설계 방법론에 가깝다.
1) TDD Cycle
RED
- 동작하는 프로덕션 코드가 없는 상황에서 테스트 코드를 먼저 작성하는 것
- 요구 사항을 작성하는 것 과 유사
GREEN
- 테스트를 통과하는 최소한의 코드를 작성하는 과정
- 명백한 실제 구현을 입력하는 방법도 있지만 최대한 빨리 GREEN을 보기 위해서는 상수를 반환하는 코드를 만들고 점진적으로 변수로 바꾸어 바꾸어나가는 방법도 있음
REFACTOR
- GREEN을 만들기 위해서 작성한 코드를 수습
- 좋은 코드로 변경해나가는 과정
2) 필요한 이유
- 변화에 대한 불안감 해소
- 한번에 하나의 일만 집중
- 빠른 피드백
- 테스트 코드 자체가 문서화될 수 있다.
- 테스트를 나중에 작성하는 것은 귀찮은 작업
- 테스트 코드를 작성하면 의존성이 높은 코드는 테스트가 어렵기 때문에 모듈 간 결합도가 낮고 응집도를 높일 수 있는 코드를 사용
3. Test에 의해 주도되는 전형적 모델
1) TDD의 리듬
- 테스트 하나 추가
- 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인
- 코드 변경
- 모든 테스트를 실행하고 전부 성공하는지 확인
- 리팩토링: 중복을 제거하고 나쁜 코드를 좋은 코드로 변경
Spring Boot 실습을 통해 TDD가 어떤 방식으로 돌아가는지 알아보겠다.
깃허브 실습 레포: https://github.com/Autoever-mobility-cloud-school/TDD-example.git
GitHub - Autoever-mobility-cloud-school/TDD-example: TDD 예제
TDD 예제. Contribute to Autoever-mobility-cloud-school/TDD-example development by creating an account on GitHub.
github.com
2) 테스트 코드 작성
테스트 코드를 작성하고, 실행시킨다.
public class TestClass {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
문법적 오류로 인해서 컴파일이 되지 않는다.
⛔️ 에러를 발생시킨 요인
- Dollar 클래스가 없음
- 매개변수를 받는 생성자가 없음
- times 메서드가 없음
- amount 필드가 없음
3) 테스트를 통과하도록 수정
앞에서 작성한 테스트 코드를 통과할 수 있도록 코드를 작성한다.
수정1: 문법적인 에러 해결하기
public class Dollar {
int amount;
public Dollar(int amount){
}
public void times(int multiplier){
}
}
문법적인 에러는 해결되었지만, 또 에러가 발생하는 것을 확인할 수 있다.
수정2: 테스트를 통과하기 위한 코드로 변경
상수를 사용했다가, 변수로 전부 치환한다.
public class Dollar {
int amount;
public Dollar(int amount){
this.amount = amount;
}
public void times(int multiplier){
amount = amount * multiplier;
}
}
이제 테스트가 통과한 것을 확인할 수 있다.
4) 코드 리팩토링 1 : 타락한 객체, 불변의 객체
일반적인 TDD 주기
- 테스트를 작성
- 테스트 코드를 통과하도록 작성
- 리팩토링
작동하는 코드를 만들고 깔끔한 코드를 만드는 것에 유의한다.
깔끔한 코드를 만들고 작동하도록 해결해나가면 Architecture-Driven Development(아키텍처 주도 개발)이다.
🟠 타락한 객체
타락한 객체는 객체지향 원칙을 지키지 못해 코드의 예측 가능성과 신뢰성을 해치는 상태를 말한다.
이를 방지하려면 캡슐화와 불변성 같은 객체지향 설계 원칙을 준수해야 한다.
예시로, 위의 Dollar 객체는 타락한 객체이다.
- amount 필드가 public으로 선언되어 외부에서 아무나 수정할 수 있음.
- times 메서드가 객체의 내부 상태를 변경하므로 예측 가능한 동작을 보장하지 못함.
해결 방법
- 캡슐화: 필드는 private으로 설정하고, 상태를 외부에서 직접 수정할 수 없도록 제한
- 불변 객체로 설계: 메서드는 항상 새로운 객체를 반환하고, 원래 객체의 상태를 변경하지 않도록 설계
- 객체의 역할 명확화: 단일 책임 원칙(SRP)을 준수하고, 객체가 본래의 책임에 충실하도록 설계
🟢 불변의 객체
객체는 항상 동일한 동작을 수행하고 동일한 데이터를 나타내는 것이 좋다. 그래야 객체의 동작을 예측할 수 있기 때문이다.
동일한 연산을 여러 번 수행했을 때 동일한 결과를 만들어 내는 성질을 멱등성이라고 한다.
연산을 수행하고 나면 호출한 객체를 변경하는 것보다는 새로운 결과를 갖는 객체를 생성해서 리턴해주는 것이 좋다.
이런 방식으로 작업을 하면 이전으로 돌아가는 것도 수월해진다.
단점은 매번 새로운 객체를 만들어서 가지고 있기 때문에 연산을 여러 번 하면 메모리 부담을 증가시키게 되므로, 여기에 대비한 방법도 생각해두어야 한다.
쿠버네티스에서 이전 객체를 변경하는 StatefulSet과 새로운 객체를 만들어서 제공하는 Stateless(Deployment, ReplicaSet, ReplicaController, Pod)한 객체를 만들어주는 방법 모두 제공하고 있다. Python의 DataFrame의 경우에도 데이터를 조작하는 함수들은 inplace 옵션을 이용해서 현재 객체를 변경할 것인지 여부를 설정할 수 있도록 하고 있다.
코드 리팩토링: Dollar
times 메서드가 기존 객체의 상태를 변경하지 않고, 새로운 Dollar 객체를 반환하도록 개선한다.
이는 객체의 불변성을 확보해 원래 객체의 상태를 유지하고, 멱등성을 보장하여 동일한 입력에 대해 항상 같은 결과를 반환할 수 있게 한다. 결과적으로 코드의 예측 가능성과 테스트 신뢰성이 높아지게 된다.
public class Dollar {
private final int amount;
public Dollar(int amount){
this.amount = amount;
}
public Dollar times(int multiplier){
return new Dollar(amount * multiplier);
}
public int getAmount() {
return amount;
}
}
멱등성 테스트
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testMultiplication(){
// 멱등성 테스트
Dollar dollar = new Dollar(5);
Dollar result1 = dollar.times(2);
Dollar result2 = dollar.times(2);
// 첫 호출과 두 번째 호출의 결과가 동일한지 확인
assertEquals(result1.amount, result2.amount);
// 원래 객체가 변경되지 않았는지 확인 (불변 객체 여부 확인)
assertEquals(5, dollar.amount);
}
}
5) 코드 리팩토링 2 : 객체의 비교
객체의 필드를 호출해서 객체를 값처럼 사용하는 패턴을 Value Object Pattern이라고 한다.
Value Object Pattern
- 필드가 생성자를 통해서 일단 설정한 후에는 결코 변하지 않아야 한다.
- 데이터가 한 번 만들어지면 절대로 변경되지 않는다.
- 모든 연산이 새로운 객체를 반환해야 한다.
- 비교를 위해 필드를 직접 호출하지 않고, 비교를 위한 메서드나 함수를 만들어 사용하는 것을 권장한다.
- 필드는 private으로 만들고, equals와 같은 메서드를 생성해서 사용한다.
equals & hashCode 오버라이드
자바에서는 Dollar를 해시 테이블의 키로 사용할 것이라면 equals를 구현할 때 hashCode 메서드도 같이 구현하는 것을 권장한다.
아래 코드를 Dollar.java에 추가한다.
@Override
public boolean equals(Object obj) {
if (obj instanceof Dollar other) {
return this.amount == other.amount;
}
return false;
}
@Override
public int hashCode() {
return Integer.hashCode(amount);
}
객체 비교 테스트
TestClass에 객체 비교 테스트 추가
@Test
public void testEquality(){
assertEquals(new Dollar(5), new Dollar(5)); // 동일한 금액
assertNotEquals(new Dollar(5), new Dollar(10)); // 다른 금액
assertEquals(new Dollar(5), new Dollar(5)); // 다시 동일성 확인
}
이처럼, TDD에서는 테스트를 먼저 만든 후, 해당 테스트를 통과하게 하기 위해 코드를 작성하고 수정하면서 개발한다.
이렇게 Spring Boot로 개발할 때 테스트 코드를 작성하는 방법과 코드 리팩토링하는 과정을 다뤄보았다.