TDD 학습내용 정리

TDD

테스트로부터 시작하는 개발 방식

  • (실패하는) 테스트 코드 작성
  • 테스트를 통과시킬만큼 구현
  • 코드정리

테스트코드는 given when then으로 구성됨

1
2
3
4
5
6
7
8
9
10
@Test
void confirmMember() {
// 상황: 대기 상태 회원이 존재
memoryMemberRepository.save(Member.id(“id”).status(WAITING).builder().build());
// 실행: 회원을 승인하면
confirmMemberService.confirm(“id”);
// 결과: 회원이 활성 상태가 됨
Member m = memoryMemberRepository.findById(“id”);
assertThat(m.getStatus()).isEqualTo(ACTIVE);
}

외부 상태에 의존하는경우 대역을 사용한다.
(ex. 외부서버에 붙거나, 내가 붙기힘든것들..)

대역(double 라고 함)

대체구현은 대역을 이요해서 테스트에 필요한 상황/결과를 구성

대역의 종류
스텁 (stub) - 구현을 최대한 단순한 것으로 대체
result를 미리 세팅해놓고 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RefundAccountRegisterTest {
private RefundAccountRegister register;
private StubAccountValidator stuValidator = new StubAccountValidator();

@BeforeEach
void setUp() {
register = new RefundAccountRegister(stuValidator);
}

@Test
void invalidAccount() {
stuValidator.setResult(AccountValidity.INVALID);
assertThatThrownBy(() ->
register.registerRefundAccount(“12345”, “name”, “loginId”))
.isInstanceOf(InvalidAccountException.class);
}
}

가짜 (fake) - 기능을 구현해서 진짜와 유사 하게 동작 (경량 버전)

1
2
3
4
5
6
7
8
9
10
public class MemoryMemberRepository implements MemberRepository {
private Map<String,Member> members = new HashMap<>();
@Override
public Member findById(String id) {
return members.get(id);
}
public void save(Member member) {
member.put(member.getId(), member);
}
}

스파이(spy) - 호출된 내역을 기록하여 활용
mock는 목 객체 그자체를 의미하여 껍데기만 있는 클래스라고 볼 수 있다. 즉 Mocking를 해야 무언가가 리턴됨
spy는 기존의 클래스기능을 기대로 쓰되 일부분만 mock하여 활용가능함.

MyWorkflow라는 Service가 있고 이 Service는 UserService, LoginService를 주입받는애라고 할 때
아래처럼 코드가 구성될것이다.

UserService, LoginService 는 모든 메소드에 Mocking을 걸어야 사용가능하지만
MyWorkflow의 경우 기존에있는 아무 메소드나 호출이 가능함. 근데 Mocking를 걸려면 @Spy를 추가로 붙여줘야 한다.
즉 MyWorkflow를 테스트하기 위해 만든 MyWorkflowTest내 에서 MyWorkflow를 테스트하기 위해 Mocking할 때 사용된다고 보면될듯하다

1
2
3
4
5
6
7
8
@Mock 
UserService userService;
@Mock
LoginService loginService;

@InjectMocks
@Spy
MyWorkflow myWorkflow;

https://jojoldu.tistory.com/239

모의(mock) 객체 - 기대한대로 상호작용하는지 행위를 검증(보통 모의 객체는 스텁과 스파이 가능)
모의객체는 가짜를 빼고 다 활용가능한 다재다능한 아이임
주로 모의객체를 만들고 스텁과 스파이, 모의객체 그 자체로 그때그때 상황에 맞게 활용한다.
모의객체는 상호작용이 원활히 이뤄지는지 검증하는데 활용함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AutoDebitRegisterTest {
private AutoDebitRegister register;
private CardNumberValidator cardNumberValidator = mock(CardNumberValidator.class);
private AutoDebitInfoRepository repository = mock(AutoDebitInfoRepository.class);
@BeforeEach
void setUp() {
register = new AutoDebitRegister(cardNumberValidator, repository);
}
@Test
void validCard_Then_Info_Saved() {

//스텁처럼 쓰여짐.
given(cardNumberValidator.validate(anyString())).willReturn(CardValidity.VALID);

AutoDebitReq req = new AutoDebitReq("user1", "1234123412341234");
RegisterResult result = register.register(req);

//repository의 save가 되는지 검증
then(repository).should().save(Mockito.any());
}
}

대역을 사용할 경우 장점

1.의존 대상의 진짜 구현없이 테스트 가능
현재 구현 대상에 집중, 병행 개발 가능
의존 대상에 대한 상황 지정을 가능 하게함
의존 대상 에 대한 결과를 확인할 수 있게 함

2.개발속도 향상
서버 구동 없이 상당한 기능 검증 가능
외부 시스템 연동 없이 주요 로직 검증 가능 등등

상황이나 결과를 만들어낼 수 있다.

예외적인 경우를 먼저 테스트해라.

예외적인 경우는 코드 구조에 영향을 주고, 예외적인 경우를 나중에 하게 될 경우 코드가 복잡해질 수 있음.

정상적인 경우 보다 예외적인 경우에 대해 먼저 작성하라

회원가입 예 : 같은ID회원이 존재하는경우 / 같은 ID 회원이 없는경우 (전자 먼저 작성)
주문취소 예 : 주문이 이미 취소된 경우 / 주문이 취소 가능한 경우(전자 먼저 작성)

작은단계로 진행. 즉 점진적으로 하라

상수 리턴
값 비교
구현 일반화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if ("ab12!@A".equals(s))
return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}

public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if ("ab12!@A".equals(s) || "Ab12!c".equals(s))
return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}

public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s.length() < 8)
return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}

대역 도출시점

1.테스트 상황을 만드려할 때 (given)

2.결과를 확인해야 하는데 테스트 대상으로 확인할 수 없을 때

3.기능을 구현하는 과정에서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void sameIdExists() {
//동일 ID가 존재하는 상황 필요!
try {
svc.register(new RegistReq("id", …));
fail();
} catch(DupIdEx ex) {
}
}


@Test
void sameIdExists() {
memoryRepo.save(new Member("id", …));
try {
svc.register(new RegistReq("id", …));
fail();
} catch(DupIdEx ex) {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void noSameIdExists() {
svc.register(new RegistReq("id", …));
//회원 데이터 생성 확인 필요
}

@Test
void noSameIdExists() {
svc.register(new RegistReq("id", …));
Member m = memoryRepo.findById("id");
assertEquals("id", m.getId());
}


TDD 효과
테스트 코드가 쌓이면 디버깅 시간 감소
테스트 코드가 있으면 문제 범위를 좁혀서 디버깅하는게 수월

테스트 코드가 쌓이면 코드 변경에 따른 영향 범위 확인 가능
코드를 수정했는데 실패하는 테스트가 발생하면 문제를 빨리 알 수 있음 (회귀테스트)

코드 구조/설계가 좋아질 가능성이 높아짐
테스트가 가능하려면 의존 대상을 대역으로 교체할 수 있어야함
대역으로 교체할 수 있는 구조는 그 만큼 역할별로 분리되어 있을 가능성이 높음

Share