Java TDD - 1

Tadap·2023년 8월 2일
0

Test

목록 보기
1/4

강의를 들으면서 정리한 TDD

1. TDD 실패 이유

너무 맘에 와닿았다. Service로직 짜는데는 10분이면 되는데 그걸 테스트 할려고 그에 배에 대한 시간이 들어가고. 뿐만 아니라 DB, File 등 여러가지 복잡한 테스트들은 테스트 짠다고 하루 이틀 쓴적도 많이 있다. 테스트를 하니까 오히려 속도가 느려진다. 진짜 내가 겪는 상황이라 공감이 갔다.

문제점

  1. 코드에 테스트를 넣는다고 무조건 TDD가 아니다.
  2. 테스트의 목적은 1. 회귀버그 방지와 2. 유연한 설계를 할 수 있게 한다. 이중 유연한 설계가 중요하다고 한다.
  3. 커버리지에 집착하면 안된다. - 테스트가 필요 없는 부분은 빼버려야한다... 마음이 아프다.

결론은 TDD 이전에 테스트가 가능한 구조로 코드가 변경되어야 한다는 것이다

나는 뭔가 지금까지 테스트를 위한 테스트만 짜고 있었던것 같다.
일단 이제부터 테스트를 필요한 부분만 짜야겠다

2. 테스트란

시스템이 잘 돌아가는지 검정
1. 인수테스트 - 직접 하기
2. 자동테스트 - 테스트 코드로 자동화
로 나뉜다.

테스트를 돌려서 잘 돌아가는지 확인. 문제가 생기면 문제 해결.

TDD란

테스트 주도 개발. 3가지 순서를 거친다.
1. RED - 테스트가 실패한 뒤.
2. GREEN - 테스트를 성공하게 바꾸고
3. BLUE - 성공한 코드를 리팩토링을 한다.
를 끝날때 까지 돌아간다.

장점

  1. RED를 먼저 작성하기 때문에 인터페이스를 먼저 만드는 것이 강제된다.
    • 이를 통해서 What/Who 사이클을 고민하게 도와준다. 어떤 행위를 누가 할지 결정하는 과정이다
  2. 장기적 관점에서 개발 비용 감소
    • 처음에는 비용이 증가하지만. 길게 보면 개발로 인해 문제가 생겼는지 바로 확인 가능하기 때문에 개발 비용이 감소한다.
    • 단 스타트업 같은 프로젝트는 처음에 개발비용이 특점 지점을 지나기 전까지 매우 크기 때문에 비싸다

여기서 생기는 고민

  1. 무의미한 테스트
    • 매우 동의하는 부분으로 예로들어 DataJPA를 사용하여 save를 하는 경우. 이런건 아마 잘 해줄꺼 같다. 강연과 동의하는게 어차피 이건 안돌아갈리가...? 있을까? 이런걸 커버리지를 채우기 위한 목적이 아니면 잘 모르겠다.
    • 이건 아무래도 지금 하고있는 캡스톤 사람이 부족해서 드는 생각일수도 있다.
  2. 느리고 쉽게 깨지는 테스트
    아래는 지금 내가 하고있는 프로젝트의 Repo 테스트이다.

    총 시간은 거의 2초가 걸렸다.
    총 테스트는 29개이고, 컴터는 라이젠 5800X, 32기가램에서 돌렸다.
    컴퓨터 성능을 생각하면 생각보다 긴 시간이다.
    총 테스트를 다 돌리면 현재 123개정도를 작성했는데 5초 조금 넘게 걸린다.
    앞으로 테스트가 더 늘 예정이고 시간은 촉박해서 더 고민이다.
    거기다 Redis 를 사용하는데 가끔 이게 죽는다. 진짜 이유도 없이 죽고 컴퓨터를 재부팅해주면 해결된다. 해결책도 없는듯 하다.
  3. 테스트가 불가능한 경우
    아직은 안만나봤다.(내가 아직 생각을 못해서 이런 코드를 못짜본 것일수도?)
    여기서는 로그인 당시의 시간을 테스트 하고 싶을때를 가정했는데 당연히 힘들다.
    ms까지 비교해야하면 불가능이라고 보면 되고 컴터 성능에 따라 실패가 좌우된다.
    이럴때 설계가 잘못되었다 라고 신호를 보내는 것이라고 한다.
    이럴때 그냥 Mock을 써버리면 안된다고 강의에서는 말한다.

테스트를 작성하면서 내가 작성한 코드의 문제점을 파악 가능하다.

SOLID와 TEST

둘은 약간 상호 보완적인 관계라고 말한다.

1. 단일 책임 원칙

테스트를 짜다보면 하나의 테스트에 너무 많은 기능이 있다
-> 너무 많은 책임을 하나의 클래스가 지고 있다 == 이제 클래스를 분할해야 한다.
보통 이런 과정을 밟게 된다고 한다.

2. 개방 폐쇄 원칙

테스트용, 프로덕트용 컴포넌트가 분리되면서 원하는 곳에 탈부착을 할 수 있게 한다.

3. 리스코프 치환 원칙

이상적인 테스트는 모든 케이스를 커버하고 있다 따라서 서브 클래스에 대한 치환 여부를 테스트가 판단해 준다.

4. 인터페이스 분리 원칙

테스트를 하면서 인터페이스를 직접 사용해 본다.
따라서 코드를 작성하면서 불필요한 의존성을 실제로 확인 할 수 있다.

5. 의존성 역전

가짜 객체를 이용해 테스트를 작성하려면 의존성이 역전되어 있어야 하는 경우가 생긴다.

테스트가 필요한 이유

보통 Regression을 막기위해 사용한다.
내가 짠 코드때문에 모든게 멈춘다면 아마 나같아도 불안할 것이다.
만약 내가 은행앱을 건드린다면...? 멈춘다면 끔찍할것 같다.

테스트의 3분류

전통적으로 1. 유닛 2. 통합 3. API 테스트로 나뉜다.
구글에서는 1. 소형 2. 중형 3. 대형 테스트로 나눈다.
위는 애매해 구글로 설명을 하자면

  1. 소형은 단일서버 / 단일프로세스 / 단일 스레드에서 돌아간다.
    • DiskIO, Blocking call 이 있으면 안된다.
  2. 중형 테스트는 멀티프로세스, 멀티 스레드가 가능하다.
    • 이때부터 H2 같은 DB가 사용 가능하다.
    • 이걸 많이 만드는 테스트가 많으면 좋지 않다.
  3. 대형은 멀티서버가 가능하다 E2E테스트라고 한다.

소형테스트가 중요하고 이를 많이 늘리는게 목표이다.

테스트에 필요 지식

개념

  1. SUT - 테스트를 할려는 대상
  2. BDD - TDD + DDD + AGILE ㅡ등등 복잡, 사용자 스토리에 집중해서 테스트 짜기
    given, when, then 과 같다
  3. 상호작용 테스트 - 메서드가 실제로 호출했는지에 대한 테스트
  4. 상태기반검증 - 특정값을 넣었을때 나온 값이 기대값과 일치?
  5. 행위기반 검증 - 상호작용 테스트와 같다, 어떤 매서드가 실행되는지 검증
  6. 테스트 픽스처 - 테스트를 위해 필요한 자원을 생성 @BeforeEach쓰는거, 테스트가 한눈에 잘 안들어와서 사용을 안하려고 하는게 좋은듯?

구글에서 가져온 규칙
7. 비욘세 규칙 - 유지하고 싶은 상태나 정책이있다 -> 테스트를 만들어야 한다.
어떤 정책을 정했고, 이를 열심히 개발문서에 적고 설명을 해줘도 한계가 있다.
하지만 테스트로 작성해두면 버그가 나 아! 하고 알게된다.

유지하고 싶은 모든 상태는 테스트 작성 == 곧 정책이 됨

비욘세 규칙 엄청 좋은 것 같다.


  1. test double - 테스트의 대역, 이미지 저장때 마다, 이미지 저장을 할 수 없으니. 대역을 넣어서 해결

대역

  1. dummy - 일을 시켜도 아무것도 하지 않음.
  2. fake - dummy와 다르게 자체 코드를 가지고 행동을 함
  3. stub - 미리 준비된 값을 출력하는 객체, 보통 Mockito를 이용해서 구현
  4. mock - 매서드 호출을 확인하기 위한 객체, 현재는 대역들을 통칭하고 있음 실제로는 특정 객체가 잘 호출되었는지 확인
  5. spy - 모든 매소드 호출을 기록하는 객체

테스트와 의존성

의존성

의존성 - 특정 객체의 함수등을 사용하는 상태
의존성 주입 - 외부에서 객체를 주입받아 사용 이를 통해 유연하게 구성, 자세한건 앞에서 적어둔 내용 참조
의존성 역전 - 사용할려는 객체를 인터페이스를 통해 일을 시킴, 좀 격하게 적용을 하면 import는 무조건 Interface만 해야 한다는 의견도 있다. -> 추상적인 것에만 의존하도록 하자

의존성과 테스트

테스트를 잘 하려면 위 주입과 역전을 잘 사용해야한다.
ex)
로그인 기록을 저장한다고 할 때, Clock을 사용한다고 하자.

public void login(){
	this.loginTimestamp = Clock.systemUTC().mils();
}

이러면 의존성이 숨겨져 있다고 할 수 있다.
위와 같은 경우 테스트를 하려면 시간 비교가 사실상 불가능 하다.
테스트를 할려면 stub 라이브러리를 써야한다 -> Test가 주는 경고

이를 해결해 보자

public void login(Clock clock){
	this.loginTimestamp = clock.systemUTC().mils();
}

이번에는 시계를 주입받았다. 이러면 테스트 시에 Clock 자체에서 fixed를 이용해 테스트가 가능하다.
1. 숨겨진 의존성은 테스트가 힘들다.
2. 의존성은 드러나는게 좋다.
하지만 항상 정답이 아니다. 어딘가에서는 항상 고정된 값을 입력해 줘야 한다.(Service 단이나 어딘가에서, 아니면 결국 폭탄돌리기임)

-> 이를 해결하기 위해 의존성 역전 사용

interface ClockHolder{
	long getMillis();
}

이렇게 인터페이스 작성 후 clockHolder.getMillis(); 를 통해 가져온다 이때는

public void login(ClockHolder clockHolder){
	this.loginTimestamp = clockHolder.getMillis();
}

이렇게 의존성 역전과 주입을 이용했다.
테스트 때는 TestClockHolder라는 구현체를 만들어 사용하고.
실사용 시에는 SystemClockHolder를 만들어 사용하면 된다.

테스트 가능성

쉽게 데이터를 넣고, 쉽게 검증이 가능한가? 이다
의존성을 감춘다 == 가능성이 떨어진다.
해결로 Mock 을 이용해서 테스트를 한다. == 하지만 모든게 Mock을 이용해서 해결 될 순 없다.(테스트가 신호 보내는 중)
의존성 역전으로 해결한다 == 해결

기타 내용 설명

앞으로 필요한 내용을 적었다

  1. Builder 패턴 - 생성자가 지나치게 많아지는것을 막음
  2. 엔티티(Entity) - 도메인 엔티티, DB 엔티티로 나뉘며 도메인 엔티티는 DDD의 핵심 개념이다
    1. 도메인 엔티티: 소프트웨어에서 어떤 문제를 해결하기 위해 만든 모델로 비즈니스 로직, 식별가능, 생명주기를 가지는 특성이 있다 -> class가 결과물 (비즈니스)
    2. DB 엔티티: 데이터베이스에서의 객체이다 -> table이 결과물(RDB)
  3. 영속성 객체 - 위 엔티티들이 같이 협업을 하여 서비스가 만들어 지는데 JPA 가 합쳐지면서 만들어진 객체라고 생각하면 된다.(ORM)
    실제로는 여러 회사들에서도 엔티티들을 혼합해서 사용중이다.
    정답은 없지만 혼합시 단점은 RDB에 종속이 된다는 단점이 있다.
  4. Private매소드는 테스트 하지 않아도 된다. 만약 하고 싶다는 생각이 들면 private가 아닌 public 매소드여야 한다는 것이다. -> 설계 개선 신호
  5. final을 stub 하는것은 잘못된 설계이다. -> 의존성 역전으로 완충제를 둔다
  6. DRY, DAMP
    • DRY: Don't Repeat Yourself 반복하지 말자
    • DAMP: Descriptiva And Meaningful Phrase 근데 테스트 할때는 가끔 해야할지도?(중복해도 가독성이 좋으면 해라)
  7. 테스트에 논리 로직은 피해라 (+, -, for, if)

자신의 의견을 중간중간 이야기 해주시는것 보니까 아직 명확한 정답이 없는 부분이 많은것 같다.
하지만 지금까지 작성한 테스트를 생각해 보면 벌써 의미하는 바가 많은것 같다.
테스트 코드가 문제가 생기면 항상 Mock으로 해결을 해왔는데 다 듣고 나면 수정할 부분이 많이 생길것 같다

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

정보 감사합니다.

답글 달기