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에 패키지를 배포합니다.