[Swift] 단위 테스트(Unit Test)

민경준·2023년 3월 7일
0

🌟 단위 테스트(Unit Test) 란?

유닛 테스트(unit test)는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차다.
즉, 모든 함수와 메소드에 대한 테스트 케이스(Test case)를 작성하는 절차를 말한다.
이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우, 단시간 내에 이를 파악하고 바로 잡을 수 있도록 해준다.
이상적으로, 각 테스트 케이스는 서로 분리되어야 한다. 이를 위해 가짜 객체(Mock object)를 생성하는 것도 좋은 방법이다.

출처 - wikipedia, 단위 테스트란?

단위 테스트는 테스트 주도 개발(TDD)에서 대표적으로 사용되는 테스트이다. 위의 설명과 같이 작성한 코드가 의도대로 작동하는지 검증하기 위한 절차로 문제를 방지하는 기능도 있지만, 부수적으로 코드 변경에 의한 사이드 이펙트를 최대한 줄일 수 있는 예방책이 되기도 한다.

이처럼 단위 테스트 작성에는 여러 장점이 있는데 살펴 보도록 하자.

🔥 단위 테스트의 장점

  1. 문제점을 발견하기 쉽다.
    → 문제가 생길시에 모듈화된 코드와 독립적인 테스트를 통해 어느 부분이 잘못되어 있는지 정확하게 확인 할 수 있다.
    그렇게 되면 디버깅 시간을 줄여 테스팅에 대한 시간과 비용을 절감 할 수 있다.

  2. 변경이 쉽다.
    → 테스트 코드를 믿고 언제든지 리팩토링 하거나 새로이 기능을 추가할 수 있고,
    이후에 해당 기능들이 잘 작동하는지 확인하는 Unit Test회귀 테스트를 통해서 확인 할 수 있다.

  3. 통합이 간단하다.
    → 유닛 자체의 불확실성을 제거해주므로 Bottom-Up(상향식) 테스트 방식에서 유용하다.
    각 기능을 검증하고 합쳐서 다시 검증하는 통합 테스트에서 더욱 효율적이다.

  4. 코드에 대한 문서가 될 수 있다.
    → 테스트 코드를 보면 해당 코드나 모듈을 어떤 의도로 작성했는지 알 수 있고,
    해당 의도에 따라 코드를 수정하거나 리팩토링 할 수 있다.


🔥 단위 테스트의 특징 및 구조

  1. AAA
    → 준비(Arrange, Given), 실행(Act, When), 검증(Assert, Then)
  2. 명확성
    → 테스트 케이스만 보고 어떤 테스트인지 알 수 있어야 한다. 불확실성을 두지 말아야 한다.
    예를들어 테스트 구문 안에서 if문은 피해야 한다.
  3. 커버리지
    테스트 케이스가 얼마나 충족되었는지를 나타내는 지표 중 하나이다. 테스트를 진행하였을 때 코드 자체가 얼마나 실행 되었는가를 나타낸다.
    수치가 매우 낮으면 문제지만 (60%) 이 수치를 넘어 점수가 높다고 해서 다른 의미를 갖진 않는다.
  4. 독립성
    → 테스트 코드들은 서로 분리되어 있고, 테스트 하는 코드와도 분리되어 있다. 이는 문제를 쉽게 찾고 해결하게 해준다.
  5. 단위
    → 테스트 구문에서 단위는 동작의 단위이지 코드의 단위가 아니다. 단일 동작의 단위는 여러 결과를 낼 수 있으며, 하나의 테스트로 그 모든 결과를 평가하는것이 좋다.

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

1. 리팩토링 내성

테스트 구문을 변경하지 않고 코드를 리팩터링 할 수 있는지에 대한 척도.
기능은 예전과 같이 완벽히 작동하는데 테스트가 빨간색으로 바뀌었다면, 이런 상황을 거짓 양성이라고 한다.

앱의 지속가능한 성장

테스트가 앱의 지속 가능한 성장을 돕는 매커니즘은 회귀 없이 주기적으로 리팩토링하고 새로운 기능을 추가할 수 있는것이다.

여기에는 두 가지의 이점이 존재한다.

  1. 기존 기능이 고장났을 때 테스트가 조기 경고를 제공한다. 이러한 조기 경고는 코드가 운영 환경에 배포되기 전에 문제를 해결할 수 있도록 돕는다.
  2. 코드 변경이 회귀로 이어지지 않을것이란 확신을 준다. 이러한 확신이 없으면 리팩토링을 하는데 주저하게 되고 코드 베이스가 나빠질 가능성이 높아진다.

거짓 양성의 원인

거짓 양성은 위의 두 가지 이점을 모두 방해하므로 적을 수록 좋다.

테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석된다. 시간이 흐르며 그러한 실패에 익숙해지게 된다면 그만큼 신경을 덜 쓰게 되는데 이내 타당한 실패도 무시하기 시작하면서 기능이 고장나도 운영 환경에 배포되는 수준까지 도달하게 된다.

반면 거짓 양성이 빈번하면 테스트 스위트에 대한 신뢰가 서서히 떨어지며, 더 이상 믿을만한 안전망으로 인식하지 않는다. 즉, 허위 경보로 인식이 나빠진다.
이렇게 신뢰가 부족해지면 회귀를 피하기 위해 코드 변경을 최소한으로 하게 되며 리팩토링이 줄어든다.

테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합 될수록 허위 경보가 더 많이 생긴다.
이 문제의 해결 방법은 해당 구현 세부 사항에서 테스트를 분리하는 것뿐이다.

2. 회귀 방지

회귀방지란 SW 버그를 방지할 수 있어야 한다는 의미이다. 코드 수정 후 버그가 있었는데 테스트가 통과하면 안된다.
기능이 동작하지 않는데 테스트는 통과하는 경우를 거짓 음성이라고 한다.

회귀방지를 평가하기 위해서는 코드 커버리지, 코드 복잡도, 코드 도메인 유의성을 고려해야 한다.
또, 회귀방지를 극대화 하려면 테스트가 가능한 많은 코드를 실행하는 것을 목표로 해야 한다.

거짓 양성과 거짓 음성의 중요성

단기적으로는 리팩토링이 바로 필요하지 않기 때문에 거짓 양성의 중요성이 떨어진다.
하지만 시간이 지날수록 코드 베이스는 나빠지고 복잡해지고 체계성이 떨어지게 된다.
이런 경향을 줄이기 위해 정기적으로 리팩토링을 진행해야 한다. 그렇기에 후반으로 갈 수록 거짓 양성의 중요성이 올라간다.

3. 빠른 피드백

코드가 변경될 때 마다 실행해야 하기 때문에 통과/실패에 해단 피드백이 즉각적으로 전달되어야 한다.
오래 걸리는 테스트는 자주 실행하지 못하고 이는 잘못된 방향으로 코드가 작성되기 쉬우며 더 많은 리소스를 낭비하게 된다.

4. 유지 보수성

테스트가 얼마나 이해하기 어려운지, 얼마나 실행하기 어려운지.
테스트 코드 라인이 많을수록 이해하기 어려운것이고, 사전에 가져와야 하는 무언가가 있다면 실행하기 어려운것이다. (ex. 데이터베이스)


🔥 이상적인 테스트

테스트의 정확도는 신호(발견된 버그 수) / 소음(허위 경보 발생 수)로 수치를 계산할 수 있다.
위 4대 요소는 곱셈으로 계산되며 어떤 특성이라도 0이 되면 전체가 0이 된다.
그러므로 테스트가 가치 있으려면 네가지 모두 점수를 내야한다.

회귀 방지, 리팩토링 내성, 빠른 피드백은 상호 배타적이기 때문에 두 가지를 선택해서 극대화하고 한 가지를 희생해야 한다.
극단적 사례를 살펴보자.

  • 엔드 투 엔드
    • 많은 코드 테스트를 하므로 회귀 방지가 훌륭하다.
    • 거짓 양성에 대한 보호를 훌륭히 해내 리팩터링 내성이 우수하다.
    • 하지만 느린 속도로 빠른 피드백의 지표해서 실패해 단위 테스트 가치가 없다.
  • 간단한 테스트
    • 코드가 간단해 거짓 양성이 생길 여지가 낮아 리팩터링 내성이 우수하다.
    • 매우 빠르게 실행 되므로 빠른 피드백을 제공한다.
    • 하지만 기반 코드에 실수할 여지가 많이 않기 때문에 회귀 자체를 나타내지 않을 코드이다. 즉, 회귀 방지가 없다.
  • 깨지기 쉬운 테스트
    • 내부 구현을 그대로 가져다 사용한 테스트이다.
    • 빠르게 실행되고 회귀 방지를 훌륭히 해낸다.
    • 하지만 내부 구현 사항을 바꾸는 것으로 테스트는 깨지므로 리팩터링 내성이 좋지 않다.

테스트의 전략적 절충

좋은 테스트를 만드는 특성 간에 균형을 이뤄내는 것은 쉽지 않다. 세 가지 범주에서 점수를 모두 최대로 낼 수 없고, 유지 보수 관점을 계속 지켜야 테스트가 꽤 짧아지고 간결해진다. 따라서 절충하고 부분적으로 전략적으로 희생해야 한다. 그럼 어떻게 전략적으로 절충을 해야 할까?

우선 리팩토링 내성은 포기할 수 없다. 따라서 테스트가 얼마나 버그를 잘 찾는지(회귀방지)얼마나 빠른지(빠른 피드백) 사이에서 선택하여 절충해야 한다.
리팩토링 내성을 포기할 수 없는 이유는 테스트가 이 특성을 갖고 있는지 여부를 이진 선택이기 때문이다. 즉, All or Nothing 이다.

테스트 피라미드

테스트 유형간에 정확한 비율은 각 팀과 프로젝트마다 다를것이지만, 보통 피라미드 형태를 유지해야 한다.
즉, 엔드투엔드 테스트가 가장 적고 단위 테스트가 가장 많으며 통합 테스트는 중간 어디쯤에 있어야 한다.

상위 계층으로 올라갈 수록 회귀 방지에 유리하고 하단은 실행 속도를 강조한다.
하지만 어떤 계층도 리팩터링 내성은 포기하지 않는다.

이러한 피라미드 구조에는 예외가 있다. 비즈니스 규칙이나 복잡도가 거의 업는 기본적인 CRUD 작업이라면, 테스트 '피라미드'는 단위 테스트와 통합 테스트의 크기가 같아지고 엔드투엔드 테스트가 없어져 직사각형 처럼 보이게 될것이다. 왜 그럴까?

단위 테스트는 비즈니스 복잡도가 없는 상황에서 유용하지 않으므로 간단한 테스트 수준까지 빠르게 내려간다. 반면 통합 테스트는 그 가치가 잘 지켜진다. 코드가 아무리 단순하더라도 DB와 같이 하위 시스템과 통합돼 잘 작동하는지 확인하는 것이 중요하다. 결국 단위 테스트는 더 적어지고 통합 테스트가 더 많아져 '피라미드'가 직사각형이 되는것이다.


🔥 블랙박스, 화이트박스

  • 블랙박스 테스트: 시스템의 내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 SW 테스트 방법이다. 일반적으로 앱이 어떻게 해야 하는지가 아니라 명세와 요구사항에 따라 무엇을 해야 하는지를 중심으로 구축된다.
  • 화이트박스 테스트: 앱의 내부 작업을 검증하는 테스트 방식이며, 명세와 요구사항에 따라가는것이 아닌 소스 코드에서 파생된 테스트를 진행한다.

화이트박스 테스트 대신 블랙박스 테스트를 기본으로 선택해야 한다. 모든 테스트가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인해야 한다.
테스트를 통해 비즈니스 요구 사항으로 거슬러 올라갈 수 없다면, 이는 테스트가 깨지기 쉬움을 나타낸다.



Reference

profile
iOS Developer 💻

0개의 댓글