Unit Test (단위 테스트) 에 관한 생각

0

지금 부서의 역할이 사내에서 제품을 가지고 있는 팀들과 협업을 하면서 기존에 문제를 해결하고 사용자 관점에서 더 가치있는 제품을 항상 딜리버리 가능한 상태로 만드는 것을 목표로 하고 있다.
기존의 문제를 해결하는 방법으로 Agile의 Practice를 필요에 따라 활용하고 있으며, 특히 개발 Practice로는 TDD, Unit Test, Code Review, Pair Programming, CI/CD 자동화, Build Monitor 등의 프랙티스를 가져간다.

개발자로서 프로젝트를 진행하면서 TDD까지 전파하는 것은 정말 어려운 일이고, TDD까지 가기전에 Unit Test 작성하는 것은 제1순위로 전파하려고 하는 편이다.
테스트케이스 작성하는 방법을 체득화하고 그 이후에 TDD까지 이어가려고 하는데, 개발하는 습관이나 사고방식이 함께 바껴져야 하기 때문에 TDD까지 팀에 체득화 시키는 것은 항상 쉽지 않았다.
TDD에 대해서는 할말이 너무 많지만, 이번 글은 Unit Test (단위 테스트)에 대해서 개인적인 글을 써보겠다.

단위 테스트 장점

테스트케이스를 꼼꼼하게 작성을 하면서 내가 변경한 부분이 어디에 영향을 끼치는지 쉽게 파악할 수 있었다. 그 덕에 안심하고 리팩토링을 할 수 있는 점이 가장 좋았다.
단위 테스트를 하지 않는다면 제대로 수정했는지 확인될때까지 로컬에서 확인을 했을 것이다. 만약 오류가 발생하면 디버깅을 통해서 확인하는 작업을 반복했을 것이다.

이렇게 소요되는 시간을 포함해서 생각해본다면 “단위테스트 작성때문에 생산성이 떨어지는 것은 아니냐?” 라는 의문에 대해서 그래도 얻는 장점이 더 많다고 말할 수 있지 않을까 생각한다.

# 단위 테스트가 없다면 어떻게 작업을 했을까?

단위 테스트 작성시 무엇이 좋았더냐?

  • 테스트케이스가 꼼꼼하게 작성되어 있다면, 개발과정 중에 미리 문제를 파악할 수 있다.
  • 코드변경시, 변경한 부분으로 인한 영향도를 쉽게 파악할 수 있다.
  • 코드 리팩토링을 안심하고 할 수 있다. 테스트케이스들은 내가 가입한 보험들 같다.
  • 테스트 자동화를 통해서 항상 딜리버리 가능한 제품을 만들 수 있다.
  • 새로운 입력이 팀에 합류했을때, 개발 스타일, 표준, 컨벤션등을 공유하기에 좋다.
  • 페어 프로그래밍을 할때, 테스트케이스 작성하고 개발하는 역할 핑퐁을 통해서 개발을 페어로 집중해서 진행할 수 있다.
    • TDD + Pair Programming을 한다면, 테스트케이스 작성한 사람의 설계를 공유하면서 소스 개발까지 이어질 수 있으므로 집중력있게 개발할 수 있다.

테스트케이스에 대한 걱정

기존에 단위 테스트를 작성하지 않는 개발자분들과 프로젝트를 할때, 공통적으로 듣는 질문이나 내용들이 있다. 그렇다면 질문에 대해서 하나씩 생각해보자.
아직 해보지 않았거나 어떻게 해야할지 모르기 때문에 생각보다 두려움이 더 커져서 시도하기 어려울 수도 있다. 하지만 그 방법을 알거나 조금씩 해결할 수 있다면 단위 테스트를 적용할 수 있을 것이다.

Q. 기존에 레거시 코드는 어떻게 해야하나?

기존에 만들어진 코드에 모두 테스트 케이스를 만드는 것은 상당히 어렵다. 시간적인 여유가 팀원들이 함께 짤 수 있다면 시도를 해보는 것은 나쁘지 않다.
하지만 이미 만들어진 코드의 규모가 크면 열정만으로는 다 커버하기가 쉽지 않다. 게다가 테스트 작성하는 법을 잘 모르고 있다면 익숙해지기 전까지 시간이 오래 걸린다.
투자한 노력에 비해서 아직 커버해야하는 부분이 많이 남아있다는 것을 확인하는 순간 지칠수도 있다.

  • 기존의 레거시 코드는 E2E 테스트로 커버한다.
  • 새로 개발하거나 변경하는 부분을 대상으로 단위 테스트를 시작하라.

그렇게 하면 시간은 오래걸릴 수 있지만, 점차적으로 단위 테스트를 늘려갈 수 있을 것이다. E2E 테스트또한 기능이 복잡하거나 중요한 기능부터 커버하기 시작하라. 그렇다면 효율적으로 단위테스트 적용이 가능할 것이다.

Q. 소스를 변경할때마다 단위테스트가 실패하면 유지보수 하는데 시간이 들지 않나?

TDD(Test Driven Development, 테스트주도개발)의 라이프 사이클을 보면 테스트를 작성하고, 실패하는 것을 확인하고, 테스트가 통과하도록 고친다. 그이후에 리팩토링을 통해서 코드품질을 향상한다.
이 과정을 반복을 하는데, 추가요건이 생기거나 리팩토링을 통해서 소스가 변경되어 단위 테스트가 실패하면 이를 고치는 것은 자연스러운 과정이다.
물론 테스트를 유지보수하기 쉽게 작성하는 것이 중요하다. 어떻게 하면 유지보수하기 쉬운 테스트케이스를 작성할 수 있을까?

  • 하나의 테스트에 하나의 기능만 검증하라.

    • SRP (Single Responsibility Principle, 단일책임의원칙)을 객체지향 개발에서 중요한 원칙으로 생각한다. 이는 테스트 작성에도 도움이 된다고 생각한다.
    • 이를 유의하면서 테스트케이스를 작성하면 테스트 케이스의 수직거리가 짧기 때문에 훨씬 간결해진다.
    • 하나의 테스트케이스에서 너무 많은 기능을 검증하려고 하면, 중간에 깨졌을때 어떤부분 때문에 깨졌는지 확인하는데 시간이 오래 걸리는 부작용을 낳는다.
  • 테스트케이스에서 반복되는 부분도 리팩토링을 하라.

    • 테스트케이스를 짜다보면 반복되는 부분들이 발생한다.
    • 예를 들면 특정 상황을 만들기 위해서 반복해서 Mock객체에 Stub을 통해서 의도한 결과를 만드는 것이 필요한 경우, 부모클래스나 메소드로 분리하면 테스트 작성자는 테스트 짜는 것에만 집중할 수 있다.

# TDD life cycle

Q. 작성하는 방법을 잘 모르는데 러닝커브가 크지 않나?

초반에는 테스트케이스 작성하는 방법에 익숙하지 않기 때문에 분명히 시간이 든다. 테스트케이스 없이 개발할때 보다 시간이 더 오래 걸릴 것이다. 만약에 팀원들이아 리더가 코드 품질이나 테스트 케이스의 중요성에 대해서 인식하지 못하고 있다면 어떻게 해야할까?
사실 그런 상황속에서 개인의 노력과 희생으로 익숙해질때까지 버티는 것은 쉽지않다.

최소한 같은 팀이라면 코드 품질과 테스트의 중요성은 공감하고 있어야 한다. 그래야 나중에라도 생산성이 떨어지는 것 아니냐 등의 볼멘소리가 나오지 않는다.
Agile로 일하는 방식을 전파할때도 팀뿐만 아니라 리더들에게도 일하는 방식이나 사고방식의 변화의 중요성을 공유하는 이유가 그것 때문이다.
테스트케이스 작성에 익숙해지면 그 이후부터는 대부분 비슷한 경우의 반복이 많으며, 테스트케이스 작성때문에 생산성이 떨어지는 상황은 크게 문제가 되지 않았다.

  • 잘 작성된 테스트케이스는 사전과 같다. 나중에 기억이 잘 안나면 찾아보고 작성하면 된다.
    • 단, 기존에 없었던 테스트를 작성해야 하는 상황에는 당연히 스파이킹 시간을 투자해야한다.
    • 테스트케이스 자체가 문서라고 생각해보라. 물론 문서라는 것이 한번 만들고나면 현행화가 하는데 노력이 많이 들기도 하지만, 한번 만들어 놓으면 필요할때 찾아볼 수 있다.
  • 단위 테스트 작성에 익숙한 사람과 덜 익숙한 사람과 페어프로그래밍(Pair Programming)을 진행해보라.
    • 처음에는 익숙한 사람이 리딩을 하고, 그 이후에는 역할을 바꿔보며 페어로 진행하면 집중한 시간에 러닝커브를 가장 효율적으로 줄일 수 있다.
    • 두 사람의 숙련도 차이가 너무 많이 나는 경우에는 차라리 교육으로 진행하는 것이 효율적이다.

도입하기

단위테스트의 장점도 알았고, 경험해보지 못해서 생긴 두려움도 어떻게 극복할 수 있을지 이야기해보았다. 그렇다면 당장 어떻게 팀에 도입할 수 있을까?
일단 팀원 모두가 공통적으로 필요성을 공감해야 지속할 수 있다. 기존에 프로젝트에 어떤 불편한점이 있었고, 단위테스트를 도입함으로써 어떻게 해결할 수 있을지 공감하고, 필요성을 느끼는 상태에서 시작해야 오래갈 수 있다.
팀원에 한명이라도 부정적인 생각을 가지고 있다면, 그 사람은 테스트케이스 짜기를 앞으로도 꺼려해 누군가가 또 비는 곳을 채우는 상황들이 벌어질 수 있기 때문이다.
기존에 작성하지 않는 팀이 도입하기 위해서는 어떤 변화가 필요할까?

  • PM은 테스트케이스 작성에 개발자들이 익숙해지기까지 팀의 Velocity가 당장 떨어질 수 있다는 것을 인지해야 한다.
    • 팀의 Velocity가 올라올때 까지 얼마나 걸릴지 확인해야하고, 그것을 감안해 일의 우선순의를 정리할 수 있어야 한다.
    • 작성에 익숙해지면 그 이후부터는 개발하는 과정일 뿐이지 Velocity가 떨어지는 등의 일은 발생하지 않을 것이다.
  • 테스트케이스 작성의 원칙, 범위, 작성방법을 정하고 시작한다.
    • 개발자들마다 개발하는 스타일이 다 다를지라도, 소스의 유지보수를 위해서 표준, 코드 컨벤션 등의 스타일을 맞추는 작업을 진행한다.
    • 테스트케이스의 유지보수를 위해서 얼마나 간결하게 작성을 할 것인지 범위를 정하는 것과 어떤 assertion이나 matcher 도구 등을 사용할지 정하고 가야 작성법이 단순해지기 전에 유지보수하기 편하다.
      (예: junit + hamcrest matcher vs assertJ)
    • 테스트케이스 작성법이나 원칙은 계속 개선해나가는 것이 좋다. 단 변경될때마다 팀원들과 적극적으로 공유해야 테스트케이스 작성법을 비슷한 수준으로 유지할 수 있다.

단위테스트 작성원칙

하나의 테스트케이스에 최소한의 기능만 검증하고, 최대한 간결하게 짠다.

  • 테스트케이스가 많은 것은 큰 문제가 되지 않는다. IDE 툴을 사용하면 테스트케이스가 많은 것은 쉽게 찾을 수 있다.
  • [안티패턴] Java를 예를들면, 클래스에서 하나의 메소드를 하나의 테스트케이스를 기준으로 작성하는 경우를 본적이 있다. 분기문이 있거나 exception을 발생하는 경우가 다 하나의 테스트케이스에 포함되어 있다면 유지보수가 너무 어렵다.
  • 소스를 변경해서 테스트케이스를 고치야 하는 경우, 테스트케이스를 이해하기위해서 시간을 많이 든다면 얼마나 비효율적인가? 유지보수하기 편하게 작성하는 것이 중요하다.

입력값에 대한 결과 값을 검증하는 방식으로 짜는 것이 기본이다.

  • 입력값에 대한 결과값을 확인하는 방식으로 작성하면 소스가 변경되더라도 테스트케이스를 변경할 일이 훨씬 적다.
  • 테스트케이스를 작성하다보면 구현체에 의존하지 않는 테스트케이스를 작성해야 유지보수가 쉽다. 하지만 단위테스트는 Mock객체를 사용하고 때로는 Stubbing도 해야되기 때문에 어느정도 구현체에 의존적일 수 밖에 없다.
    하지만 구현체에 의존적이지 않아야 유지보수 노력이 덜 든다.
  • 항상 옳은 방법은 아닐 수 있지만, 기본적으로는 입력 값대비 결과값을 검증하는 방식으로 테스트케이스를 작성한다고 생각하면 쉽다.
  • Java에서 단위 테스트에서 Private이나 Static method에 stub을 하기 위해 PowerMock을 사용하는 경우가 있다. 물론 File 관련 테스트를 작성하는 경우에 PowerMock이 도움이 된다.
    하지만 정말 복잡하거나 필요한 경우가 아니라면 PowerMock을 도입하는 것보다, 입력값에 대한 결과값을 확인하는 방식으로 작성하라.

불안한 부분이 없도록, 개발하는 부분은 최대한 커버하라.

  • 우리가 개발하는 부분은 최대한 꼼꼼하게 작성해야 테스트케이스의 효과를 최대한 얻을 수 있다. 촘촘한 그물로 물고기를 많이 잡을 수 있는 것과 비슷하다.
  • 커버리지는 꼼꼼하게 단위테스트를 작성하면 따라온다. 커버리지 수치 자체가 목적이 되어서는 안된다.

Third Party Library의 기능은 믿고, 검증대상에서 제외하라.

  • 효율적인 개발을 위해 적용한 라이브러리 또는 플러그인의 기능은 정상적으로 동작할거라 믿고 검증하지 않는다.
  • 그럼에도 중요한 기능이고 테스트가 없어서 커버하지 못한다, Unit Test외에 Integration Test나 다른 테스트로 검증을 하는 것도 고려해본다.
  • 예를들어, Spring에서 JPA, Mybatis 등을 사용하는데 Query의 기능을 검증하고 싶은 경우가 있다. 단위테스트를 위해 라이브러리 내부에서 동작하는 방식을 확인하면서 Mock객체로 바꾸고, Stub하는 과정은 상당히 괴롭다.
    이런 경우 Embedded DB나 Test DB를 대상으로 Repository Test 또는 Integration Test를 하는 것이 효율적이다.

단위테스트를 힘들게 드디어 팀에 도입했다. 시작이 반이라고는 하지만 이제부터가 진짜 중요하다. 어떻게 하면 힘들게 도입하게된 단위 테스트의 장점을 만끽하면서 지속할 수 있을까?
테스트케이스를 작성하면서 개발을 시작했는데, 코드의 변경이 없어서 테스트케이스를 왜 작성해야하는지 모르겠다고 한다. 그냥 일만 두배로 하는 기분이고 이게 어떤 장점이 있는지 모르겠다고 한다.
물론 기능이 동작하는 것이 가장 중요하지만, 한번 작성한 코드를 개선하는 노력없이 그대로 계속 둔다는 것은 충격적이다.

단위테스트를 지속하기 위한 방법

Refactoring = 지속하는 원동력

개인적으로는 내가 작성한 코드가 되었던 내가 만드는 제품은 살아있는 생명체라 생각한다. 더 이상 관심을 두지않고, 그대로 둔다면 그냥 죽어있는 거다. 현실은 변경이 있을때마다 누더기처럼 덕지덕지 코드가 붙고, 그러다보니 기존에 만들어진 구조에서 최소한의 변경만 하려고 한다.
이런 상황을 포장해서 고급스럽게(?) 레거시 프로그램이기 때문에 어쩔 수 없다고 말하는 사람들을 만나면 복잡한 생각이 든다.

리팩토링의 장점은 정말 크다고 생각한다. 내가 하고 있는 일, 내가 만드는 것들에 대해 가치를 불어넣어주는 중요한 행위라 생각한다. 리팩토링이 성공했다는 보장이 기존의 테스트케이스 통과이기 때문에, 꼼꼼한 테스트케이스가 있을수록 더 나은 코드로 개선할 수 있는 기회가 많다는 것이다.
그래서 기존에 테스트케이스 효과를 경험하지 못한분들과 페어를 할 때는 유저스토리를 하나하면 리팩토링을 하나하는 순으로 일을 하려한다. 우리가 만든 코드를 개선하면서 바로 작성한 테스트케이스가 효과가 있는 것을 경험하는 과정이 말보다 훨씬 공감하기 좋은 방법이었다.

리팩토링을 습관화하기 위한 좋은 방법은 페어프로그래밍이 있지만 현실적 여건상 하기 어려운 곳들도 있다. 그런 경우에는 코드리뷰를 적용해보라. 코드리뷰를 통해서 더 나은 코드를 눈으로 보고, 내가 만드는 코드들이 더 나은 모습으로 개선될 수 있다는 것을 알게되면 자연스레 어느 정도의 코드품질을 지키기위해 리팩토링을 하게된다.

# 리팩토링은 사랑입니다.

테스트 자동화

테스트 자동화는 딜리버리 가능한 제품을 만들기 위해서는 선택이 아니라 필수다. 테스트 커버리지 측정, Report 생성해주는 라이브러리들, Sonar, Build Monitor를 이용해서 눈으로 우리가 만들고 있는 제품의 상황을 확인할 수 있다.

테스트케이스를 작성하는 목적보다 수치에 집착하는 것은 옳지 않다. 빌드 모니터의 목적은 눈으로 문제를 바로 확인할 수 있기 때문에, 가장 빠르게 대처하기 위함이다. 특히 사람은 눈에 보이는 것들에 민감하게 반응한다. 코드 커버리지나 코드 스멜, 보안성 취약점 등이 눈으로 확인되면 개선을 하려고 한다.
그리고 한번 지켜지는 코드품질의 수준 이하로 내려가는 경우는 드물다.

# Build Monitor
# Sonar Coverage

결론

이번 글은 개인적으로 프로젝트에 단위 테스트를 전파했던 경험들에 기반한 내용으로 개인적인 경험에 기반한 내용이라 부족하거나, 혹시 잘못된 생각이 있을 수도 있습니다.

처음에 단위테스트와 TDD를 프로젝트에서 하고 싶은데 방법을 몰라서 그만두게 된적이 몇번 있었다. 그 이후에 방법을 알고 싶어서 지금 부서에 와서 일을 하게 되었고, 이 글이 저와 비슷한 상황을 겪는 분들께 도움이 되었으면 합니다.
지금의 저는 부족하면 더 많은 테스트케이스를 짜려고 합니다. 단위테스트로 부족하다고 생각이 들면 다른 테스트를 통해서 커버할 수 있는 방법이 있는지 고민합니다. 서비스를 만들다보면 소스가 자주 변화는 경우도 많고, 오늘의 나는 어제의 내가 짠 소스를 보면서 이해못하는 순간이 많습니다.
그런 상황에서 믿을 것은 동료도 있지만 꼼꼼하게 작성한 테스트케이스가 큰 의지가 됩니다.

사람은 한번 더 편안한 것을 알게되면 이전에 구린방식으로 돌아가긴 어렵습니다. 제게 단위테스트가 그랬습니다. 만약에 이 글을 읽고 궁금한 점이 있으시면 댓글로 남겨주시면 확인하는대로 답변드리겠습니다.

Share