단위 테스트 책 회고

chloe·2023년 10월 24일
0

개발 서적 읽기

목록 보기
1/1

8월에 시작한 단위 테스트 스터디가 끝났다!
11주 동안 스터디에 한 번도 빠지지 않고, 시간이 없어서 공부를 못해도 스터디 전날 새벽까지 바득바득 다 읽고 정리해서해서 참석했다.


학습 정리 Notion 링크

공부를 하게 된 계기

단위 테스트는 특히 작년 이맘 때 부터 계속 오랫 동안 방법론이나 효용성에 대해서 많은 고민을 하는 것에 비해, 같이 토론 할 사람이 주변에 없었는데 사내 스터디 모집 글을 보고 너무 기뻣다.

작년에 작성한 단위 테스트에 관한 고민 글
https://velog.io/@kny8092/Unit-Test-Integration-Test

단위 테스트는 개발을 다시 시작하고 나서 일년 동안 속 시원하게 결론이 잘 안서서 공부가 끝나면 회고도 꼭 해보고 싶었다! 근데 책 회고는 어떻게 해야할지 잘 모르겠다.

스터디 방식

1주에 1장 씩 미리 공부해와서 대면으로 회사 미팅룸에서 즉석해서 사다리 타기를 통해 뽑은 발표자의 주도로 내용을 리뷰하고 토론하는 방식으로 진행했다.

스터디원들도 다들 적극적이시고 팀에서 작성하는 테스트 코드에 고민이 있을 때 마다 공유하면서 서로 의견을 내다보니, 다들 비슷한 고민들을 하고있었고, 고민을 같이 생각하는 것 자체가 재미있었다.

테스트는 나에게 사실

구교환이 미우면 사랑해버린다고 했던가.. 테스트는 나에게 그런 존재다 (우윀)
비지니스 로직 짜는 만큼의 시간이 들지만 포함된 조직에 따라 구성원들이 중요성의 경중을 느끼는 바가 다르지만, 나는 여전히 중요하다고 생각하고 그냥 눈물 한번 닦고 해야하는 일이다.

횡전개를 위해 팀에도 꼬박꼬박 공유하기

원래 팀에서 테스트 코드 작성이 활성화되어 있지 않다가, 올해 테스트를 조금씩 작성하기 시작하면서 내가 하고 있는 스터디에 궁금해하시는 분들이 계셨다.
그래서 일부러 공부했던 내용을 요약해서 공유하기도 하고, 테스트 작성 관련해서 의견을 묻기도 하면서 나름대로 스터디 외에 팀에서도 토론이 활성화되었으면 하는 바람에서 슬랙이나 대면으로 이야기를 먼저 꺼내봤다.

테스트의 가치에 대해서는 개인마다 편차가 존재했다.
그래도 최근에는 테스트를 옆에서 나와 같이 많이 작성하는 팀원분과 이야기를 나눴는데, 정답은 없어도 이런 토론을 하는 것 자체가 즐거웠고, 이번 스프린트 기간에 팀 내 테스트를 어떻게 가져갈지 리뷰하기로 했다!

아무튼 진짜 회고

무엇을 알게되었는가?

분기 커버리지
코드 라인 수에 기반한 커버리지는 알고 있었지만 분기를 기준으로 커버리지를 측정하는 방식이 있다는 것을 처음 알았다.

런던파와 고전파
통합테스트와 단위테스트의 피라미드 같은건 많이 봤지만 단위 테스트를 이렇게 나눌 수 있는지 처음 알았다.

Stub
Mock은 알고있는데, Stub과 무슨 차인지 정확히 알지 못했는데 결국엔 통합된 Mock이라는 용어로 사용할 순 있지만 예시를 통해서 이해할 수 있어서 좋았다.

통신 기반 테스트
전까지는 출력 기반 테스트를 생각하며 작성했는데 통신 기반 테스트라는 것이 있는지 처음 알았다. 라이브러리 신기해요..

외부 의존성

관리 의존성은 실제 인스턴스를 사용하고, 비 관리 의존성은 목으로 대체하라고 하면서 의존성을 구분짓는 것이 인상깊었다.

뭐가 제일 좋았는가?

전반적으로 테스트를 위해 코드를 어떤 방식으로 작성하면 좋은지와 실제 테스트 코드에 대한 예시가 잘 나타난 편이라 좋았고, 의미 없는 테스트는 하지말라고 쿨하게 말해줘서 속이 시원했다.

커버리지 지표에 관한 생각

coverage가 너무 낮으면 테스트가 충분하지 않다는 증거이긴 하나, 100% coverage가 반드시 양질의 테스트 스위트를 보장하지 않는다

좋은 단위 테스트의 4대 요소

4대 요소 : 회귀 방지 / 리팩터링 내성 / 빠른 피드백 / 유지 보수성
균형을 이뤄내는 것은 쉽지 않으며 조금씩 인정하는 것이 최선의 전략이다.
리팩터링 내성을 최대화하고 빠른 피드백을 위해서는 단위 테스트를, 회귀 방지를 위해선 E2E 테스트로

단위 테스트와 통합 테스트를 고민할 때 어떤 것에 중점을 둘지 지표로 두기 좋을 것 같아 많이 공감되었다.

협력자 수와 복잡도를 고려한 코드의 4가지 유형과 험블 객체 패턴

도메인 모델과 알고리즘만 단위테스트 하는 것이 매우 가치있고 유지 보수가 쉬운 테스트 스위트로 가는 길이다.
목표를 테스트 커버리지 보다는 각각의 테스트가 프로젝트 가치를 높이는 테스트 스위트인지 생각해보자.

험블 객체 패턴 : 테스트가 어려운 행위들을 단위테스트가 쉬운 행위, 테스트가 어려운 행위 두가지 모듈 또는 클래스로 나누는 것

SUT(테스트 대상 시스템)가 지나치게 복잡한 코드에 해당하는지 고민하면서 개발하게 된 좋은 내용이었다.

테스트 피라미드

단위 테스트로 가능한 한 많이 비지니스 시나리오의 예외 상황을 확인하고, 
통합 테스트는 주요 흐름과 단위 테스트가 다루지 못하는 기타 예외 사항을 다룬다.

Mocking의 모범 사례

시스템 끝에서 비관리 의존성과의 상호 작용을 검증하라
시스템 끝에서 상호 작용을 확인하면 회귀 방지가 좋아질 뿐만 아니라 리팩터링 내성도 향상된다.

테스트를 잘 할 수 있는 코드 예시를 자세하게 설명해주고 있어서 좋았다.

부족한 점은

테스트 취약성과 리팩토링 내용에서 Hexagonal Architecture, 객체 패턴에 대해서 나왔는데, 기반 지식이 좀 부족해서 다음에 좀 더 공부해야할 것 같다.
최근에는 리팩토링 관련해서 디자인 패턴도 조금 씩 공부하는 중이다.

구현 세부 사항과 private method

- 리팩터링 내성을 높이는 방법은 SUT의 구현 세부 사항과 테스트 간의 결합도를 낮춰야한다.
- 단위 테스트를 하려고 비공개 메서드를 노출하는 것은 식별할 수 있는 동작만 테스트하는 것을 위반한다.
- 비공개 메서드를 노출하면 테스트가 구현 세부 사항과 결합되고 리팩터링 내성이 떨어진다.

위의 내용이 책의 초반 부터 마지막 까지 계속 나오는데, 구현 세부 사항의 정의와 범위를 실제로 적용하기엔 조금 어려움이 있는 것 같다.
하지만 테스트 코드를 꾸준히 작성하다보면 점점 인사이트가 생기지 않을까

그래서 테스트 코드가 변화했는가?

  • 코드의 4가지 유형 중 도메인 모델과 알고리즘(협력자 수가 적고 복잡도가 높은 코드)을 테스트할 수 있도록 생각하면서 코드를 작성하게된 것 같다.
  • 테스트 코드에 Stub 처리가 많은 부분을 차지하는데, Stub을 편하게 하기 위한 방식을 고민해봤다.
    • 처음 고안한 방식
      • 상위 추상 클래스를 두고, 이를 상속받은 실제 구현 Class와 Test Stub용 Class 각각 작성.
      • Test에서는 Stub 용으로 만든 Class를 주입하고, 운영에서는 구현 Class를 주입하는 방식
      • Stub용 Class의 내부 구현은 JSON 파일에 저장한 예시 id 기반 내용들을 저장해두고 list로 읽어와 원하는 id의 내용만 filter로 가져와 쓰려고 했다.
    • 문제점
      • 이미 기존에 구현 Class만 존재하기 때문에, Test를 위해 상위 추상 클래스를 새로 만드는 것은 Test를 위해 확장해야하며, 실제 구현체는 실질적으로 1개임
      • 팀원들에게 의견을 구했을 때 위와 같은 반대 의견이 지배적이라 추상화는 하지 못했다.
      • 구현 클래스를 Stub 클래스가 상속 받아 Override도 좋은 방식이 아님
    • 차선책
      • 구현 클래스와 메소드 명을 동일하게 맞춰서 작성하고 Stub 에 명시하도록 Stub Class 작성
public class AgodaApi{

	public Mono<List<Content>> getContents(List<Integer> ids) {
      return webClient.get(url) ..  
    }
    
}

public class AgodaApiStub {
	public Mono<List<Content>> getContents(List<Integer> ids) {
      String jsonString = "";/* resources에 json 파일로 저장한 list string*/
      var contents = objectMapper.readValue(jsonString, new TypeReference<List<Content>>(){});
      return contents.stream()
      			.filter(content -> ids.contain(content.getId))
                .collect(Collectors.toList());
    }

}

class ServiceTest extends Specification{
	
    AgodaApiStub agodaApiStub
    AgodaApi agodaApi
    Service service
    
    setup(){
    	agodaApiStub = new AgodaApiStub
        agodaApi = Mock()
        service = new Service(agodaApi)
    }
    
    def "stub test"(){
    	given:
        List<String> ids = List.of("123","345")
        // 메소드 명과 요구하는 id input을 맞춰서 stub class를 생성
        agodaApi.getContents(ids) >> agodaApiStub.getContents(ids)
        
        when:
        def result = service.getContents(ids);
        
        then:
        result.size() == 2
    }
}

상위 추상 클래스를 만들고 실제 구현 클래스와 Stub 클래스를 완전히 갈아끼울 수 있다면 좋을텐데 회사 소스에서 해당 방식을 적용하지 못한 것이 아쉽고, 더 나은 방식이 있는지 계속 고민해봐야할 것 같다.

다음에 또 스터디를 한다면..

스터디 시간에 공유하는 의견들이 소중한데, 의미있는 토론을 했다는 사실에 비해 끝나면 쉽게 잊어버렸다.
평소에 업무나 공부한 내용은 정리하는 습관을 만들었는데, 스터디가 끝나고 바로 집에 숑- 가지 말고 스터디 중에 있었던 토론도 내용을 한번 정리한 후에 퇴근했으면 더 좋았을 것 같다.

앞으로는 뭘 할 것인가?

  1. JUnit, Spock 좀 더 알아가기
    테스트를 어떻게 할 것인가에 대한 내용은 공부했으니, 지금 사용하고있는 JUnit과 Spock을 좀 더 잘 사용해서 테스트 작성 속도를 높이기 위해, 라이브러리도 확실히 알아가야할 것 같다.

  2. 어떻게하면 Mocking을 더 편하게 할 수 있을까?
    회사 코드에서 외부 API 호출을 Stub하는 것에 대해서 Helper method를 잘 작성해두고 테스트를 더 빠르게 작성할 수 있으면 좋겠다!

사실 이부분은 위에서도 말했듯 최근에 어느정도 만들어두긴했는데, 개인적으로 완벽한 방식은 아닌 것 같아 좀 더 develop 해야할 것 같다.

profile
삽질전문 아티스트

0개의 댓글