개발

Flutter 테스트 기초: 왜 테스트를 작성해야 하는가?

자동화된 테스트로 더 안전하고 확실한 개발하기

2025년 9월 18일
25분 읽기
Flutter 테스트 기초: 왜 테스트를 작성해야 하는가?

테스트란 무엇인가?

테스트는 우리가 작성한 코드가 의도한 대로 동작하는지
자동으로 확인하는 코드입니다. 마치 수학 문제를 풀고 검산하는 것처럼,
프로그래밍에서도 우리가 만든 기능이 제대로 작동하는지 검증하는 과정입니다.

수동으로 앱을 실행해서 버튼을 눌러보고, 화면을 확인하는 것도 테스트입니다.
하지만 앱이 커질수록 모든 기능을 매번 수동으로 확인하는 것은 불가능해집니다.
그래서 자동화된 테스트가 필요합니다.

테스트를 작성하지 않으면 생기는 문제

실제 개발 상황을 생각해봅시다. 장바구니 기능을
만들었다고 가정하겠습니다.

처음에는 간단합니다. 상품을 담고, 삭제하고, 총액을 계산하는 기능만 있습니다.
수동으로 테스트하는 데 5분이면 충분합니다.

하지만 시간이 지나면서 기능이 추가됩니다:

할인 쿠폰 적용
배송비 계산
재고 확인
구매 수량 제한
회원 등급별 할인

이제 모든 조합을 수동으로 테스트하려면 1시간이 넘게 걸립니다. 게다가 사람이
하는 일이라 실수할 가능성도 있습니다. 어떤 시나리오는 깜빡하고 놓칠 수도 있죠.

더 큰 문제는 리팩토링이 두려워진다는 것입니다. 코드를 개선하고 싶어도 "혹시
다른 기능이 깨지면 어쩌지?"라는 걱정 때문에 손대지 못하게 됩니다. 결국 코드는
점점 복잡해지고 유지보수가 어려워집니다.

테스트의 이점

1. 자신감 있는 변경

테스트가 있으면 코드를 수정한 후 즉시 문제를 발견할 수
있습니다. 예를 들어, 할인 계산 로직을 개선했는데 테스트가 실패한다면, 뭔가
잘못되었다는 것을 바로 알 수 있습니다.
Dart
// 할인 계산 함수를 수정했다고 가정
  double calculateDiscount(double price, double discountRate) {
    // 기존: return price * discountRate;
    // 수정: return price * (1 - discountRate);  // 실수!
    return price * (1 - discountRate);
  }

  // 테스트가 있다면 즉시 문제 발견
  test('10% 할인 계산', () {
    final result = calculateDiscount(100, 0.1);
    expect(result, equals(10));  // 실패! 90이 나옴
  });

2. 문서화 효과

테스트는 코드가 어떻게 사용되어야 하는지 보여주는
살아있는 문서입니다. 새로운 팀원이 합류했을 때, 테스트를 보면 각 기능이 어떻게
동작해야 하는지 명확히 알 수 있습니다.
Dart
test('장바구니는 최대 10개 상품만 담을 수 있다', () {
    final cart = ShoppingCart();
    
    // 10개 추가 - 성공해야 함
    for (int i = 0; i < 10; i++) {
      cart.addItem(Product(id: '$i'));
    }
    expect(cart.itemCount, equals(10));
    
    // 11번째 추가 - 실패해야 함
    expect(
      () => cart.addItem(Product(id: '11')),
      throwsException,
    );
  });
이 테스트를 보면 "아, 장바구니는 10개까지만 담을 수
있구나"라는 비즈니스 규칙을 바로 이해할 수 있습니다.

3. 디버깅 시간 단축

버그가 발생했을 때, 테스트가 있으면 문제를 빠르게 찾을
수 있습니다. 어떤 테스트가 실패하는지 보면, 어느 부분에 문제가 있는지 범위를
좁힐 수 있습니다.

4. 리팩토링의 안전망

코드를 개선하고 싶을 때, 테스트는 안전망 역할을 합니다.
내부 구현을 바꿔도 테스트가 통과한다면, 기능은 여전히 제대로 동작한다는
확신을 가질 수 있습니다.

Flutter의 테스트 종류

Flutter는 세 가지 종류의 테스트를 제공합니다. 각각의
목적과 범위가 다릅니다.

1. Unit Test (단위 테스트)

가장 작은 단위의 코드를 테스트합니다. 주로 함수,
클래스, 메서드를 대상으로 합니다.

특징:
• 가장 빠르게 실행됩니다 (밀리초 단위)
• 외부 의존성이 없습니다
• 가장 많이 작성해야 합니다

테스트 대상:
• 비즈니스 로직
• 유틸리티 함수
• 데이터 변환
• 계산 로직

2. Widget Test (위젯 테스트)

개별 위젯이 올바르게 렌더링되고 상호작용하는지
테스트합니다.

특징:
• Unit Test보다 느리지만 여전히 빠릅니다
• 실제 기기가 필요 없습니다
• UI 컴포넌트의 동작을 확인합니다

테스트 대상:
• 위젯의 렌더링
• 사용자 상호작용 (탭, 스크롤)
• 위젯 간의 통합

3. Integration Test (통합 테스트)

전체 앱 또는 큰 기능 단위를 테스트합니다. 실제 기기나
에뮬레이터에서 실행됩니다.

특징:
• 가장 느립니다 (초~분 단위)
• 실제 환경과 가장 유사합니다
• 가장 적게 작성합니다

테스트 대상:
• 전체 사용자 시나리오
• 여러 화면 간의 네비게이션
• 실제 API 통합

테스트 피라미드

테스트 전략에서 중요한 개념이 "테스트 피라미드"입니다.
이는 각 테스트 종류를 얼마나 작성해야 하는지 가이드라인을 제공합니다.
text
       /\
        /통합\     ← 적게 (10%)
       /------\
      /위젯 테스트\  ← 적당히 (30%)
     /------------\
    /              \
   / 단위 테스트     \ ← 많이 (60%)
  /________________\
피라미드 모양인 이유:

아래로 갈수록 많이: Unit Test를 가장 많이 작성합니다
아래로 갈수록 빠름: Unit Test가 가장 빠르게 실행됩니다
위로 갈수록 현실적: Integration Test가 실제 사용과 가장 유사합니다

좋은 테스트의 특징

1. Fast (빠른 실행)

테스트는 빨라야 합니다. 개발하면서 자주 실행해야 하기
때문입니다. 전체 테스트 스위트가 몇 분 안에 실행되어야 이상적입니다.

2. Independent (독립적)

각 테스트는 독립적이어야 합니다. 다른 테스트의 실행
순서나 결과에 영향받으면 안 됩니다.
Dart
// ❌ 나쁜 예: 전역 상태에 의존
  int globalCounter = 0;

  test('첫 번째 테스트', () {
    globalCounter++;
    expect(globalCounter, equals(1));
  });

  test('두 번째 테스트', () {
    globalCounter++;
    expect(globalCounter, equals(2));  // 첫 번째 테스트가 먼저 실행되어야만 
  통과
  });

  // ✅ 좋은 예: 각 테스트가 독립적
  test('카운터 증가', () {
    final counter = Counter();
    counter.increment();
    expect(counter.value, equals(1));
  });

  test('카운터 감소', () {
    final counter = Counter(initialValue: 5);
    counter.decrement();
    expect(counter.value, equals(4));
  });

3. Repeatable (반복 가능)

같은 테스트를 여러 번 실행해도 같은 결과가 나와야
합니다. 랜덤 값이나 현재 시간에 의존하면 안 됩니다.

4. Self-Validating (자체 검증)

테스트는 성공 또는 실패를 명확히 판단할 수 있어야
합니다. 수동으로 로그를 확인하거나 결과를 해석할 필요가 없어야 합니다.

5. Timely (적시에 작성)

테스트는 코드와 함께 작성되어야 합니다. 나중에
작성하려고 미루면 결국 작성하지 않게 됩니다.

테스트 작성을 시작하는 방법

1단계: 가장 중요한 비즈니스 로직부터

처음부터 모든 것을 테스트하려고 하지 마세요. 가장
중요하고 복잡한 비즈니스 로직부터 시작하세요.

예를 들어, 쇼핑몰 앱이라면:
가격 계산 로직
할인 적용 규칙
재고 확인 로직

2단계: 버그가 발견되면 테스트 추가

버그를 수정할 때마다 해당 버그를 재현하는 테스트를 먼저
작성하세요. 이렇게 하면:

• 버그가 정말 수정되었는지 확인할 수 있습니다
• 같은 버그가 다시 발생하지 않습니다

3단계: 새 기능을 추가할 때 테스트도 함께

새로운 기능을 개발할 때는 테스트도 함께 작성하세요.
TDD(Test-Driven Development)까지는 아니더라도, 기능 구현 직후에 테스트를
작성하는 습관을 들이세요.

테스트 작성의 기본 구조

모든 테스트는 기본적으로 AAA 패턴을 따릅니다:

Arrange (준비): 테스트에 필요한 데이터와 환경을 준비합니다
Act (실행): 테스트하고자 하는 동작을 실행합니다
Assert (확인): 결과가 예상과 일치하는지 확인합니다
Dart
test('할인 쿠폰 적용 시 가격이 감소한다', () {
    // Arrange: 준비
    final product = Product(name: 'Laptop', price: 1000);
    final coupon = DiscountCoupon(rate: 0.1);  // 10% 할인
    
    // Act: 실행
    final discountedPrice = applyDiscount(product.price, coupon);
    
    // Assert: 확인
    expect(discountedPrice, equals(900));
  });

정리

테스트는 선택이 아니라 필수입니다. 처음에는 테스트
작성이 시간 낭비처럼 느껴질 수 있지만, 프로젝트가 커질수록 그 가치를 실감하게
됩니다.

테스트가 있으면:

• 버그를 빨리 발견합니다
• 코드 변경이 두렵지 않습니다
• 코드가 어떻게 동작해야 하는지 명확해집니다
• 장기적으로 개발 시간이 단축됩니다

다음 편에서는 Flutter에서 Unit Test를 작성하는 구체적인 방법을
알아보겠습니다.
#Flutter
#Testing
#Unit Test
#Widget Test
#Integration Test
#TDD
#Best Practices