Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-17 00:00
관리 메뉴

nomad-programmer

[Dart] Unit Test (유닛 테스트) 본문

Programming/Flutter

[Dart] Unit Test (유닛 테스트)

scii 2020. 10. 22. 02:38

플러터에서 쓰이는 테스트 방식은 크게 3가지이다.

  1. 유닛 테스트
  2. 위젯 테스트
  3. 통합 테스트

유닛 테스트는 메소드나 클래스처럼 작은 단위를 테스트할 때 쓰인다. 그리고 외부에 의존하지 않는 테스트를 말한다.

보통 IO처리, 데이터베이스 접근하는 것을 외부에 의존한다고 하는데, 외부에 의존하는 경우는 Mockito 같은 테스트 프레임워크를 사용해 테스트한다.

다트 기본 테스트 프레임워크

test 라이브러리를 추가한다.

pub.dev/packages/test

 

test | Dart Package

A full featured library for writing and running Dart tests.

pub.dev

// pubspec.yaml

dev_dependencies:
  test: any

test 디렉토리에다 테스트 파일(simple_test.dart)을 생성한다. 파일 이름은 항상 "test"로 끝나야 한다.

// test/simple_test.dart

import 'package:test/test.dart';

void main() {
  // 어떤 테스트를 할지 설명하고, 이 안에 있는 테스트를 실행한다.
  test('should be lowercase', () {
    String hello = 'Hello World';

    // 테스트를 실행했을 때의 기대값과 실제값을 비교한다.
    expect(hello.toLowerCase(), equals('hello world'));
  });
}

 

테스트 코드는 main()에 선언해야한다. 테스트 코드는 크게 test() 함수와 expect() 함수로 구성되어 있다.

  • test() 함수 : 테스트를 실행할 때 사용하는 함수
  • expect() 함수 : 테스트 실행값과 기대값을 비교하는 함수

테스트 실행 방법은 다음과 같다.

// 소문자 변경 테스트
flutter test test/simple_test.dart

/* 결과

00:03 +1: All tests passed!

*/

Dart의 테스트 함수

  • test
    • 테스트에 대한 설명과 실제 테스트 코드를 적는다.
    • 시간 제한(timeout) 이나 테스트 환경(browser, OS) 등도 명시할 수 있다.
  • expect
    • expect(실제값, 기대값)
    • 테스트의 기대값과 실제값을 비교
    • 다른 언어의 assert 와 동일하다고 보면 된다.
  • setup
    • 테스트를 시작하기 전에 설정을 해준다.
    • 테스트 단위 하나마다 실행된다. (test() 함수 하나가 테스트 단위 하나이다. 한 파일에 여러 test() 함수가 있으면 여러번 실행된다)
  • setupAll
    • 테스트를 시작하기 전에 설정을 해준다.
    • 파일 하나에 한번만 실행된다. (데이터베이스 설정할 때 사용하기 용이하다)
  • teardown
    • 테스트를 마치고 할 작업을 정해준다.
    • 테스트 단위 하나마다 실행된다. (setup() 함수와 동일)
  • teardownAll()
    • 테스트를 마치고 할 작업을 정해준다.
    • 파일 하나에 한번만 실행된다. (setupAll() 함수와 동일)

이것뿐만 아니라 테스트 시간 제한, 비동기 테스트 등 많은 것들을 테스트할 수 있다. 


테스트 실패

// test/simple_test.dart

import 'package:test/test.dart';

void main() {
  // 어떤 페스트를 할지 설명하고, 이 안에 있는 테스트를 실행한다.
  test('should be lowercase', () {
    String hello = 'Hello World';

    // 테스트를 실행했을 때의 기대값과 실제값을 비교한다.
    expect(hello.toLowerCase(), equals('hello world'));
  });

  test('should contain name', () {
    String hello = 'Hello World, ethan';

    expect(hello.contains('john'), equals(true));
  });
}

// 실행
flutter test test/simple_test.dart

/* 결과

00:04 +1 -1: should contain name [E]
  Expected: <true>     // 기대값
    Actual: <false>    // 실제값

  package:test_api            expect
  test\simple_test.dart 15:5  main.<fn>

00:04 +1 -1: Some tests failed.

*/

테스트 실패시, 기대값과 실제값이 어떻게 다른지 보여준다. 


그룹(Group) 테스트

덧셈, 뺄셈, 스퀘어 함수를 생성하여 테스트를 해보자. group() 함수를 이용하면 여러 테스트를 묶어서 테스트할 수 있다.

// lib/calculator.dart

class Calculator {
  int add(int x, int y) => x + y;
  int minus(int x, int y) => x - y;
  int square(int x) => x * x;
}
// test/calculator_test.dart

import 'package:flutter_unit_test/calculator.dart';
import 'package:test/test.dart';

void main() {
  group('calculator', () {
    Calculator calc = Calculator();

    test('add should be equal to a + b', () {
      expect(calc.add(5, 8), equals(13));
    });

    test('minus should be equal to a - b', () {
      expect(calc.minus(10, 5), equals(5));
    });

    test('square should be equal to a * a', () {
      expect(calc.square(5), equals(25));
    });
  });
}

// 실행
flutter test test/calculator_test.dart

/* 결과

00:02 +3: All tests passed!

*/

비동기(Asynchronous) 테스트

플러터 개발할 때, 비동기 데이터를 확인할 일이 많다. Future 객체를 어떻게 테스트하는지 알아보자.

// test/asynchronous_test.dart

import 'package:test/test.dart';

void main() {
  test('new Future.value() returns the value', () {
    Future<int> value = Future<int>.value(10);

    expect(value, equals(10));
  });
}

// 실행
flutter teset test/asynchronous_test.dart

/* 결과

00:03 +0 -1: new Future.value() returns the value [E]
  Expected: <10>
    Actual: <Instance of 'Future<int>'>

  package:test_api                 expect
  test\asynchronous_test.dart 7:5  main.<fn>

00:03 +0 -1: Some tests failed.

*/

테스트를 실패하였다. 기대값은 <10> 인데, 실제값은 <Future<int>> 객체라고한다. 그렇다면 기대값을 Future객체로 바꿔주고 다시 테스트를 해보자.

// test/asynchronous_test.dart

import 'package:test/test.dart';

void main() {
  test('new Future.value() returns the value', () {
    Future<int> value = Future<int>.value(10);

    expect(value, equals(Future<int>.value(10)));
  });
}

// 실행
flutter test test/asynchronous_test.dart

/* 결과

00:03 +0 -1: new Future.value() returns the value [E]
  Expected: <Instance of 'Future<int>'>
    Actual: <Instance of 'Future<int>'>

  package:test_api                 expect
  test\asynchronous_test.dart 7:5  main.<fn>

00:03 +0 -1: Some tests failed.

*/

이번에도 테스트를 실패하였다. 기대값과 실제값 모두 Future의 인스턴스지만 같은 인스턴스가 아니라서 실패했다. 

Future를 테스트할 때는, expect(실제값, 기대값) 중 기대값을 "completion" 으로 해줘야 한다.

completion은 Future가 완료될 때까지 테스트를 종료하지 않도록 한다.
// test/asynchronous_test.dart

import 'package:test/test.dart';

void main() {
  test('new Future.value() returns the value', () {
    Future<int> value = Future<int>.value(10);

    expect(value, equals(completion(10)));
  });
}

// 실행
flutter test test/asynchronous_test.dart

/* 결과

00:03 +1: All tests passed!

*/

테스트가 성공적으로 완료된것을 확인할 수 있다. 


이메일과 주민등록번호 테스트 예제

주민등록번호는 앞에 생년월일, 뒤에는 성별 및 주소로 되어있다. 이를 확인하는 정규식을 만들어 해당 정규식이 올바르게 작동하는지 테스트해보자.

// lib/field_validator.dart

class FieldValidator {
  static bool validateSocialSecurityNumber(String input) {
    if (input.isEmpty) {
      return false;
    }
    Pattern pattern =
        r'^[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|[3][01])-([1-4][0-9]{6})';
    RegExp exp = RegExp(pattern);

    if (exp.hasMatch(input)) {
      return true;
    }
    return false;
  }
}

입력 필드를 확인하는 클래스이고 그 안에 주민등록번호를 확인하는 정적 메소드가 존재한다. 이 메소드가 잘 작동되는지 다음과 같이 테스트를 진행해본다.

// test/field_validator_test.dart

import 'package:flutter_unit_test/field_validator.dart';
import 'package:test/test.dart';

void main() {
  group('field validator test', () {
    test('validate social security number', () {
      final String socialNumber1 = '901213-2110332';
      expect(FieldValidator.validateSocialSecurityNumber(socialNumber1), true);

      final String socialNumber2 = '950728-2235385';
      expect(FieldValidator.validateSocialSecurityNumber(socialNumber2), true);

      final String socialNumber3 = '983907-2139853';
      expect(FieldValidator.validateSocialSecurityNumber(socialNumber3), false);
    });
  });
}

// 실행
flutter test test/field_validator_test.dart

/* 결과

00:04 +1: All tests passed!

*/

해당 메소드가 잘 작동하는 것을 볼 수 있다. 이번에는 이메일 유효성 체크 메소드를 테스트하는 예제를 보도록하자.

이메일은 @를 중심으로 문자와 숫자 _ 등이 포진되어있다.

// lib/field_validator.dart

class FieldValidator {
  static bool validateEmail(String email) {
    if (email.isEmpty) {
      return false;
    }

    Pattern pattern = r'^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$';
    RegExp exp = RegExp(pattern);

    if (exp.hasMatch(email)) {
      return true;
    }
    return false;
  }
}

이메일 유효성 정규식 클래스를 작성하였으며 이것을 다음과 같이 테스트해보자.

// test/field_validator_test.dart

import 'package:flutter_unit_test/field_validator.dart';
import 'package:test/test.dart';

void main() {
  group('field validator test', () {
    test('validateEmail', () {
      const String email1 = 'yuna@gmail.com';

      expect(FieldValidator.validateEmail(email1), true);

      const String email2 = 'hana^@gmail.com';

      expect(FieldValidator.validateEmail(email2), true,
          reason: '^ is a not valid character');
    });
  });
}

// 실행
flutter test test/field_validator_test.dart

/* 결과

00:03 +0 -1: field validator test validateEmail [E]
  Expected: <true>
    Actual: <false>
  ^ is a not valid character

  package:test_api                     expect
  test\field_validator_test.dart 13:7  main.<fn>.<fn>

00:03 +0 -1: Some tests failed.

*/

expect() 함수의 reason 파라미터를 이용해 테스트가 실패했을 때, 그 원인을 적어줄 수 있다.

Comments