ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 테스트 자동화(1) - 자동 테스트의 필요성(feat.TDD)
    카테고리 없음 2024. 11. 10. 14:12


    들어가며

     테스트는 소프트웨어 품질과 장기적인 관점에서의 개발 생산성에 필수적인 과정이다. 하지만 단기적으로는 코드 작성 시간이 두배 가까이 들기도 하고 꽤 귀찮은 작업이다. 그럼에도 테스트는 중요하다.

     

     작년 내내 나의 화두는 "테스트 자동화"였고, 테스트코드와 TDD에 대한 고민과 적용을 거듭해왔다. 이런 경험을 바탕으로 아래와 같이 3번에 걸쳐서 테스트 자동화에 대한 포스팅을 연재할 예정이다! 

      1. 테스트 자동화(1) -  자동 테스트의 필요성(feat.TDD) 

      2. 테스트 자동화(2) - 테스터블한 코드란?

      3. 테스트 자동화(3) - TestContainers를 이용한 테스트 자동화 도입기

     

     자동 테스트는 코드 기반의 테스트로, 테스트코드를 논하자면 TDD가 빠질 수 없다. TDD는 테스트코드를 운영 코드보다 먼저 작성하는 개발 방법론이다. 하지만 요구사항이 불명확한 상황에서 모든 엣지케이스를 떠올리는 것은 불가능에 가깝기 때문에 TDD로 개발하는 것은 현실적으로 어려울 수 있다.

     

     따라서 TDD가 제시하는 개발 방법론에 얽매여 테스트 순서에 집중하기보다는, TDD의 지향점을 떠올리며 테스터블한 코드로 개발하는 습관이 중요하다고 생각한다. 나의 실무 환경은 테스트 자체가 쉽지 않았었기에, 이 포스팅을 통해 그동안 내가 느꼈던 자동 테스트의 필요성과 내가 옳다고 생각하는 TDD 적용 방식에 대해서 다뤄보고자 한다.

     

    목차

    1.  테스트 자동화 도입을 결심하다

    2. 자동 테스트란?

    3. TDD란?

    4. 마무리

     


     

    1. 테스트 자동화 도입을 결심하다

    인수테스트 지옥 😇

     재직중인 회사의 실무 프로세스는 개발 → 테스트 문서 작성  테스트 실행  리팩토링  테스트 문서 수정  테스트 실행 이런 식으로 진행됐고, 테스트 중에 운영 코드 상의 문제를 발견하면 리팩토링 → 테스트 문서 수정 → 테스트 실행의 무한 굴레를 반복해야했다. 그리고 모든 테트스 케이스에 대해서 엑셀로 정리했었더랬지..

     

     그리고 개발 이후에는 아무도 그 문서를 들여다보지 않았다.🫠 최악의 효율성을 가진 작업이었다. 이 문서의 목적에는 엔지니어분들께 개발 완료한 엔진의 사용법을 전달하기 위함도 있었는데, 패치 후 문제가 생길 때마다 유선상으로 설명을 드려야했고 거의 다 문서에 있는 내용이었다. '왜 문서를 보시지 않는 것일까..?' 라고 생각했지만 내가 봐도 보기 쉬운 문서의 형태는 아니었다.

     

     대략 이런 식... 모든 테스트케이스에 대해서 정리하고, 결과 캡쳐해놓는 가내수공업의 향연..

     

     이러한 원시적인 테스트 방법을 개선하기 위해 팀에서는 테스트 자동화를 진행하기로 했다.

     

    PDD는 유명한 기도메타법이야..

     실무에서는 SNMP로 직접 장비에 데이터를 요청하기 때문에 테스트가 가능한 장비가 없다면 시뮬레이터를 만들지 않는 이상 실제 데이터와 동일하게 Stubbing 하는 것이 거의 불가능에 가깝다. 실제 데이터 응답이 어떻게 오는지에 대한 정보가 없을 때는 시뮬레이터로 목데이터를 생성할 수도 없기 때문에 PDD(Pray Driven Development) 즉, 기도메타법에 의존할 때도 있다.. 그리고 꼭 이럴 때 문제가 발생하고 일을 두 번 하게 된다.

     

     이렇게 테스트가 불가능한 경우도 있지만 테스트는 참 귀찮아서 기본적으로 '굳이..?' 라는 생각이 항상 들고, 단기적으로 보면 테스트 때문에 개발 과정이 느려지는 것처럼 느껴진다. 하지만 기능이 추가되어 테스트를 다시 진행하거나 모든 팀원이 공통 기능을 개발하여 중복적으로 테스트를 진행해야하는 상황 등 장기적으로 봤을 때는 가장 빠른 개발 방법이라는 것을 실감하고 있다.

     

     그리고 테스트가 주는 부담을 조금이라도 완화시키기 위해서는 테스트가 편리한 환경을 만듦으로써 '테스트는 편하고 익숙한 것'이라는 인지의 변화를 만들 필요가 있었다(맞아요 가스라이팅입니다. 하지만 사실인걸요). 이를 위해 자동 테스트가 가능한 테스트 코드 작성 CI에 테스트를 추가하여 테스트 자동화 프로세스를 구축했다.

     

     

    2. 자동 테스트란?

    코드 기반 테스트

     수동 테스트는 어플리케이션을 활용해 사람이 직접 한다. QA 직무 범위에 해당하기도 하고, QA가 없다면 개발자가 어플리케이션을 활용해 여러가지 테스트 케이스를 직접 검증한다. E2E 테스트라고도 할 수 있다. 반면, 자동 테스트는 테스팅 프레임워크를 이용해 구현하는 코드 기반의 테스트이다. 그리고 테스트 코드를 작성하는 대표적인 개발 방법론이 TDD이다.


     참고로 '자동 테스트'와 '테스트 자동화'는 살짝 다른 개념이다. 자동 테스트는 운영코드의 동작을 코드로 검증하는 과정이고, 테스트 자동화는 CI/CD 과정에서 테스트를 추가하여 테스트코드가 자동으로 실행되게 하는 것이다. 테스트 자동화를 통해 테스트 커버리지가 일정 수준을 못 넘기면 build를 못 하게 하는 등의 조치를 취할 수 있다. 

     

     

    3. TDD란?

    테스트코드 먼저

     TDD는 Test Driven Development의 약자로, 말 그대로 Test가 개발 프로세스의 중심이 된다. 실패하는 테스트 코드 작성 → 테스트를 통과하는 최소한의 운영코드 작성 → 운영 코드 리팩토링의 순서로 진행한다. 이렇게 운영 코드 작성 전에 테스트코드를 먼저 작성하면 해피케이스와 엣지케이스에 대한 고려 과정에서 완성도 높은 운영 코드가 나오게 된다. 그리고 자연스럽게 합리적이고 테스터블한 파라미터 구성도 가능하다. 그리고 테스터블한 코드 작성은 유지보수가 쉬운 코드로 이어진다.

     

    출처: https://www.icterra.com/tdd-is-not-about-testing-but-the-design/

     

     예를 들어, 운영코드를 먼저 작성한다고 해봤을 때, 이런 코드가 나올 가능성이 크다. 

    // 기존 운영 코드
    class Order createOrder(): Order {
        val currentDateTime: LocalDateTime.now()
        val currentTime: currentDateTime.toLocalTime()
        if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
        	throw IllegalArgumentException("주문 가능 시간이 아닙니다")
        }
        
        return Order(currentDateTime, beverages) 
    }

     

     그리고 테스트코드를 작성한다. 이 테스트는 현재 시간에 따라 성공할수도, 실패할 수도 있게 된다. 

    @Test
    fun createOrder() {
        // given
        val cafeKiosk = CafeKiosk()
        val beverageName = "아메리카노"
    
        // when
        cafeKiosk.add(Americano(), 3)
        val order = cafeKiosk.createOrder()
    
        // then
        assertThat(order.beverages)
            .hasSize(3)
            .allMatch { it.name == beverageName }
    }

     

     

     만약 시간을 외부에서 주입받고, 그 시간이 SHOP_OPEN_TIME과 SHOP_CLOSE_TIME 사이에 있다면 테스트는 무조건 성공할 수 있을 것이라는 생각이 들어 아래와 같이 createOrder의 파라미터로 시간정보를 주입하는 테스트코드를 작성하게 된다.

    @Test
    fun createOrder() {
        // given
        val cafeKiosk = CafeKiosk()
        val beverageName = "아메리카노"
        val currentDateTime = LocalDateTime.of(2024, 11, 20, 22, 0)
    
        // when
        cafeKiosk.add(Americano(), 3)
        val order = cafeKiosk.createOrder(currentDateTime)
    
        // then
        assertThat(order.beverages)
            .hasSize(3)
            .allMatch { it.name == beverageName }
    }

     

     따라서 운영코드도 아래와 같이 시간 정보를 외부에서 주입받을 수 있도록 개선될 것이다! 그리고 테스트코드를 먼저 작성했다면 처음부터 이런 운영코드를 작성할 수 있었을 것이다.

    // 기존 코드를 테스터블한 코드로 리팩토링
    class Order createOrder(currentDateTime: LocalDateTime): Order {
    	val currentTime = currentDateTime.toLocalTime()
        if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
        	throw IllegalArgumentException("주문 가능 시간이 아닙니다")
        }
        
        return Order(currentDateTime, beverages)
    }

     

     

    TDD를 지향하되, 상황에 따라 유연하게 대처하자

     어쩌면 TDD는 환상에 가까울 수도 있다. 대부분의 개발 요청서는 불명확하며, 요청 사항이 지속적으로 변화하는 상황도 비일비재하기 때문이다. 이런 상황에서는 테스트 코드 작성의 선행이 오히려 개발 과정의 병목이 될 수 있다. 이럴 때는 TDD가 테스터블한 코드를 작성하기 위한 도구임을 떠올리면서 개발하되, 굳이 테스트코드를 먼저 작성할 필요는 없는 것 같다. 테스트 코드의 작성 순서보다는 테스트코드 작성 자체가 유의미하다고 생각한다.

     

     

    4. 마무리

     이번 포스팅에서는 테스트 자동화의 필요성을 다루면서 자동 테스트가 가능한 테스트 코드 작성법에 대해서 간단히 언급했다. 다음 포스팅에서는 코드 레벨에서의 테스트 코드 작성법을 자세히 살펴본 후, 마지막 포스팅에서 TestContainers를 사용한 테스트 자동화 프로세스 구축 과정을 다룰 예정이다.

     

     

    * Reference

    https://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C/dashboard

     

    Practical Testing: 실용적인 테스트 가이드 강의 | 박우빈 - 인프런

    박우빈 | 이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을

    www.inflearn.com

     

    댓글

Designed by Tistory.