[iOS] 테스트 코드의 종류와 작성 관점

Zerom·2026년 7월 7일

iOS 정리

목록 보기
13/14

들어가며

AI를 활용한 개발이 일상화되면서 코드를 생산하는 속도는 빨라졌지만, 그 코드가 실제로 의도대로 동작하는지 확인하는 책임은 여전히 개발자에게 남아 있습니다. AI 코드 리뷰가 품질 보증의 한 축이 될 수 있지만, 실행 가능한 테스트 코드는 리뷰가 줄 수 없는 것을 제공합니다. 바로 반복 가능하고 자동화된 검증입니다.

테스트를 작성해야 한다는 것은 알지만, 어떤 테스트를 얼마나 작성해야 하는지, 그리고 어떤 관점으로 접근해야 하는지 막막할 때가 있습니다. 이 글은 테스트의 종류와 각각이 필요한 이유, 그리고 좋은 테스트를 만드는 사고방식을 정리합니다.


테스트의 종류와 역할

소프트웨어 테스트는 검증하는 범위와 속도에 따라 크게 세 가지 계층으로 나뉩니다. 이 세 계층의 관계를 테스트 피라미드라는 개념으로 이해하면 각 테스트가 왜 필요한지 명확해집니다.

단위 테스트 (Unit Test)

단위 테스트는 가장 작은 단위의 코드(함수 하나, 메서드 하나, 혹은 타입 하나)가 의도한 대로 동작하는지 검증합니다. 외부 의존성(네트워크, 데이터베이스, 파일 시스템)을 모두 격리한 상태에서 실행되기 때문에 빠르고, 결정론적이며, 디버깅이 쉽습니다.

단위 테스트가 빠른 것은 단순한 편의의 문제가 아닙니다. 개발자가 코드를 수정할 때마다 수백 개의 단위 테스트가 수 초 안에 통과 여부를 알려준다면, 피드백 루프가 극도로 짧아집니다. 이 빠른 피드백이 리팩터링을 안전하게 만들고, 더 자주 시도하게 만듭니다.

단위 테스트가 특히 효과적인 상황은 다음과 같습니다.

  • 비즈니스 로직이 복잡할 때 — 계산, 변환, 상태 전이 등
  • 경계 조건(빈 배열, 최솟값/최댓값, nil)을 꼼꼼히 검증해야 할 때
  • 버그를 발견했을 때 — 해당 버그를 재현하는 테스트를 먼저 작성하고 수정

통합 테스트 (Integration Test)

통합 테스트는 두 개 이상의 컴포넌트가 함께 올바르게 동작하는지 검증합니다. 단위 테스트가 각 부품을 개별적으로 점검한다면, 통합 테스트는 부품들이 조립된 상태에서의 동작을 확인합니다.

단위 테스트가 모두 통과한다고 해서 전체 시스템이 정상 동작하는 것은 아닙니다. 각 컴포넌트가 올바르게 구현되어 있더라도, 인터페이스의 계약이 맞지 않거나 데이터 형식이 불일치하거나, 의존성 주입이 잘못 구성되어 있으면 통합 시 오류가 발생합니다. 이런 종류의 문제는 단위 테스트로는 포착할 수 없고, 통합 테스트가 필요합니다.

iOS 개발에서 통합 테스트의 대표적인 대상은 다음과 같습니다.

  • 레포지토리와 네트워크 레이어: 실제 API 또는 테스트용 서버와 통신하여 데이터가 올바르게 파싱되는지 확인
  • 데이터베이스 레이어: 저장, 조회, 삭제 등의 연산이 실제 스토리지와 함께 동작하는지 확인
  • 뷰모델과 도메인 레이어: 비즈니스 로직과 UI 상태가 함께 연동될 때의 동작 검증

단위 테스트보다 설정이 복잡하고 실행 속도가 느리기 때문에, 통합 테스트는 단위 테스트보다 수가 적어야 합니다. 하지만 단위 테스트가 줄 수 없는 신뢰를 제공하기 때문에 생략할 수 없습니다.

E2E 테스트 (End-to-End Test)

E2E 테스트는 실제 사용자의 관점에서 전체 흐름을 검증합니다. 앱을 실제로 실행하고, 화면을 탐색하며, 버튼을 탭하고, 결과를 확인합니다. iOS에서는 Xcode의 UI Testing 프레임워크가 이 역할을 담당합니다.

E2E 테스트는 "이 시나리오가 실제로 사용자에게 잘 동작하는가"에 답합니다. 단위 테스트와 통합 테스트가 모두 통과해도 실제 UI 흐름에서 문제가 생길 수 있습니다. 예를 들어 화면 전환 타이밍, 키보드 처리, 접근성 레이블 누락 같은 문제는 단위 테스트나 통합 테스트로는 발견하기 어렵습니다.

반면 E2E 테스트는 설정이 복잡하고, 실행 속도가 느리며, 불안정(flaky)할 가능성이 높습니다. 네트워크 상태, 애니메이션 타이밍, 시뮬레이터 상태 등 외부 요인에 민감하기 때문입니다. 따라서 E2E 테스트는 핵심 사용자 시나리오에 집중해 최소한으로 유지하는 것이 일반적입니다.


테스트 피라미드 - 비율의 원칙

세 가지 테스트 계층의 이상적인 구성을 시각화한 것이 테스트 피라미드입니다.

        /\
       /  \
      / E2E\      ← 적게 (느리고, 유지비용이 높음)
     /------\
    /통합테스트 \    ← 중간 정도
   /----------\
  /  단위테스트   \  ← 많이 (빠르고, 격리됨)
 /--------------\

피라미드 형태가 제안하는 원칙은 명확합니다. 빠르고 격리된 테스트를 많이, 느리고 통합된 테스트를 적게 유지하라는 것입니다.

이 비율이 역전된 경우를 "역피라미드" 혹은 "아이스크림 콘" 안티패턴이라고 합니다. E2E 테스트 위주로 구성된 테스트 스위트는 실행 속도가 느려 CI 파이프라인이 수십 분씩 걸리고, 테스트가 불안정하여 신뢰도가 낮아지며, 실패 원인을 찾기 어렵습니다.

피라미드의 정확한 비율보다 중요한 것은 각 계층이 제 역할을 하고 있는가입니다. 단위 테스트로 검증할 수 있는 것을 E2E 테스트로 검증하고 있다면, 그것은 불필요한 비용입니다.


테스트는 구현이 아닌 설계의 도구다

테스트 코드를 바라보는 관점은 크게 두 가지로 나뉩니다. 하나는 "완성된 코드가 올바른지 확인하는 도구"로 보는 관점이고, 다른 하나는 "코드를 어떻게 설계할지 생각하는 도구"로 보는 관점입니다.

후자의 관점이 더 생산적인 이유는, 테스트가 코드를 사용하는 첫 번째 클라이언트가 되기 때문입니다. 테스트를 작성하려면 해당 코드의 공개 인터페이스를 어떻게 노출할지, 의존성을 어떻게 주입할지, 상태를 어떻게 관리할지를 먼저 결정해야 합니다. 이 과정에서 설계의 문제가 일찍 드러납니다.

테스트하기 어려운 코드는 설계가 나쁜 코드다

코드를 작성하고 나서 테스트를 붙이려고 할 때 유독 어렵게 느껴지는 경우가 있습니다. 전역 상태에 의존하거나, 의존성이 내부에 생성되거나, 여러 책임이 한 타입에 뒤섞여 있을 때입니다.

이 어려움은 테스트 프레임워크나 테스트 작성 기술의 부족이 아니라, 코드 설계 자체의 문제에서 비롯됩니다.

  • 의존성을 외부에서 주입받도록 설계된 코드는 테스트에서 가짜 의존성으로 교체하기 쉽습니다.
  • 단일 책임 원칙을 따르는 코드는 테스트 케이스가 단순해집니다.
  • 순수 함수(같은 입력 → 항상 같은 출력)로 작성된 비즈니스 로직은 외부 상태 없이도 검증할 수 있습니다.

"이 코드를 어떻게 테스트할 수 있을까?"라는 질문을 코드를 작성하면서 함께 던지는 것만으로도 설계가 달라집니다. 테스트를 나중에 붙이는 것이 어렵다면, 그것은 지금 작성하는 코드가 테스트 가능하게 설계되어 있지 않다는 신호입니다.

Testability와 좋은 설계의 관계

Testability(테스트 가능성)는 좋은 설계의 결과이자 척도입니다. 코드가 테스트하기 쉬울수록, 대체로 다음 특성을 만족합니다.

  • 의존성이 명시적: 어떤 외부 요소에 의존하는지 공개 인터페이스에서 드러납니다
  • 책임이 분리됨: 각 타입이 하나의 일만 합니다
  • 상태가 예측 가능: 같은 입력에 항상 같은 출력을 냅니다

이 특성들은 테스트를 위한 것이기도 하지만, 동시에 코드를 유지보수하기 쉽게 만들고, 재사용 가능하게 하며, 이해하기 쉽게 합니다.


좋은 테스트를 작성하는 관점

테스트의 이름이 문서다

좋은 테스트 이름은 테스트가 실패했을 때 코드를 열어보지 않아도 무엇이 깨진 것인지 알 수 있어야 합니다. testLogin() 보다는 로그인_성공_시_홈화면으로_이동한다() 또는 loginShouldNavigateToHomeWhenSucceeds()가 더 많은 정보를 담습니다.

테스트 이름을 작성할 때 유용한 패턴은 "조건 — 행동 — 기대 결과" 구조입니다.

  • 어떤 상태에서(Given)
  • 어떤 행동을 했을 때(When)
  • 어떤 결과가 나와야 하는가(Then)

이 구조는 테스트를 하나의 명세(specification)로 만들어줍니다. 프로젝트에 새로 합류한 동료가 테스트 목록을 읽는 것만으로도 시스템의 동작을 파악할 수 있다면, 테스트 이름이 충분히 잘 작성된 것입니다.

구현이 아닌 행동(behavior)을 테스트한다

테스트가 구현 세부사항에 의존하면, 코드를 리팩터링할 때마다 테스트가 깨집니다. 동작은 변하지 않았는데 내부 구현만 바꿨을 뿐인데도 테스트를 함께 수정해야 한다면, 테스트가 안전망이 아니라 짐이 됩니다.

좋은 테스트는 "어떻게 구현되어 있는가"가 아니라 "무엇을 하는가"를 검증합니다. 공개 인터페이스를 통해 관찰 가능한 결과만 검증하고, 내부 상태나 private 메서드를 직접 테스트하려는 충동은 설계를 재검토해야 한다는 신호로 받아들이는 것이 적합합니다.

한 테스트는 한 가지만 검증한다

하나의 테스트에서 여러 검증(assert)을 하면, 테스트가 실패했을 때 어느 검증이 실패한 것인지 파악하기 어렵습니다. 테스트 하나가 실패했을 때 원인이 단 하나의 이유여야 합니다.

여러 케이스를 커버하고 싶다면, 테스트를 여러 개 작성하는 것이 낫습니다. 테스트 코드는 적을수록 좋은 것이 아니라, 각각이 명확한 의도를 가질수록 좋습니다.

TDD를 실천하지 않더라도 TDD적 사고는 유효하다

TDD(Test-Driven Development)는 테스트를 먼저 작성하고 구현을 나중에 작성하는 방법론입니다. 실제로 TDD를 엄격하게 따르는 개발자는 많지 않지만, TDD가 촉진하는 사고방식은 실천 여부와 상관없이 도움이 됩니다.

TDD가 강제하는 것은 "이 코드를 어떻게 사용할 것인가"를 먼저 생각하는 것입니다. 구현보다 사용 시나리오를 먼저 설계하면, 인터페이스가 더 명확해지고 불필요한 복잡성이 줄어드는 경향이 있습니다.

TDD를 완전히 따르지 않더라도, 코드를 작성하기 전에 "이 코드를 어떻게 테스트할 것인가"를 잠시 생각해보는 습관이 설계의 품질을 높여줍니다.


AI 주도 개발과 테스트의 관계

AI 코드 생성 도구를 활용할 때, 테스트 코드의 역할은 더 중요해집니다. AI가 생성한 코드는 문법적으로 정확하고 패턴도 익숙하지만, 현재 프로젝트의 구체적인 요구사항이나 엣지 케이스를 완전히 이해하지 못한 채 작성된 경우가 많습니다.

테스트가 잘 작성되어 있다면 두 가지 측면에서 AI 개발의 신뢰성을 높여줍니다.

첫째, AI가 생성한 코드의 정확성을 자동으로 검증할 수 있습니다. AI가 구현을 제안하고 기존 테스트를 통과한다면, 해당 구현이 적어도 기존에 정의된 동작은 만족한다는 것을 확인할 수 있습니다.

둘째, AI에게 맥락을 제공하는 명세로 활용할 수 있습니다. 잘 작성된 테스트는 시스템이 어떻게 동작해야 하는지를 코드로 표현한 명세입니다. AI에게 기존 테스트와 함께 새 기능 구현을 요청하면, 더 의도에 맞는 코드가 나올 가능성이 높습니다.

반대로 테스트 없이 AI 주도로 개발하면, 생성된 코드가 의도대로 동작하는지 확인하는 방법이 수동 테스트뿐입니다. 수동 테스트는 반복할 수 없고, 누락이 생기며, 사람에 따라 결과가 달라집니다.


정리

테스트 종류검증 범위속도안정성주요 목적
단위 테스트함수/클래스 단위빠름높음비즈니스 로직, 경계 조건
통합 테스트컴포넌트 간 협력중간중간인터페이스 계약, 데이터 흐름
E2E 테스트전체 사용자 흐름느림낮음핵심 시나리오 검증

테스트 코드를 작성하는 것은 코드가 완성된 후 "이게 맞나?" 확인하는 작업이 아닙니다. 코드를 어떻게 설계할지, 인터페이스를 어떻게 노출할지, 의존성을 어떻게 관리할지를 함께 고민하는 과정입니다. 테스트하기 어려운 코드를 만났을 때 "테스트 작성 기술이 부족해서"가 아니라 "설계를 다시 봐야겠다"고 생각할 수 있다면, 테스트를 바라보는 관점이 바뀐 것입니다.

글에 대한 피드백이나 더 좋은 방법이 있다면 댓글로 공유해주세요.

참고 자료

profile
꼼꼼한 iOS 개발자 /
Apple Developer Academy @ POSTECH 2기 / 멋쟁이사자처럼 앱스쿨 1기

0개의 댓글