앱과 코드 테스트하기

앱과 코드 테스트하기

package:test 라이브러리로 Dart 코드를 위한 테스트를 작성하는 방법을 배워 봐요.

출처: Test your app & code

본문

이 장에서는 Dart 코드를 위한 테스트를 작성하는 방법을 배워요. 테스트는 애플리케이션이 예상대로 동작하고, 변경 사항을 만들 때도 안정적으로 유지되게 하는 데 아주 중요해요. Dart의 인기 있는 테스트 프레임워크인 package:test 라이브러리를 사용해 이전 장에서 만든 데이터 모델에 대한 단위 테스트를 작성할 거예요.

  • test 패키지를 dev 의존성으로 추가해요.
  • test 패키지와 매처(matcher)로 구조화된 테스트를 작성해요.
  • JSON 역직렬화(deserialization) 로직을 테스트해요.

사전 준비 (Prerequisites)

이 장을 시작하기 전에 다음을 확인해 주세요:

  • 9장을 마치고 dartpedia 프로젝트가 있는, 동작하는 Dart 개발 환경을 갖췄는지 확인해요.
  • 변수, 함수, 제어 흐름 같은 기본 프로그래밍 개념에 익숙한지 확인해요.
  • 소프트웨어 개발에서 테스트의 목적을 이해하고 있는지 확인해요.

작업 (Tasks)

이 장에서는 wikipedia 패키지에 테스트를 추가해서 데이터 모델의 JSON 역직렬화 로직이 올바르게 작동하는지 확인해요.

작업 1: test 의존성 추가하기 (Add the test dependency)

먼저 test 패키지가 이미 프로젝트의 개발 의존성으로 있는지 확인해요.

  • 프로젝트 안의 wikipedia/pubspec.yaml 파일을 열어 주세요.

  • dev_dependencies 섹션을 찾아 주세요.

  • dev_dependencies 아래에 test: ^1.24.0(또는 최신 안정 버전)이 있는지 확인해 주세요:

dev_dependencies:
  lints: ^5.0.0
  test: ^1.24.0

test 의존성이 없다면 pubspec.yaml 파일에 추가해 주세요. ^ 기호는 호환되는 버전을 사용할 수 있게 해 줘요.

  • 파일을 변경했다면 pubspec.yaml을 저장하고 wikipedia 디렉터리에서 터미널에 dart pub get을 실행해 주세요. 이 명령은 새로 추가된 의존성을 가져와 프로젝트에서 사용할 수 있게 해 줘요.

다음과 비슷한 출력이 보여야 해요:

Resolving dependencies...
Downloading packages...
+ test 1.25.1
Changed 2 dependencies!

작업 2: 테스트 파일 만들고 import 추가하기 (Create a test file and add imports)

다음으로 데이터 모델을 위한 테스트 파일을 만들고 필요한 import를 추가해 주세요.

  • wikipedia/test 디렉터리로 이동해 주세요.

  • 이 디렉터리에 model_test.dart라는 새 파일을 만들어 주세요.

  • wikipedia/test/model_test.dart 파일을 열고 파일 맨 위에 다음 import 문을 추가해 주세요:

import 'dart:convert';
import 'dart:io';

import 'package:test/test.dart';
import 'package:wikipedia/src/model/article.dart';
import 'package:wikipedia/src/model/search_results.dart';
import 'package:wikipedia/src/model/summary.dart';

const String dartLangSummaryJson = './test/test_data/dart_lang_summary.json';
const String catExtractJson = './test/test_data/cat_extract.json';
const String openSearchResponse = './test/test_data/open_search_response.json';

이 줄들은 테스트 프레임워크를 제공하는 test 패키지와 테스트 대상인 데이터 모델 파일을 import해요. 상수 문자열은 샘플 데이터의 위치를 선언해요.

작업 3: 테스트 데이터 파일 만들기 (Create the test data files)

작성해야 할 테스트들은 Wikipedia API의 응답을 모방한 로컬 JSON 파일에 의존해요. test_data 디렉터리를 만들고 세 개의 파일로 채워야 해요.

  • wikipedia/test 디렉터리로 이동해 주세요.

  • test_data라는 새 디렉터리를 만들어 주세요.

  • test_data 디렉터리 안에 dart_lang_summary.json이라는 새 파일을 만들고 다음 내용을 붙여 넣어 주세요:

{
  "type": "standard",
  "title": "Dart (programming language)",
  "displaytitle": "<span class=\"mw-page-title-main\">Dart (programming language)</span>",
  "namespace": {
      "id": 0,
      "text": ""
  },
  "wikibase_item": "Q406009",
  "titles": {
    "canonical": "Dart_(programming_language)",
    "normalized": "Dart (programming language)",
    "display": "<span class=\"mw-page-title-main\">Dart (programming language)</span>"
  },
  "pageid": 33033735,
  "lang": "en",
  "dir": "ltr",
  "revision": "1259309990",
  "tid": "671bc7c6-aa67-11ef-aa2a-7c1da4fbe8fb",
  "timestamp": "2024-11-24T13:24:16Z",
  "description": "Programming language",
  "description_source": "local",
  "content_urls": {
    "desktop": {
      "page": "https://en.wikipedia.org/wiki/Dart_(programming_language)",
      "revisions": "https://en.wikipedia.org/wiki/Dart_(programming_language)?action=history",
      "edit": "https://en.wikipedia.org/wiki/Dart_(programming_language)?action=edit",
      "talk": "https://en.wikipedia.org/wiki/Talk:Dart_(programming_language)"
    },
    "mobile": {
      "page": "https://en.m.wikipedia.org/wiki/Dart_(programming_language)",
      "revisions": "https://en.m.wikipedia.org/wiki/Special:History/Dart_(programming_language)",
      "edit": "https://en.m.wikipedia.org/wiki/Dart_(programming_language)?action=edit",
      "talk": "https://en.m.wikipedia.org/wiki/Talk:Dart_(programming_language)"
    }
  },
  "extract": "Dart is a programming language designed by Lars Bak and Kasper Lund and developed by Google. It can be used to develop web and mobile apps as well as server and desktop applications.",
  "extract_html": "<p><b>Dart</b> is a programming language designed by Lars Bak and Kasper Lund and developed by Google. It can be used to develop web and mobile apps as well as server and desktop applications.</p>"
}
[
    "dart",
    [
        "Dart",
        "Darth Vader",
        "Dartmouth College",
        "Darts",
        "Darth Maul",
        "Dartford Crossing",
        "Dart (programming language)",
        "Dartmouth College fraternities and sororities",
        "Dartmoor",
        "Dartmouth, Massachusetts"
    ],
    [
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
        ""
    ],
    [
        "https://en.wikipedia.org/wiki/Dart",
        "https://en.wikipedia.org/wiki/Darth_Vader",
        "https://en.wikipedia.org/wiki/Dartmouth_College",
        "https://en.wikipedia.org/wiki/Darts",
        "https://en.wikipedia.org/wiki/Darth_Maul",
        "https://en.wikipedia.org/wiki/Dartford_Crossing",
        "https://en.wikipedia.org/wiki/Dart_(programming_language)",
        "https://en.wikipedia.org/wiki/Dartmouth_College_fraternities_and_sororities",
        "https://en.wikipedia.org/wiki/Dartmoor",
        "https://en.wikipedia.org/wiki/Dartmouth,_Massachusetts"
    ]
]

이 파일들이 준비되면 데이터 모델을 검증할 테스트를 작성할 준비가 된 거예요.

작업 4: JSON 역직렬화 테스트 작성하기 (Write tests for JSON deserialization)

이제 데이터 모델의 JSON 역직렬화 로직에 대한 테스트를 작성할 거예요. test 패키지의 group, test, expect 함수를 사용해요.

  • group 함수로 관련 테스트를 함께 묶어 주세요. wikipedia/test/model_test.dart 파일에 다음을 추가해 주세요:
void main() {
  group('deserialize example JSON responses from wikipedia API', () {
    // Tests will go here
  });
}

group 함수는 그룹의 설명과 테스트를 담은 콜백 함수를 받아요.

  • Summary 모델에 대한 테스트를 만들어 주세요. group 함수 안에 다음 test 함수를 추가해 주세요:
void main() {
  group('deserialize example JSON responses from wikipedia API', () {
    test('deserialize Dart Programming Language page summary example data from '
        'json file into a Summary object', () async {
      final String pageSummaryInput =
          await File(dartLangSummaryJson).readAsString();
      final Map<String, Object?> pageSummaryMap =
          jsonDecode(pageSummaryInput) as Map<String, Object?>;
      final Summary summary = Summary.fromJson(pageSummaryMap);
      expect(summary.titles.canonical, 'Dart_(programming_language)');
    });
  });
}

test 함수는 다음을 수행해요:

  • dart_lang_summary.json 파일의 내용을 읽어요.
  • JSON 문자열을 Map<String, Object?>로 디코딩해요.
  • Summary.fromJson 생성자로 맵에서 Summary 객체를 만들어요.
  • expect 함수로 titles 객체의 canonical 속성이 'Dart_(programming_language)'와 같은지를 단언(assert)해요.

expect 함수는 값과 매처(matcher)를 받아요. 매처는 값이 특정 기준을 충족하는지 단언하는 데 사용돼요. 이 경우 equals 매처가 값이 특정 문자열과 같은지를 단언하는 데 사용돼요.

  • Article 모델에 대한 테스트를 만들어 주세요. 이전 테스트 뒤, group 함수 안에 다음 test 함수를 추가해 주세요:
void main() {
  group('deserialize example JSON responses from wikipedia API', () {
    test('deserialize Dart Programming Language page summary example data from '
        'json file into a Summary object', () async {
      final String pageSummaryInput =
          await File(dartLangSummaryJson).readAsString();
      final Map<String, Object?> pageSummaryMap =
          jsonDecode(pageSummaryInput) as Map<String, Object?>;
      final Summary summary = Summary.fromJson(pageSummaryMap);
      expect(summary.titles.canonical, 'Dart_(programming_language)');
    });

    test('deserialize Cat article example data from json file into '
        'an Article object', () async {
      final String articleJson = await File(catExtractJson).readAsString();
      final Map<String, Object?> articleMap =
          jsonDecode(articleJson) as Map<String, Object?>;
      final List<Article> articles = Article.listFromJson(articleMap);
      expect(articles.first.title.toLowerCase(), 'cat');
    });
  });
}

test 함수는 다음을 수행해요:

  • cat_extract.json 파일의 내용을 읽어요.

  • JSON 문자열을 Map<String, Object?>로 디코딩해요.

  • Article.listFromJson 정적 메서드로 맵에서 List<Article> 객체를 만들어요.

  • expect 함수로 첫 번째 문서의 title 속성이 'cat'과 같은지를 단언해요.

  • SearchResults 모델에 대한 테스트를 만들어 주세요. 이전 테스트 뒤, group 함수 안에 다음 test 함수를 추가해 주세요:

void main() {
  group('deserialize example JSON responses from wikipedia API', () {
    test('deserialize Dart Programming Language page summary example data from '
          'json file into a Summary object', () async {
      final String pageSummaryInput =
          await File(dartLangSummaryJson).readAsString();
      final Map<String, Object?> pageSummaryMap =
          jsonDecode(pageSummaryInput) as Map<String, Object?>;
      final Summary summary = Summary.fromJson(pageSummaryMap);
      expect(summary.titles.canonical, 'Dart_(programming_language)');
    });

    test('deserialize Cat article example data from json file into '
        'an Article object', () async {
      final String articleJson = await File(catExtractJson).readAsString();
      final Map<String, Object?> articleMap =
          jsonDecode(articleJson) as Map<String, Object?>;
      final List<Article> articles = Article.listFromJson(articleMap);
      expect(articles.first.title.toLowerCase(), 'cat');
    });

    test('deserialize Open Search results example data from json file '
        'into an SearchResults object', () async {
      final String resultsString =
          await File(openSearchResponse).readAsString();
      final List<Object?> resultsAsList =
          jsonDecode(resultsString) as List<Object?>;
      final SearchResults results = SearchResults.fromJson(resultsAsList);
      expect(results.results.length, greaterThan(1));
    });
  });
}

test 함수는 다음을 수행해요:

  • open_search_response.json 파일의 내용을 읽어요.
  • JSON 문자열을 List<Object?>로 디코딩해요.
  • SearchResults.fromJson 생성자로 리스트에서 SearchResults 객체를 만들어요.
  • expect 함수로 results 리스트의 길이가 1보다 큰지를 단언해요.

작업 5: 테스트 실행하기 (Run the tests)

이제 테스트를 작성했으니 실행해서 통과하는지 확인해 봐요.

  • 터미널을 열고 wikipedia 디렉터리로 이동해 주세요.

  • dart test 명령을 실행해 주세요.

다음과 비슷한 출력이 보여야 해요:

00:02 +4: All tests passed!

이것은 세 테스트가 모두 통과한다는 것을 확인해 줘요.

복습 (Review)

배운 내용 (What you accomplished)

이번 강의에서 만들고 배운 내용을 정리해 드릴게요.

  • test 패키지를 dev 의존성으로 추가하기: pubspec.yamltest를 dev 의존성으로 추가하고 test/ 디렉터리에 테스트 파일을 만들었어요. pub dev 의존성은 프로덕션이 아니라 개발과 테스트 중에 사용하기 위한 거예요. 그 결과 lib 디렉터리에서는 사용할 수 없어요.
  • test 패키지와 매처로 구조화된 테스트 작성하기: group()으로 관련 테스트를 조직하고, test()로 개별 테스트 케이스를 정의하며, equalsgreaterThan 같은 매처와 함께 expect()로 예상 동작을 단언했어요. dart test를 실행하면 패키지가 선언한 모든 테스트가 실행돼요.
  • JSON 역직렬화 로직 테스트하기: 샘플 JSON이 담긴 테스트 데이터 파일을 만들고, fromJson 메서드가 데이터를 Dart 객체로 올바르게 파싱하는지 검증하는 테스트를 작성했어요. 이렇게 하면 모델이 실제 API 응답을 제대로 처리함을 보장해요.

퀴즈 (Quiz)

이해도를 확인해 봐요 (Check your understanding)

Q1. Dart 테스트 라이브러리에서 group, test, expect의 관계는 무엇인가요?

  • group은 관련 테스트를 조직하고, test는 개별 테스트 케이스를 정의하며, expect는 값이 기대와 일치하는지 단언해요. — 맞아요! group은 테스트를 담고, 테스트는 기대치를 담아요. 이 계층 구조는 테스트 스위트를 조직적으로 유지하고 단언을 명확하게 해 줘요.
  • group은 테스트를 실행하고, test는 단언을 정의하며, expect는 출력을 조직해요. — 아니에요. 역할이 뒤섞여 있어요. 각 이름이 시사하는 바를 떠올려 보세요. "group"은 조직을, "test"는 테스트 케이스를 암시해요.
  • grouptest는 서로 바꿔 쓸 수 있고, expect가 둘 다 실행해요. — 아니에요. 서로 바꿔 쓸 수 없어요. 각각 테스트 코드를 구조화하는 데서 뚜렷한 목적을 가져요.
  • test 전에 expect를, group 전에 test를 호출해야 해요. — 아니에요. 이 답은 중첩 순서가 뒤집혀 있어요. 어느 요소가 컨테이너이고 어느 것이 담기는지 생각해 보세요.

Q2. expect(results.length, greaterThan(1))에서 greaterThan(1)을 무엇이라고 하고 무엇을 하나요?

  • 매처(matcher)예요. 실제 값이 충족해야 할 조건을 설명해요. — 맞아요! greaterThan, equals, contains, isNull 같은 매처는 예상 조건을 설명해요. 테스트 단언을 읽기 쉽게 만들고 유용한 실패 메시지를 제공해요.
  • 검증기(validator)예요. 값이 틀리면 예외를 던져요. — 아니에요. 가깝지만 정확하진 않아요. 매처는 직접 던지지 않고, expect가 테스트를 통과/실패시키는 데 사용하는 결과를 반환해요.
  • 비교기(comparator)예요. 정렬을 위해 truefalse를 반환해요. — 아니에요. 비교기는 컬렉션 정렬에 사용돼요. 테스트 맥락에서는 다른 목적을 가져요.
  • 술어(predicate)예요. 테스트 전에 결과를 필터링해요. — 아니에요. 술어는 보통 컬렉션 필터링에 사용돼요. expect에서 두 번째 인자는 필터링이 아닌 다른 역할을 해요.

Q3. await File(path).readAsString()을 호출하는 테스트를 작성해요. 이것이 작동하려면 테스트 함수에 무엇을 추가해야 하나요?

  • 테스트 콜백을 async로 표시해요: test('...', () async { ... }) — 맞아요! 일반 Dart 함수처럼 테스트 콜백도 async일 수 있어요. 테스트 프레임워크가 반환된 Future가 완료될 때까지 자동으로 기다려요.
  • 파일 읽기를 Future.sync() 호출로 감싸요. — 아니에요. Dart에는 Future.sync()가 없어요. 함수가 await를 사용할 때 보통 무엇을 하는지 생각해 보세요.
  • test 대신 testAsync를 사용해요. — 아니에요. testAsync 함수는 없어요. 일반 test 함수도 콜백을 적절히 수정하면 비동기 연산을 처리할 수 있어요.
  • 비동기 모드를 활성화하기 위해 먼저 await Future.delayed(Duration.zero)를 호출해요. — 아니에요. 특별한 초기화나 지연은 필요 없어요. 필요한 변경은 함수 본문이 아니라 함수 시그니처에 있어요.

더 알아보기