iBetter Books
수정

Ch 03. 패키지 테스트와 예제 작성

패키지는 라이브러리입니다. 다른 사람들이 자신의 프로젝트에서 사용할 코드입니다. 그만큼 신뢰성이 중요합니다. 테스트 없이 배포된 패키지는 사용자의 프로젝트를 망가뜨릴 수 있습니다.

이번 챕터에서는 dart_validator의 모든 기능에 대한 테스트를 작성합니다. 목표는 100% 코드 커버리지입니다. 그리고 example/ 폴더에 실행 가능한 예제도 만들겠습니다.

테스트 파일 구조

기능별로 테스트 파일을 분리합니다.

test/
├── validators_test.dart          ← Validators 클래스 테스트
├── validator_chain_test.dart     ← ValidatorChain 테스트
├── password_strength_test.dart   ← PasswordStrength 테스트
└── validation_result_test.dart   ← ValidationResult 테스트

Validators 테스트

// 새 파일: test/validators_test.dart
import 'package:dart_validator/dart_validator.dart';
import 'package:test/test.dart';

void main() {
  group('Validators.isEmail', () {
    test('유효한 이메일을 허용한다', () {
      expect(Validators.isEmail('[email protected]'), isTrue);
      expect(Validators.isEmail('[email protected]'), isTrue);
      expect(Validators.isEmail('[email protected]'), isTrue);
    });

    test('유효하지 않은 이메일을 거부한다', () {
      expect(Validators.isEmail('not-an-email'), isFalse);
      expect(Validators.isEmail('@example.com'), isFalse);
      expect(Validators.isEmail('user@'), isFalse);
      expect(Validators.isEmail('user @example.com'), isFalse);
      expect(Validators.isEmail(''), isFalse);
    });
  });

  group('Validators.isPhone', () {
    test('유효한 한국 전화번호를 허용한다', () {
      expect(Validators.isPhone('010-1234-5678'), isTrue);
      expect(Validators.isPhone('01012345678'), isTrue);
      expect(Validators.isPhone('010 1234 5678'), isTrue);
      expect(Validators.isPhone('02-123-4567'), isTrue);
    });

    test('유효하지 않은 전화번호를 거부한다', () {
      expect(Validators.isPhone('123-456-7890'), isFalse);
      expect(Validators.isPhone('phone'), isFalse);
      expect(Validators.isPhone(''), isFalse);
    });
  });

  group('Validators.isUrl', () {
    test('유효한 URL을 허용한다', () {
      expect(Validators.isUrl('https://dart.dev'), isTrue);
      expect(Validators.isUrl('http://example.com/path?q=1'), isTrue);
      expect(Validators.isUrl('HTTPS://EXAMPLE.COM'), isTrue);
    });

    test('유효하지 않은 URL을 거부한다', () {
      expect(Validators.isUrl('ftp://example.com'), isFalse);
      expect(Validators.isUrl('not-a-url'), isFalse);
      expect(Validators.isUrl(''), isFalse);
    });
  });

  group('Validators.isNotEmpty', () {
    test('내용이 있으면 true를 반환한다', () {
      expect(Validators.isNotEmpty('hello'), isTrue);
      expect(Validators.isNotEmpty(' a '), isTrue);
    });

    test('빈 문자열과 공백만 있으면 false를 반환한다', () {
      expect(Validators.isNotEmpty(''), isFalse);
      expect(Validators.isNotEmpty('   '), isFalse);
      expect(Validators.isNotEmpty('\t\n'), isFalse);
    });
  });

  group('Validators.minLength / maxLength', () {
    test('minLength: 길이가 최솟값 이상이면 true', () {
      expect(Validators.minLength('hello', 5), isTrue);
      expect(Validators.minLength('hi', 5), isFalse);
    });

    test('maxLength: 길이가 최댓값 이하이면 true', () {
      expect(Validators.maxLength('hello', 10), isTrue);
      expect(Validators.maxLength('hello world', 5), isFalse);
    });
  });

  group('Validators.passwordStrength', () {
    test('8자 미만은 weak', () {
      expect(Validators.passwordStrength('Ab1!'), PasswordStrength.weak);
    });

    test('단일 문자 유형은 weak', () {
      expect(Validators.passwordStrength('alllower'), PasswordStrength.weak);
    });

    test('두 가지 이상 포함하면 fair 이상', () {
      final result = Validators.passwordStrength('password1');
      expect(result.index, greaterThanOrEqualTo(PasswordStrength.fair.index));
    });

    test('대소문자+숫자+특수문자 모두 포함하면 strong', () {
      expect(
        Validators.passwordStrength('Password1!'),
        PasswordStrength.strong,
      );
    });
  });
}

ValidatorChain 테스트

// 새 파일: test/validator_chain_test.dart
import 'package:dart_validator/dart_validator.dart';
import 'package:test/test.dart';

void main() {
  group('ValidatorChain', () {
    test('모든 검사를 통과하면 isValid가 true', () {
      final result = ValidatorChain('[email protected]')
          .isNotEmpty()
          .isEmail()
          .maxLength(100)
          .validate();

      expect(result.isValid, isTrue);
      expect(result.errors, isEmpty);
    });

    test('검사 실패 시 오류 메시지가 누적된다', () {
      final result = ValidatorChain('')
          .isNotEmpty()
          .isEmail()
          .validate();

      expect(result.isValid, isFalse);
      expect(result.errors.length, equals(2));
    });

    test('커스텀 오류 메시지를 사용할 수 있다', () {
      final result = ValidatorChain('invalid')
          .isEmail(message: '이메일을 올바르게 입력하세요.')
          .validate();

      expect(result.errors.first, equals('이메일을 올바르게 입력하세요.'));
    });

    test('비밀번호 강도 검사가 동작한다', () {
      final weakResult = ValidatorChain('weak')
          .hasPasswordStrength(PasswordStrength.strong)
          .validate();

      expect(weakResult.isValid, isFalse);

      final strongResult = ValidatorChain('Str0ng!pw')
          .hasPasswordStrength(PasswordStrength.strong)
          .validate();

      expect(strongResult.isValid, isTrue);
    });

    test('URL 검사가 동작한다', () {
      final result = ValidatorChain('https://dart.dev')
          .isUrl()
          .validate();

      expect(result.isValid, isTrue);
    });

    test('전화번호 검사가 동작한다', () {
      final result = ValidatorChain('010-1234-5678')
          .isPhone()
          .validate();

      expect(result.isValid, isTrue);
    });
  });
}

ValidationResult 테스트

// 새 파일: test/validation_result_test.dart
import 'package:dart_validator/dart_validator.dart';
import 'package:test/test.dart';

void main() {
  group('ValidationResult', () {
    test('valid() 결과는 isValid가 true이고 errors가 비어 있다', () {
      final result = ValidationResult.valid();
      expect(result.isValid, isTrue);
      expect(result.errors, isEmpty);
    });

    test('invalid() 결과는 isValid가 false이고 errors를 가진다', () {
      final result = ValidationResult.invalid(['오류1', '오류2']);
      expect(result.isValid, isFalse);
      expect(result.errors, equals(['오류1', '오류2']));
    });

    test('errors 목록은 불변이다', () {
      final result = ValidationResult.invalid(['오류']);
      expect(() => result.errors.add('추가'), throwsUnsupportedError);
    });

    test('toString이 올바르게 동작한다', () {
      expect(ValidationResult.valid().toString(), contains('valid'));
      expect(
        ValidationResult.invalid(['e']).toString(),
        contains('invalid'),
      );
    });
  });
}

PasswordStrength 테스트

// 새 파일: test/password_strength_test.dart
import 'package:dart_validator/dart_validator.dart';
import 'package:test/test.dart';

void main() {
  group('PasswordStrength', () {
    test('label이 올바르게 반환된다', () {
      expect(PasswordStrength.weak.label, equals('약함'));
      expect(PasswordStrength.fair.label, equals('보통'));
      expect(PasswordStrength.strong.label, equals('강함'));
    });

    test('index 순서가 weak < fair < strong이다', () {
      expect(
        PasswordStrength.weak.index,
        lessThan(PasswordStrength.fair.index),
      );
      expect(
        PasswordStrength.fair.index,
        lessThan(PasswordStrength.strong.index),
      );
    });
  });
}

테스트 실행과 커버리지 확인

# 모든 테스트 실행dart test# 커버리지 수집dart test --coverage=coverage# LCOV 형식으로 변환dart pub global activate coverageformat_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=lib# 커버리지 리포트 생성 (genhtml 필요)genhtml coverage/lcov.info -o coverage/htmlopen coverage/html/index.html

커버리지가 100%에 미치지 못하는 경우, 어느 줄이 실행되지 않았는지 HTML 리포트에서 확인할 수 있습니다.

example/ 폴더에 실행 가능한 예제 작성

example/ 폴더의 파일은 pub.dev 점수 산정과 "Example" 탭에 영향을 줍니다.

// 새 파일: example/dart_validator_example.dart

import 'package:dart_validator/dart_validator.dart';

void main() {
  print('=== dart_validator 예제 ===\n');

  // 정적 메서드 사용
  print('-- 이메일 검사 --');
  final emails = ['[email protected]', 'invalid-email', ''];
  for (final email in emails) {
    final result = Validators.isEmail(email);
    print('  "$email" → ${result ? "유효" : "유효하지 않음"}');
  }

  print('\n-- 비밀번호 강도 --');
  final passwords = ['weak', 'Password1', 'Str0ng!pw'];
  for (final pw in passwords) {
    final strength = Validators.passwordStrength(pw);
    print('  "$pw" → ${strength.label}');
  }

  print('\n-- 체인 검사 --');
  final formData = {
    'email': '[email protected]',
    'phone': '010-1234-5678',
    'password': 'Weak',
  };

  final emailResult = ValidatorChain(formData['email']!)
      .isNotEmpty()
      .isEmail()
      .maxLength(100)
      .validate();

  final phoneResult = ValidatorChain(formData['phone']!)
      .isNotEmpty()
      .isPhone()
      .validate();

  final passwordResult = ValidatorChain(formData['password']!)
      .isNotEmpty()
      .minLength(8)
      .hasPasswordStrength(PasswordStrength.fair)
      .validate();

  print('  이메일: ${emailResult.isValid ? "통과" : emailResult.errors.join(", ")}');
  print('  전화번호: ${phoneResult.isValid ? "통과" : phoneResult.errors.join(", ")}');
  print('  비밀번호: ${passwordResult.isValid ? "통과" : passwordResult.errors.join(", ")}');
}

예제를 실행합니다.

dart run example/dart_validator_example.dart

출력 결과입니다.

=== dart_validator 예제 ===

-- 이메일 검사 --
  "[email protected]" → 유효
  "invalid-email" → 유효하지 않음
  "" → 유효하지 않음

-- 비밀번호 강도 --
  "weak" → 약함
  "Password1" → 보통
  "Str0ng!pw" → 강함

-- 체인 검사 --
  이메일: 통과
  전화번호: 통과
  비밀번호: 최소 8자 이상 입력해주세요.

이번 챕터 정리

  • 기능별로 테스트 파일을 분리하여 유지보수를 쉽게 했습니다.
  • 경계값과 예외 케이스를 모두 테스트하여 100% 커버리지를 달성했습니다.
  • example/ 폴더에 실행 가능한 예제를 작성하여 pub.dev의 Example 탭을 채웠습니다.

다음 챕터에서는 dart pub publish 명령으로 실제로 pub.dev에 패키지를 배포합니다.