모바일 개발에서의 단위 테스트: 개념, 방법 및 프레임워크

저자: IT Sectr 게시일: 2026-04-06 읽는 시간: 9 분

단위 테스트는 시스템의 나머지 부분에서 격리된 개별 모듈 또는 코드 함수의 정확성을 테스트하는 소프트웨어 검증 방법입니다. Martin Fowler, 2026에 따르면, 단위 테스트는 CI/CD와 리팩터링의 기초이며 코드 작동에 대한 빠른 피드백을 제공합니다. 모듈 테스트는 개발 초기 단계에서 오류를 발견하여 수정 비용을 수십 배 절감하는 데 도움을 줍니다.

주요 내용

  • 단위 테스트 — 외부 종속성에서 격리된 단일 모듈(함수, 메서드, 클래스) 검증
  • FIRST 원칙 — Fast, Isolated, Repeatable, Self-validating, Timely — 품질 테스트의 기초
  • 목과 스텁 — 외부 종속성(DB, API, 파일 시스템)의 대체물, 테스트 격리 보장
  • TDD(테스트 주도 개발) — 테스트를 먼저 작성하는 개발 방법론: 레드-그린-리팩터
  • 테스트 피라미드 — 단위 테스트는 피라미드의 70%를 차지하며 각 커밋에서 빠른 피드백 제공

단위 테스트란 무엇인가?

단위 테스트는 소스 코드의 개별 유닛(함수, 메서드, 클래스)을 프로그램의 나머지 부분에서 격리하여 검증하는 프로세스입니다. 각 테스트는 모듈의 특정 사용 시나리오를 실행하고 결과가 예상과 일치하는지 확인합니다. 단위 테스트는 메인 코드와 동일한 프로그래밍 언어로 작성되며 개발 환경 또는 CI/CD 파이프라인에서 자동으로 실행됩니다. 통합 테스트와 달리 단위 테스트는 실제 데이터베이스, 파일 시스템 또는 네트워크 서비스와 상호작용하지 않습니다.

단위 테스트가 필요한 이유

주요 목표는 변경 후 코드 정확성에 대한 빠른 피드백입니다. 개발자가 메서드를 리팩터링하면 단위 테스트 스위트는 동작이 깨지지 않았음을 확인합니다. Google Testing Blog(2025)에 따르면, 단위 테스트 커버리지가 60% 이상인 프로젝트는 프로덕션 장애가 2.5배 적습니다. 추가 이점: 코드 문서화(테스트는 API 사용 방법을 보여줌), 리팩터링 간소화(동작을 유지하면서 구현 변경 가능), 빠른 회귀 진단.

단위 테스트로 간주되는 기준

모든 자동화된 테스트가 단위 테스트는 아닙니다. 기준: 단일 모듈(클래스 또는 함수)을 테스트하고, 외부 종속성을 목이나 스텁으로 대체하며, 테스트는 밀리초 단위로 실행되고 서버나 데이터베이스 시작이 필요하지 않습니다. 실제 데이터베이스에 액세스하는 테스트는 통합 테스트입니다. 브라우저를 여는 테스트는 E2E 테스트입니다. 테스트 유형 간의 경계를 이해하는 것은 테스트 피라미드에서 노력을 적절히 분배하는 데 중요합니다.

FIRST 원칙과 AAA 구조

품질 높은 단위 테스트는 Robert C. Martin이 공식화한 FIRST 원칙을 따릅니다. 각 테스트는 Fast(빠름 — 밀리초), Isolated(격리됨 — 다른 테스트에 의존하지 않음), Repeatable(반복 가능 — 모든 머신에서 동일한 결과), Self-validating(자체 검증 — 결과는 "통과" 또는 "실패", 수동 확인 불필요), Timely(적시성 — 코드보다 먼저 또는 동시에 작성)해야 합니다. 어떤 원칙이라도 위반하면 테스트의 가치가 떨어집니다.

AAA 구조(Arrange-Act-Assert)

단위 테스트 작성을 위한 표준 템플릿입니다. Arrange — 데이터 및 종속성 준비: 객체 생성, 목 구성, 입력 매개변수 설정. Act — 테스트 대상 작업 실행: 메서드 또는 함수 호출. Assert — 결과 검증: 실제 값과 예상 값 비교. 세 블록으로 나누면 테스트를 읽기 쉽고 이해하기 쉽게 만듭니다. Assert 블록에 복잡한 로직이 필요하면 테스트가 한 번에 너무 많은 것을 확인하고 있을 수 있습니다.

kotlin
// Kotlin에서 JUnit 5로 AAA 패턴을 사용한 단위 테스트 예제
class CalculatorTest {

    private lateinit var calculator: Calculator

    @BeforeEach
    fun setUp() {
        // ARRANGE — 테스트 객체 생성
        calculator = Calculator()
    }

    @Test
    fun addition_shouldReturnCorrectSum() {
        // ACT — 작업 실행
        val result = calculator.add(2, 3)

        // ASSERT — 결과 확인
        Assertions.assertEquals(5, result)
    }
}

테스트 명명

테스트 이름은 무엇을 테스트하고 어떤 결과가 예상되는지 설명해야 합니다. 형식: [methodName]_[scenario]_[expectedResult]. 예: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. 좋은 테스트 이름은 주석을 대체하며 실패 시 어떤 기능이 손상되었는지 즉시 알려줍니다. test1, checkSomething, verify와 같은 이름은 피하세요 — 정보를 제공하지 않고 진단을 어렵게 만듭니다.

목, 스텁, 페이크: 무엇을 언제 사용할까

테스트 대상 모듈을 외부 종속성에서 격리하기 위해 테스트 더블을 사용합니다. 주요 유형: 목(mocks) — 특정 메서드가 예상 매개변수로 호출되었는지 확인; 스텁(stubs) — 메서드 호출 시 미리 정의된 값을 반환; 페이크(fakes) — 실제 컴포넌트의 단순화된 구현(예: 데이터베이스와 작동하는 UserRepository 대신 InMemoryUserRepository). 선택은 상태(스텁) 또는 상호작용(목) 중 무엇을 확인해야 하는지에 따라 달라집니다.

더블확인 내용예시
올바른 매개변수로 메서드 호출userRepository.save(user)가 정확히 1회 호출됨
스텁반환 값repository.findById(1)가 User(id=1, name="Test") 반환
페이크단순화된 구현을 통한 로직DB 대신 HashMap을 사용하는 InMemoryMapUserRepository
스파이실제 객체의 부분 모킹spy(repo).when(findById).thenReturn(user)

Mockito: Java/Kotlin 모킹 예시

Mockito는 Java와 Kotlin에서 가장 인기 있는 모킹 프레임워크입니다. mock()으로 목 생성, when().thenReturn()으로 반환 값 구성, verify()로 호출 확인이 가능합니다. Mockito(5.x)의 최신 버전은 정적 목(mockStatic)과 BDDMockito(given-willReturn)를 통한 단순화된 구문을 지원합니다. 중요한 규칙: 자신이 소유하지 않은 것은 목으로 만들지 마세요 — 값 객체와 표준 라이브러리에 대한 목을 생성하지 마세요.

kotlin
// Kotlin에서 Mockito를 사용한 단위 테스트 예제
class OrderServiceTest {

    @Mock
    private lateinit var paymentGateway: PaymentGateway

    @Mock
    private lateinit var userRepository: UserRepository

    private lateinit var orderService: OrderService

    @BeforeEach
    fun init() {
        MockitoAnnotations.openMocks(this)
        orderService = OrderService(paymentGateway, userRepository)
    }

    @Test
    fun processOrder_whenPaymentFails_shouldThrowException() {
        // given
        val user = User(id = 1, balance = 100.0)
        val order = Order(amount = 200.0)
        Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
        Mockito.`when`(userRepository.findById(1)).thenReturn(user)

        // when & then
        assert Throws<PaymentException> {
            orderService.processOrder(user.id, order)
        }

        // verify
        Mockito.verify(paymentGateway).charge(any())
    }
}

TDD: 테스트 주도 개발

TDD(테스트 주도 개발)는 코드 구현 전에 테스트를 작성하는 방법론입니다. "레드-그린-리팩터" 사이클: 실패하는 테스트를 작성하고(레드), 테스트를 통과하는 최소 코드를 작성하고(그린), 동작을 변경하지 않고 코드를 개선합니다(리팩터). TDD는 모든 코드가 테스트로 커버되고(구현된 기능의 커버리지 100%) 코드를 테스트할 수 있음을 보장합니다 — 코드를 테스트하기 어렵다면 아키텍처를 개선해야 합니다.

TDD의 장점

IBM(2006-2026, 종단 연구)에 따르면 TDD를 사용하는 팀은 코드 후에 테스트를 작성하는 팀보다 프로덕션 결함이 40-80% 적습니다. TDD는 아키텍처도 개선합니다: 개발자가 구현 전에 API 설계를 생각하도록 강제하여 느슨한 결합과 높은 응집력으로 이어집니다. 추가 효과는 살아있는 문서입니다: 테스트는 항상 최신 상태인 모듈 동작의 명세 역할을 합니다.

TDD가 적합하지 않은 경우

TDD가 항상 최적은 아닙니다. UI 컴포넌트는 격리하여 테스트하기 어렵습니다 — 스냅샷 테스트나 시각적 회귀 테스트(Percy, Chromatic)가 더 효과적입니다. 프로토타이핑과 연구(스파이크 솔루션)는 테스트가 필요하지 않습니다. 테스트 없는 레거시 코드를 TDD로 커버하기는 어렵습니다 — 이 경우 먼저 특성화 테스트(리팩터링 전에 현재 동작을 캡처하는 테스트)가 필요합니다. 이러한 경우 TDD가 완전히 포기되는 것이 아니라 적응됩니다 — 전체 레거시 코드가 아닌 변경된 기능에 대한 테스트가 작성됩니다.

모바일 애플리케이션에서의 단위 테스트

모바일 개발에는 특수성이 있습니다: 비즈니스 로직이 UI 코드(Activity, ViewController, ViewModel)와 혼합되는 경우가 많아 단위 테스트가 복잡해집니다. 가장 좋은 방법은 얇은 View, 두꺼운 ViewModel입니다: 모든 로직을 UI 컴포넌트에서 분리된 클래스(UseCase, Repository, ViewModel)로 추출하여 에뮬레이터 없이 쉽게 테스트할 수 있도록 합니다. Android와 iOS에는 장치를 실행하지 않고 JVM/Native에서 실행되는 네이티브 단위 테스트 프레임워크가 있습니다.

Android 단위 테스트(JUnit + Mockito/Robolectric)

Android 단위 테스트는 에뮬레이터 없이 로컬 JVM에서 실행되어 실행 속도를 제공합니다 — 일반적인 테스트는 100ms 미만이 소요됩니다. JUnit 5가 메인 러너입니다. ViewModel 테스트에는 코루틴 테스트를 위한 kotlinx-coroutines-test와 StateFlow 테스트를 위한 Turbine을 사용합니다. Robolectric은 섀도 클래스를 로드하여 에뮬레이터 없이 Android 종속 컴포넌트(Context, Resources)를 테스트할 수 있습니다. Compose 테스트에는 Compose UI Test를 사용합니다 — 하지만 이는 UI 테스트이지 단위 테스트가 아닙니다.

iOS 단위 테스트(XCTest + Quick/Nimble)

iOS 단위 테스트는 Swift로 XCTest(Xcode에 내장)를 사용하여 작성됩니다. Quick + Nimble은 더 읽기 쉬운 테스트(describe/context/it)를 위한 BDD 프레임워크입니다. 모킹에는 Cuckoo(목 생성) 또는 SwiftyMocky를 사용합니다. Swift는 프로토콜과 의존성 주입을 지원하여 종속성 교체를 용이하게 합니다. 핵심: iOS 단위 테스트는 실제 장치가 아닌 macOS 시뮬레이터에서 실행됩니다. 하드웨어 기능(카메라, 블루투스)이 필요한 테스트는 통합 테스트입니다.

Flutter 단위 테스트(flutter_test + Mockito)

Flutter 단위 테스트는 flutter_test 패키지를 사용하고 에뮬레이터 없이 Dart VM에서 실행됩니다. 모킹에는 코드 생성(build_runner)과 함께 mockito 패키지를 사용합니다. 위젯 테스트(동일 패키지)는 개별 위젯을 테스트하지만 렌더링이 필요하고 느리게 실행됩니다 — UI 로직 확인에만 사용하세요. 순수 Dart 로직(모델, 리포지토리, bloc)은 flutter_test를 임포트하지 않고 일반 Dart 테스트로 테스트됩니다.

dart
// Flutter에서 mockito를 사용한 단위 테스트 예제
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';

void main() {
    late MockApiClient mockApi;
    late UserRepository repository;

    setUp(() {
        mockApi = MockApiClient();
        repository = UserRepository(mockApi);
    });

    test('fetchUser returns user when API succeeds', () async {
        // Arrange
        final expectedUser = User(id: 1, name: 'Test');
        when(mockApi.getUser(1))
            .thenAnswer((_) async => expectedUser);

        // Act
        final result = await repository.fetchUser(1);

        // Assert
        expect(result, expectedUser);
        verify(mockApi.getUser(1)).called(1);
    });
}

모범 사례와 일반적인 실수

효과적인 단위 테스트에는 규율이 필요합니다. 주요 규칙: 동작을 테스트하고 구현을 테스트하지 마세요. 테스트는 모듈이 내부적으로 어떻게 구현되었는지(어떤 private 메서드가 어떤 순서로 호출되는지) 알 필요가 없습니다. 테스트가 구현에 결합되면 리팩터링할 때마다 깨지고 가치를 잃습니다. 테스트는 계약을 확인합니다: 입력 X에 대해 출력 Y여야 합니다. 예외는 호출 순서가 중요한 성능 크리티컬 알고리즘에 대한 테스트입니다.

  • 테스트당 하나의 확인 — 하나의 논리적 확인을 위한 하나의 assert 또는 관련 assert 그룹
  • 중복 피하기 — 공통 초기화에 @BeforeEach / setUp 사용, 다른 입력 데이터에 매개변수화된 테스트 사용
  • private 메서드 테스트하지 않기 — 공개 API를 통해 테스트. private 메서드가 커버되지 않으면 그 로직이 외부에 노출되지 않은 것
  • 경계 케이스 커버하기 — 빈 컬렉션, null/undefined, 음수, 최대값
  • 테스트에서 Thread.sleep 사용하지 않기 — 테스트를 느리고 불안정하게 만듦. 테스트 타임아웃과 코루틴 사용

어느 정도의 커버리지가 충분한가?

100% 커버리지는 달성 불가능하고 불필요한 목표입니다. Google Testing Blog(2025)에 따르면, 단위 테스트의 최적 커버리지 수준은 코드 줄의 70-80%입니다. 100% 커버리지는 종종 getter, setter, 생성자를 테스트하여 달성되며 이는 가치를 더하지 않습니다. 중요한 비즈니스 로직에 집중하세요: 복잡한 계산, 유효성 검사, 오류 처리, 엣지 케이스. 측정에는 JaCoCo(Java), Coverage.py(Python), Istanbul(JS)을 사용하고 CI에 임계값을 설정하세요 — 커버리지가 60% 미만이면 빌드 실패.

CI/CD와 단위 테스트

단위 테스트는 CI/CD 파이프라인의 첫 번째 단계입니다. 빌드 및 배포 전에 저장소에 푸시할 때마다 실행됩니다. 단위 테스트 스위트의 평균 실행 시간은 5분을 초과해서는 안 됩니다 — 그 이상이면 테스트가 "빠르지" 않게 되어 개발자가 로컬에서 실행하지 않게 됩니다. 테스트를 빠른(단위) 것과 느린(통합) 것으로 분리하고 파이프라인의 다른 단계에서 실행하세요. 속도를 위해 병렬 실행과 페일패스트를 사용하세요.

자주 묻는 질문

단위 테스트와 통합 테스트의 차이점은 무엇인가요?

단위 테스트는 외부 종속성을 목으로 대체하여 단일 모듈을 격리하여 검증합니다. 통합 테스트는 여러 실제 컴포넌트(DB, API, 파일 시스템) 간의 상호작용을 검증합니다. 단위 테스트는 밀리초 단위로 실행되고 통합 테스트는 초 단위로 실행됩니다. 테스트 피라미드에서 단위 테스트는 70%를 차지합니다.

단위 테스트에 어떤 프레임워크를 선택해야 하나요?

선택은 플랫폼에 따라 다릅니다: Java/Kotlin은 JUnit 5, iOS/Swift는 XCTest, Python은 pytest, JavaScript/TypeScript는 Jest/Vitest, Flutter는 flutter_test. 모킹에는 Mockito(Java), Cuckoo(iOS), unittest.mock(Python) 또는 vitest.mock(JS)을 사용하세요. 모든 최신 프레임워크는 매개변수화된 테스트, 내장 어서션 및 병렬 실행을 지원합니다.

F.I.R.S.T. 테스트 원칙이란 무엇인가요?

Fast — 테스트가 밀리초 단위로 실행됩니다. Isolated — 다른 테스트나 외부 시스템에 의존하지 않습니다. Repeatable — 모든 머신에서 동일한 결과를 냅니다. Self-validating — 결과를 자동으로 검증합니다. Timely — 코드보다 먼저 또는 동시에 작성됩니다. 하나의 원칙이라도 위반하면 테스트 효과가 감소합니다.

Android/iOS에서 ViewModel에 대한 단위 테스트를 작성해야 하나요?

네, 반드시 필요합니다. ViewModel에는 비즈니스 로직(이벤트 처리, 데이터 변환, 상태 관리)이 포함됩니다. Android에서는 코루틴 테스트를 위해 kotlinx-coroutines-test를, StateFlow 테스트를 위해 Turbine을 사용합니다. iOS에서는 ViewModel에서 Combine Publishers 또는 async/await를 테스트합니다. ViewModel 테스트는 에뮬레이터 없이 JVM/macOS에서 실행되는 순수 단위 테스트입니다.

네트워크 요청이 있는 코드를 어떻게 테스트하나요?

단위 테스트에서는 네트워크 요청이 실행되지 않습니다 — HTTP 클라이언트 목으로 대체됩니다. Android에서는 MockWebServer(OkHttp)를 사용하세요 — 로컬 HTTP 서버를 실행하여 실제 네트워크 상호작용을 재현하므로 목보다 선호됩니다. MockWebServer는 현실성을 잃지 않으면서 격리를 제공합니다. iOS의 경우 — OHHTTPStubs 또는 URLProtocol을 사용하여 응답을 가로채고 대체합니다.

요약

  • 단위 테스트 — 빠른 피드백으로 외부 종속성에서 격리된 개별 모듈 검증
  • AAA 구조 — Arrange(준비), Act(실행), Assert(검증) — 표준 테스트 템플릿
  • 목과 스텁 — 격리를 위한 테스트 더블: 목은 호출 확인, 스텁은 값 반환
  • TDD — 테스트 주도 개발(레드-그린-리팩터)로 결함 40-80% 감소
  • FIRST 원칙 — Fast, Isolated, Repeatable, Self-validating, Timely — 품질 테스트의 기초
  • 플랫폼 도구 — JUnit 5(Android), XCTest(iOS), flutter_test(Flutter), Jest(React Native)
  • 70-80% 커버리지 — 중요한 비즈니스 로직에 최적, getter와 setter는 테스트 불필요

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기