learn-flutter/content/docs/part7/unit-test.md
ChangJoo Park(박창주) 900b0ab68b update markdown
2025-05-14 09:34:13 +09:00

27 KiB

단위 테스트

소프트웨어 개발에서 테스트는 코드의 정확성을 검증하고 결함을 조기에 발견하는 데 핵심적인 역할을 합니다. 이 장에서는 Flutter 애플리케이션의 단위 테스트에 대해 다루겠습니다. 단위 테스트는 코드의 가장 작은 단위(일반적으로 함수나 메서드)가 예상대로 작동하는지 확인하는 테스트입니다.

단위 테스트의 중요성

단위 테스트는 다음과 같은 여러 이유로 중요합니다:

  1. 버그 조기 발견: 코드 변경이 기존 기능을 손상시키지 않는지 확인할 수 있습니다.
  2. 리팩토링 신뢰성: 코드를 변경하더라도 동작이 여전히 올바른지 확인할 수 있습니다.
  3. 문서화: 테스트는 코드가 어떻게 동작해야 하는지 보여주는 생생한 문서 역할을 합니다.
  4. 설계 개선: 테스트를 작성하면 종종 더 나은 코드 설계로 이어집니다.
  5. 개발 속도 향상: 장기적으로 디버깅 시간이 줄어들어 개발 속도가 빨라집니다.

Flutter에서 단위 테스트 설정하기

1. 의존성 추가

Flutter 프로젝트에서 단위 테스트를 시작하려면 pubspec.yaml 파일에 필요한 패키지를 추가해야 합니다:

dev_dependencies:
  flutter_test:
    sdk: flutter
  test: ^1.24.1

flutter_test는 Flutter SDK의 일부로, Flutter 위젯을 테스트하는 데 필요한 도구를 제공합니다. test 패키지는 일반 Dart 코드를 테스트하는 데 사용됩니다.

2. 테스트 파일 구성

테스트 파일은 일반적으로 프로젝트의 test 디렉토리에 위치합니다. 테스트 파일 이름은 관례적으로 {파일명}_test.dart 형식을 따릅니다:

my_app/
  ├── lib/
  │    ├── models/
  │    │    └── user.dart
  │    └── utils/
  │         └── validator.dart
  └── test/
       ├── models/
       │    └── user_test.dart
       └── utils/
            └── validator_test.dart

기본 단위 테스트 작성하기

1. 간단한 유틸리티 함수 테스트

먼저 테스트할 간단한 유틸리티 함수를 살펴보겠습니다:

// lib/utils/calculator.dart
class Calculator {
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
  int multiply(int a, int b) => a * b;
  double divide(int a, int b) {
    if (b == 0) {
      throw ArgumentError('Cannot divide by zero');
    }
    return a / b;
  }
}

이제 이 Calculator 클래스의 단위 테스트를 작성해 보겠습니다:

// test/utils/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/utils/calculator.dart';

void main() {
  late Calculator calculator;

  setUp(() {
    calculator = Calculator();
  });

  group('Calculator', () {
    test('add returns the sum of two numbers', () {
      // Arrange & Act
      final result = calculator.add(2, 3);

      // Assert
      expect(result, 5);
    });

    test('subtract returns the difference of two numbers', () {
      expect(calculator.subtract(5, 2), 3);
    });

    test('multiply returns the product of two numbers', () {
      expect(calculator.multiply(3, 4), 12);
    });

    test('divide returns the quotient of two numbers', () {
      expect(calculator.divide(10, 2), 5.0);
    });

    test('divide throws ArgumentError when dividing by zero', () {
      expect(
        () => calculator.divide(10, 0),
        throwsA(isA<ArgumentError>()),
      );
    });
  });
}

2. 테스트 실행하기

테스트를 실행하는 방법은 여러 가지가 있습니다:

명령줄에서 실행:

flutter test

특정 테스트 파일만 실행:

flutter test test/utils/calculator_test.dart

IDE에서 실행:

대부분의 IDE(예: VS Code, Android Studio)는 테스트 파일 옆에 실행 버튼을 제공하여 쉽게 테스트를 실행할 수 있습니다.

모델 클래스 테스트

모델 클래스의 테스트는 특히 JSON 변환과 관련된 코드를 검증하는 데 유용합니다:

// lib/models/user.dart
class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is User &&
        other.id == id &&
        other.name == name &&
        other.email == email;
  }

  @override
  int get hashCode => id.hashCode ^ name.hashCode ^ email.hashCode;
}

User 클래스에 대한 테스트:

// test/models/user_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/user.dart';

void main() {
  group('User', () {
    test('fromJson creates a User instance correctly', () {
      // Arrange
      final json = {
        'id': 1,
        'name': '홍길동',
        'email': 'hong@example.com',
      };

      // Act
      final user = User.fromJson(json);

      // Assert
      expect(user.id, 1);
      expect(user.name, '홍길동');
      expect(user.email, 'hong@example.com');
    });

    test('toJson returns correct map', () {
      // Arrange
      final user = User(
        id: 1,
        name: '홍길동',
        email: 'hong@example.com',
      );

      // Act
      final json = user.toJson();

      // Assert
      expect(json, {
        'id': 1,
        'name': '홍길동',
        'email': 'hong@example.com',
      });
    });

    test('equality works correctly', () {
      // Arrange
      final user1 = User(id: 1, name: '홍길동', email: 'hong@example.com');
      final user2 = User(id: 1, name: '홍길동', email: 'hong@example.com');
      final user3 = User(id: 2, name: '김철수', email: 'kim@example.com');

      // Assert
      expect(user1, equals(user2));
      expect(user1, isNot(equals(user3)));
    });

    test('fromJson throws when fields are missing', () {
      // Arrange
      final incompleteJson = {
        'id': 1,
        'name': '홍길동',
        // email이 누락됨
      };

      // Act & Assert
      expect(
        () => User.fromJson(incompleteJson),
        throwsA(isA<TypeError>()),
      );
    });
  });
}

비동기 코드 테스트

Flutter 앱에서는 네트워크 요청, 파일 입출력 등 비동기 코드가 흔합니다. 이러한 코드를 테스트하는 방법을 살펴보겠습니다:

// lib/services/user_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user.dart';

class UserService {
  final String baseUrl;
  final http.Client client;

  UserService({
    required this.baseUrl,
    required this.client,
  });

  Future<User> fetchUser(int id) async {
    final response = await client.get(Uri.parse('$baseUrl/users/$id'));

    if (response.statusCode == 200) {
      return User.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to load user');
    }
  }

  Future<List<User>> fetchUsers() async {
    final response = await client.get(Uri.parse('$baseUrl/users'));

    if (response.statusCode == 200) {
      final List<dynamic> userJsonList = json.decode(response.body);
      return userJsonList.map((json) => User.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load users');
    }
  }
}

UserService 클래스의 단위 테스트:

// test/services/user_service_test.dart
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/models/user.dart';
import 'package:my_app/services/user_service.dart';

import 'user_service_test.mocks.dart';

// Mockito를 사용하여 http.Client 클래스를 모방하는 Mock 클래스 생성
@GenerateMocks([http.Client])
void main() {
  late MockClient mockClient;
  late UserService userService;

  setUp(() {
    mockClient = MockClient();
    userService = UserService(
      baseUrl: 'https://api.example.com',
      client: mockClient,
    );
  });

  group('UserService', () {
    test('fetchUser returns a User if the http call completes successfully', () async {
      // Arrange
      final userData = {
        'id': 1,
        'name': '홍길동',
        'email': 'hong@example.com',
      };

      // mock 클라이언트가 GET 요청을 받으면 가짜 응답을 반환하도록 설정
      when(mockClient.get(Uri.parse('https://api.example.com/users/1')))
          .thenAnswer((_) async => http.Response(json.encode(userData), 200));

      // Act
      final user = await userService.fetchUser(1);

      // Assert
      expect(user.id, 1);
      expect(user.name, '홍길동');
      expect(user.email, 'hong@example.com');
    });

    test('fetchUser throws an exception if the http call fails', () async {
      // Arrange
      when(mockClient.get(Uri.parse('https://api.example.com/users/1')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      // Act & Assert
      expect(userService.fetchUser(1), throwsException);
    });

    test('fetchUsers returns a list of Users if the http call completes successfully', () async {
      // Arrange
      final usersData = [
        {
          'id': 1,
          'name': '홍길동',
          'email': 'hong@example.com',
        },
        {
          'id': 2,
          'name': '김철수',
          'email': 'kim@example.com',
        },
      ];

      when(mockClient.get(Uri.parse('https://api.example.com/users')))
          .thenAnswer((_) async => http.Response(json.encode(usersData), 200));

      // Act
      final users = await userService.fetchUsers();

      // Assert
      expect(users.length, 2);
      expect(users[0].id, 1);
      expect(users[0].name, '홍길동');
      expect(users[1].id, 2);
      expect(users[1].name, '김철수');
    });
  });
}

이 테스트를 실행하기 전에 Mockito 패키지를 설치하고 코드를 생성해야 합니다:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.4.2
  build_runner: ^2.4.6

그리고 다음 명령어를 실행하여 Mock 클래스를 생성합니다:

flutter pub run build_runner build

비즈니스 로직 레이어(Provider, Riverpod, Bloc 등) 테스트

상태 관리 라이브러리를 사용하는 비즈니스 로직 레이어 테스트를 살펴보겠습니다. 여기서는 Riverpod를 예시로 들겠습니다:

// lib/providers/counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'counter_provider.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() {
    state = state + 1;
  }

  void decrement() {
    if (state > 0) {
      state = state - 1;
    }
  }

  void reset() {
    state = 0;
  }
}

이 Riverpod 프로바이더의 단위 테스트:

// test/providers/counter_provider_test.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/providers/counter_provider.dart';

void main() {
  late ProviderContainer container;

  setUp(() {
    container = ProviderContainer();
  });

  tearDown(() {
    container.dispose();
  });

  group('CounterProvider', () {
    test('initial value is 0', () {
      final value = container.read(counterProvider);
      expect(value, 0);
    });

    test('increment increases state by 1', () {
      final notifier = container.read(counterProvider.notifier);

      // 초기값 확인
      expect(container.read(counterProvider), 0);

      // 증가 실행
      notifier.increment();

      // 변경된 값 확인
      expect(container.read(counterProvider), 1);
    });

    test('decrement decreases state by 1', () {
      final notifier = container.read(counterProvider.notifier);

      // 초기값을 1로 변경
      notifier.increment();
      expect(container.read(counterProvider), 1);

      // 감소 실행
      notifier.decrement();

      // 변경된 값 확인
      expect(container.read(counterProvider), 0);
    });

    test('decrement does not go below 0', () {
      final notifier = container.read(counterProvider.notifier);

      // 초기값 확인
      expect(container.read(counterProvider), 0);

      // 감소 실행
      notifier.decrement();

      // 여전히 0이어야 함
      expect(container.read(counterProvider), 0);
    });

    test('reset sets state back to 0', () {
      final notifier = container.read(counterProvider.notifier);

      // 증가 실행
      notifier.increment();
      notifier.increment();
      expect(container.read(counterProvider), 2);

      // 리셋 실행
      notifier.reset();

      // 0으로 리셋됨
      expect(container.read(counterProvider), 0);
    });

    test('confirms state changes correctly with multiple operations', () {
      final notifier = container.read(counterProvider.notifier);

      notifier.increment(); // 1
      notifier.increment(); // 2
      notifier.decrement(); // 1
      notifier.increment(); // 2
      notifier.increment(); // 3

      expect(container.read(counterProvider), 3);
    });
  });
}

Freezed 또는 json_serializable 모델 테스트

Freezed나 json_serializable을 사용하는 모델의 테스트 예시입니다:

// lib/models/product.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'product.freezed.dart';
part 'product.g.dart';

@freezed
class Product with _$Product {
  const factory Product({
    required int id,
    required String name,
    required double price,
    @Default(0) int stock,
    String? description,
    @Default([]) List<String> categories,
  }) = _Product;

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}

이 Freezed 모델의 테스트:

// test/models/product_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/product.dart';

void main() {
  group('Product', () {
    test('fromJson creates a valid Product instance', () {
      // Arrange
      final json = {
        'id': 1,
        'name': '노트북',
        'price': 1200000.0,
        'stock': 10,
        'description': '고성능 노트북',
        'categories': ['전자제품', '컴퓨터'],
      };

      // Act
      final product = Product.fromJson(json);

      // Assert
      expect(product.id, 1);
      expect(product.name, '노트북');
      expect(product.price, 1200000.0);
      expect(product.stock, 10);
      expect(product.description, '고성능 노트북');
      expect(product.categories, ['전자제품', '컴퓨터']);
    });

    test('fromJson creates a valid Product with default values', () {
      // Arrange
      final json = {
        'id': 1,
        'name': '노트북',
        'price': 1200000.0,
      };

      // Act
      final product = Product.fromJson(json);

      // Assert
      expect(product.id, 1);
      expect(product.name, '노트북');
      expect(product.price, 1200000.0);
      expect(product.stock, 0); // 기본값
      expect(product.description, null); // 기본값(null)
      expect(product.categories, isEmpty); // 기본값(빈 배열)
    });

    test('toJson returns valid JSON', () {
      // Arrange
      final product = Product(
        id: 1,
        name: '노트북',
        price: 1200000.0,
        stock: 10,
        description: '고성능 노트북',
        categories: ['전자제품', '컴퓨터'],
      );

      // Act
      final json = product.toJson();

      // Assert
      expect(json['id'], 1);
      expect(json['name'], '노트북');
      expect(json['price'], 1200000.0);
      expect(json['stock'], 10);
      expect(json['description'], '고성능 노트북');
      expect(json['categories'], ['전자제품', '컴퓨터']);
    });

    test('copyWith works correctly', () {
      // Arrange
      final product = Product(
        id: 1,
        name: '노트북',
        price: 1200000.0,
      );

      // Act
      final updatedProduct = product.copyWith(
        name: '고성능 노트북',
        price: 1300000.0,
        stock: 5,
      );

      // Assert
      expect(updatedProduct.id, 1); // 변경되지 않음
      expect(updatedProduct.name, '고성능 노트북'); // 변경됨
      expect(updatedProduct.price, 1300000.0); // 변경됨
      expect(updatedProduct.stock, 5); // 변경됨
    });

    test('두 제품이 같은 값을 가질 때 동등하게 취급된다', () {
      // Arrange
      final product1 = Product(
        id: 1,
        name: '노트북',
        price: 1200000.0,
      );

      final product2 = Product(
        id: 1,
        name: '노트북',
        price: 1200000.0,
      );

      // Assert
      expect(product1, equals(product2));
    });
  });
}

복잡한 비즈니스 로직 테스트

복잡한 비즈니스 로직이 포함된 클래스의 테스트 예시입니다:

// lib/services/cart_service.dart
import '../models/product.dart';

class CartItem {
  final Product product;
  final int quantity;

  CartItem({required this.product, required this.quantity});

  double get totalPrice => product.price * quantity;

  CartItem copyWith({int? quantity}) {
    return CartItem(
      product: product,
      quantity: quantity ?? this.quantity,
    );
  }
}

class CartService {
  final Map<int, CartItem> _items = {};

  List<CartItem> get items => _items.values.toList();

  int get itemCount => _items.values.fold(0, (sum, item) => sum + item.quantity);

  double get totalAmount => _items.values.fold(
        0,
        (sum, item) => sum + item.totalPrice,
      );

  void addProduct(Product product, {int quantity = 1}) {
    if (product.stock < quantity) {
      throw Exception('재고가 부족합니다');
    }

    if (_items.containsKey(product.id)) {
      final existingItem = _items[product.id]!;
      final newQuantity = existingItem.quantity + quantity;

      if (product.stock < newQuantity) {
        throw Exception('재고가 부족합니다');
      }

      _items[product.id] = existingItem.copyWith(quantity: newQuantity);
    } else {
      _items[product.id] = CartItem(product: product, quantity: quantity);
    }
  }

  void updateQuantity(int productId, int quantity) {
    if (!_items.containsKey(productId)) {
      throw Exception('장바구니에 해당 상품이 없습니다');
    }

    final item = _items[productId]!;

    if (quantity <= 0) {
      _items.remove(productId);
      return;
    }

    if (item.product.stock < quantity) {
      throw Exception('재고가 부족합니다');
    }

    _items[productId] = item.copyWith(quantity: quantity);
  }

  void removeProduct(int productId) {
    _items.remove(productId);
  }

  void clear() {
    _items.clear();
  }

  bool hasProduct(int productId) {
    return _items.containsKey(productId);
  }
}

CartService 클래스의 단위 테스트:

// test/services/cart_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/product.dart';
import 'package:my_app/services/cart_service.dart';

void main() {
  late CartService cartService;
  late Product laptop;
  late Product phone;

  setUp(() {
    cartService = CartService();

    laptop = Product(
      id: 1,
      name: '노트북',
      price: 1200000.0,
      stock: 10,
    );

    phone = Product(
      id: 2,
      name: '스마트폰',
      price: 800000.0,
      stock: 5,
    );
  });

  group('CartService', () {
    test('초기 장바구니는 비어있다', () {
      expect(cartService.items, isEmpty);
      expect(cartService.itemCount, 0);
      expect(cartService.totalAmount, 0);
    });

    test('상품을 장바구니에 추가할 수 있다', () {
      // Act
      cartService.addProduct(laptop);

      // Assert
      expect(cartService.items.length, 1);
      expect(cartService.items[0].product, laptop);
      expect(cartService.items[0].quantity, 1);
      expect(cartService.itemCount, 1);
      expect(cartService.totalAmount, 1200000.0);
    });

    test('같은 상품을 추가하면 수량이 증가한다', () {
      // Arrange
      cartService.addProduct(laptop);

      // Act
      cartService.addProduct(laptop);

      // Assert
      expect(cartService.items.length, 1);
      expect(cartService.items[0].quantity, 2);
      expect(cartService.itemCount, 2);
      expect(cartService.totalAmount, 2400000.0);
    });

    test('재고보다 많은 수량을 추가하려고 하면 예외가 발생한다', () {
      // Act & Assert
      expect(
        () => cartService.addProduct(laptop, quantity: 11),
        throwsException,
      );
    });

    test('장바구니에 있는 상품의 수량을 업데이트할 수 있다', () {
      // Arrange
      cartService.addProduct(laptop);

      // Act
      cartService.updateQuantity(laptop.id, 3);

      // Assert
      expect(cartService.items[0].quantity, 3);
      expect(cartService.itemCount, 3);
      expect(cartService.totalAmount, 3600000.0);
    });

    test('수량을 0 이하로 설정하면 상품이 장바구니에서 제거된다', () {
      // Arrange
      cartService.addProduct(laptop);

      // Act
      cartService.updateQuantity(laptop.id, 0);

      // Assert
      expect(cartService.items, isEmpty);
    });

    test('존재하지 않는 상품의 수량을 업데이트하려고 하면 예외가 발생한다', () {
      // Act & Assert
      expect(
        () => cartService.updateQuantity(999, 1),
        throwsException,
      );
    });

    test('상품을 장바구니에서 제거할 수 있다', () {
      // Arrange
      cartService.addProduct(laptop);
      cartService.addProduct(phone);
      expect(cartService.items.length, 2);

      // Act
      cartService.removeProduct(laptop.id);

      // Assert
      expect(cartService.items.length, 1);
      expect(cartService.items[0].product, phone);
    });

    test('장바구니를 비울 수 있다', () {
      // Arrange
      cartService.addProduct(laptop);
      cartService.addProduct(phone);
      expect(cartService.items.length, 2);

      // Act
      cartService.clear();

      // Assert
      expect(cartService.items, isEmpty);
      expect(cartService.itemCount, 0);
      expect(cartService.totalAmount, 0);
    });

    test('hasProduct는 상품의 존재 여부를 올바르게 확인한다', () {
      // Arrange
      cartService.addProduct(laptop);

      // Assert
      expect(cartService.hasProduct(laptop.id), true);
      expect(cartService.hasProduct(phone.id), false);
    });

    test('여러 상품을 추가하면 totalAmount가 올바르게 계산된다', () {
      // Act
      cartService.addProduct(laptop); // 1,200,000
      cartService.addProduct(phone, quantity: 2); // 800,000 * 2 = 1,600,000

      // Assert
      expect(cartService.itemCount, 3);
      expect(cartService.totalAmount, 2800000.0);
    });
  });
}

테스트 커버리지 측정하기

테스트 커버리지는 코드가 테스트에 의해 얼마나 실행되었는지를 나타내는 지표입니다. Flutter에서는 다음과 같이 커버리지를 측정할 수 있습니다:

flutter test --coverage

이 명령어는 테스트를 실행하고 coverage/lcov.info 파일을 생성합니다. 이 파일을 사람이 읽기 쉬운 HTML 리포트로 변환하려면 lcov 도구를 사용합니다:

genhtml coverage/lcov.info -o coverage/html

그런 다음 coverage/html/index.html 파일을 브라우저에서 열어 커버리지 리포트를 확인할 수 있습니다.

단위 테스트 모범 사례

1. AAA 패턴 사용하기

AAA(Arrange, Act, Assert) 패턴은 테스트를 구조화하는 명확한 방법을 제공합니다:

  • Arrange: 테스트에 필요한 데이터와 객체를 설정합니다.
  • Act: 테스트하려는 동작을 실행합니다.
  • Assert: 예상 결과를 실제 결과와 비교합니다.

2. 테스트 이름을 명확하게 지정하기

// 좋지 않은 예
test('add', () {
  expect(calculator.add(2, 3), 5);
});

// 좋은 예
test('add returns the sum of two numbers', () {
  expect(calculator.add(2, 3), 5);
});

3. 테스트 그룹화하기

관련 테스트를 group 함수를 사용하여 그룹화하면 테스트 출력을 더 잘 구조화할 수 있습니다:

group('Calculator', () {
  group('add', () {
    test('returns the sum of two positive numbers', () {
      // ...
    });

    test('returns the correct sum when one number is negative', () {
      // ...
    });
  });

  group('divide', () {
    test('returns the quotient of two numbers', () {
      // ...
    });

    test('throws ArgumentError when dividing by zero', () {
      // ...
    });
  });
});

4. 중복 제거하기

setUptearDown 함수를 사용하여 테스트 간에 공통 코드를 추출하면 테스트의 가독성과 유지보수성이 향상됩니다.

5. 의미 있는 어설션 사용하기

테스트가 실패했을 때 무엇이 잘못되었는지 명확하게 나타내는 어설션을 사용하세요:

// 좋지 않은 예
expect(user.toJson(), json); // 오류 메시지가 모호할 수 있음

// 좋은 예: 개별 필드 테스트
expect(user.toJson()['id'], json['id']);
expect(user.toJson()['name'], json['name']);
expect(user.toJson()['email'], json['email']);

6. 테스트 분리 유지하기

각 테스트는 독립적이어야 하며 다른 테스트나 외부 상태에 의존해서는 안 됩니다. 테스트의 순서가 결과에 영향을 미치지 않아야 합니다.

7. 경계 조건 테스트하기

함수나 메서드의 동작을 검증할 때는 일반적인 케이스뿐만 아니라 경계 조건도 테스트하세요. 예를 들어:

  • 빈 리스트나 맵
  • null 값 (nullable 필드인 경우)
  • 음수, 0, 매우 큰 숫자
  • 매우 긴 문자열 또는 빈 문자열

8. 실패 케이스 테스트하기

함수나 메서드가 오류를 올바르게 처리하는지 확인하기 위해 실패 케이스도 테스트하세요:

test('fetchUser throws exception for non-existent user', () {
  // Act & Assert
  expect(userService.fetchUser(-1), throwsException);
});

결론

단위 테스트는 코드의 신뢰성을 높이는 데 매우 중요합니다. Flutter에서 단위 테스트를 작성하면 앱의 품질이 향상되고, 버그를 조기에 발견할 수 있으며, 코드의 유지보수성이 개선됩니다. 이 장에서는 Dart 함수, 모델 클래스, 비동기 코드, 비즈니스 로직 등 다양한 코드 유형에 대한 단위 테스트 작성 방법을 살펴보았습니다.

가장 좋은 방법은 처음부터 테스트를 작성하는 것이지만, 기존 프로젝트에서도 점진적으로 테스트를 추가하여 이점을 얻을 수 있습니다. 테스트 주도 개발(TDD) 접근 방식을 사용하면 더 견고하고 유지보수하기 쉬운 코드베이스를 구축하는 데 도움이 됩니다.

다음 장에서는 위젯 테스트에 대해 자세히 알아보겠습니다. 위젯 테스트는 단위 테스트보다 한 단계 더 나아가 Flutter 위젯의 동작과 상호작용을 테스트합니다.