コンテンツにスキップ

飛行計画機能 forDemo テスト方針

10月デモ向けのテスト方針を示す。

以下2つの観点からテストを行う。

  • End-to-Endテスト : Testcontainersを利用し、APP・DB(PostgreSQL/PostGIS)をコンテナ起動した実環境に対してAPIを実行する疎通テストとして実施する。
  • Unitテスト : Repository層をモック化し、関数単体(UseCase等)の評価を行うテストとして実施する。
  • ユースケースflight-plan-usecase.mdから抽出したテストパターンを評価観点とした試験を実施する。
  • 各APIの異常系パターンについてはOAS flight-planning.yamlから抽出する。
  • Testcontainersを用い、APP・DB(PostgreSQL/PostGIS)をコンテナ起動した状態でAPIを実行するEnd-to-End統合の自動テスト環境とする。
  • 飛行計画通報(reportFlightPlan)は模擬DIPSを呼ぶため、Prismで立てたモックサーバーへ宛先を切り替える(ArchitecturePolicy_Flightplanning_forDemo.md17節)。モック定義はtests/mocks/mock-dips/flightplan.yaml、接続先はtestプロファイルで平文のPrismコンテナへ向ける。他のAPIは外部システム連携を行わないため切替は不要である(deleteFlightPlanのDIPS取り下げは対象外。BusinessLogicSpecifications.md4.6節)。
  • 模擬DIPSのモックは4xx/5xxを定義していないため、通報の異常系はEnd-to-Endテストでは再現しない(Unitテストで賄う。2節参照)。
  • forDemoでは機体・操縦者のCRUD APIを提供しないため、あらかじめフロントエンドに共有したデータをデータベースに登録しておくことで、飛行計画との紐づけ(pilotInfo.pilotId/aircraftIds)を行う。

10月デモでは、飛行計画機能はArchitecturePolicy_Flightplanning_forDemo.md冒頭記載の対象API(仮登録・本登録・一覧取得・詳細取得・更新・通報・削除)について、下記のパターンに絞ってテストを実施する。各パターンの根拠は同ユースケースのうち10月デモ対象(UC-PLAN-01・UC-PLAN-03・UC-DIPS-03・UC-PLAN-04およびそのサブユースケース)を基にする。

  • 正常系
    • 飛行計画仮登録(TP-1: createFlightPlan)実行時(UC-PLAN-01手順1〜9,11,12「保存」の場合)
      • DIPS必須項目(name/flightPurposes/departurePoint/flightPeriod/flightSpec/flyRoute/destinationPoint/riskMitigation/pilotInfo/email)をすべて指定して実行時
        • 飛行計画(FLIGHT_PLAN)レコードがDBに作成されること
        • 初版の飛行計画リビジョン(FLIGHT_PLAN_REVISIONrevision_no=1change_type=CREATE)が作成されること
          • ステータスがDRAFTで登録されること
        • 飛行目的(UC-PLAN-01-1、flightPurposes)がFLIGHT_PLAN_DIPS_PURPOSEに1コード1行で保存されること
        • 操縦者・機体(UC-PLAN-01-2/01-3、pilotInfo)の組がFLIGHT_PLAN_PILOT_ASSIGNMENTに保存されること(1節の前提の通り、事前にDBへ登録済みのマスタを参照する)
        • DIPS通報時にのみ使用する項目(保険情報・許可承認情報等)がFLIGHT_PLAN_DIPS_ATTRS.dips_report_detailにjsonb形式で保存されること
      • 経路/エリア(UC-PLAN-01-11、flyRoute)の指定パターンごとに実行時
        • type=circleで指定時、FLIGHT_PLAN_AREA.area_type=CIRCLEで保存されること
        • type=polygonで指定時、area_type=POLYGONで保存されること
        • type=pathで指定時、area_type=ROUTEで保存されること
      • 保険(UC-PLAN-01-4、insuranceInformation)を指定して実行時
        • dips_report_detail.insuranceInformationに保存されること
      • 許可承認(UC-PLAN-01-5、flightPermitApplicationInfo)を指定して実行時
        • dips_report_detail.flightPermitApplicationInfoに保存されること
      • Idempotency-Keyヘッダー指定で同一キー・同一内容の再送時
        • 重複作成されず、最初に作成された飛行計画が201で返ること
    • 飛行計画本登録(TP-2: registerFlightPlan)実行時(UC-PLAN-01手順9〜12「保存」の場合の後半)
      • DRAFT状態の飛行計画を対象に実行時
        • status=ACCEPTEDの新リビジョン(change_type=ACCEPT)が作成されること
      • 飛行領域が空域制限と重なる飛行計画(UC-PLAN-01-13)を対象に実行時
        • 競合した空域制限がconflict.airspaceRestrictionsに返り、CONFLICT_DETECTIONに作成したリビジョンと紐づけて記録されること
        • 詳細取得が同じ内容を返すこと(再判定せず記録済みの結果を読むこと)
        • 判定条件(形状ごとの重なり・有効期間・取得元から消えた空域制限の除外)の網羅はEnd-to-Endテストでは行わず、実DBに対するIntegrationテスト(AirspaceRestrictionConflictDetectorIntegrationTest)で検証する
      • Idempotency-Keyヘッダー指定で同一キーの再送時
        • 重複して本登録処理が行われず、最初の実行結果が200で返ること
    • 飛行計画一覧取得(TP-3: listFlightPlans)実行時(UC-PLAN-01手順2、UC-PLAN-03手順1〜3)
      • 絞り込み条件なしで実行時
        • 登録済みの飛行計画が登録順(登録日時昇順)で一覧取得できること
      • 検索対象期間(periodFrom/periodTo)を指定して実行時
        • 飛行時間帯(startTimestartTime+plannedFlightTime)が指定期間と少しでも重なる飛行計画のみが対象となること
      • 登録者ID(registeredUserId)を指定して実行時
        • 指定ユーザーが登録した飛行計画のみが対象となること
    • 飛行計画詳細取得(TP-4: getFlightPlan)実行時(UC-PLAN-03手順3〜6)
      • 本登録済み(ACCEPTED)の飛行計画IDを指定して実行時
        • 飛行計画の詳細項目(flightPlan配下の全入力項目)が取得できること
      • DRAFT状態(name以外未入力)の飛行計画IDを指定して実行時
        • 未入力の項目がnullまたはキー省略で返ること
      • 飛行経路がpolygonの飛行計画IDを指定して実行時
        • 頂点が終点(始点との重複)を含まない配列で返ること(OASのGeoJsonPolygonの数え方。ドメイン・DBは閉環で保持する)
        • 取得したflyRouteをそのまま「飛行計画更新」へ詰め直せること(GET→編集→PUTの経路)
      • 詳細取得のflightPlanをそのまま「飛行計画更新」のリクエストボディへ送って実行時
        • 200が返り、内容が変わらないこと(レスポンス専用の項目pilotInfo[].pilotName/aircraftNamesが除かれ、他の項目は往復しても値が変わらないこと)
      • jsonb(dips_report_detaildraft_fields)の値がAPIの型・コードの値域・OASのrequiredに適合しない飛行計画IDを指定して実行時
        • 当該項目のみが未設定(キー省略)で返り、他の項目と応答全体は成功すること(1項目の不適合で飛行計画が取得不能にならないこと)
        • どの飛行計画のどの項目が落ちたかがログから特定できること
    • 飛行計画更新(TP-5: updateFlightPlan)実行時(UC-PLAN-03手順7〜9)
      • DRAFT状態の飛行計画を対象に一部の項目のみを指定して実行時
        • 指定した項目のみがFLIGHT_PLAN_DRAFT.draft_fieldsに反映され、キーを省略した項目は変更されないこと
        • statusDRAFTのまま維持され、リビジョン(FLIGHT_PLAN_REVISION)が作成されないこと
        • flightPeriodを変更した場合にplanned_start_at/planned_end_atが再計算されること
      • 明示的にnullを指定して実行時
        • 当該項目が未入力の状態(キーごと削除)に戻ること
      • ACCEPTED状態の飛行計画を対象に実行時
        • revision_noが+1されchange_type=UPDATEparent_revision_id=更新前の現在リビジョンの新リビジョンが作成されること
        • FLIGHT_PLAN.current_revision_idが新リビジョンに差し替わり、statusACCEPTEDのまま維持されること
        • 指定しなかった項目が更新前のリビジョンから引き継がれること
      • 飛行経路(flyRoute)の種別を変更して実行時
        • 新しい種別のFLIGHT_PLAN_AREA(およびそのサブタイプ行)で置き換わり、旧種別のサブタイプ行が残らないこと
      • DIPS通報済み(reportStatus=REPORTED)の飛行計画を対象に、内容を変更して実行時
        • reportStatusUNREPORTEDに戻ること(statusは変わらないこと)
      • ACCEPTED状態の飛行計画を対象に、内容が変わらないリクエスト(空のボディ{}・現在値と同じ値の指定)で実行時
        • 200が返ること
        • リビジョン(FLIGHT_PLAN_REVISION)・状態遷移イベント(FLIGHT_PLAN_STATE_EVENT)が増えず、FLIGHT_PLAN.current_revision_idupdated_atも変わらないこと
        • DIPS通報済み(reportStatus=REPORTED)の場合はREPORTEDのまま維持されること
        • 飛行経路がpolygonの場合も、復元(toPatchableFields)による差でリビジョンが作成されないこと
    • 飛行計画通報(TP-6: reportFlightPlan)実行時(UC-DIPS-03手順2〜6)
      • 未通報(reportStatus=UNREPORTED)のACCEPTEDの飛行計画を対象に実行時
        • reportStatusREPORTEDになり、statusACCEPTEDのまま変わらないこと
        • 模擬DIPSが採番した受付番号がdipsFlightPlanIdとして返り、DIPS_REPORTに1行追加されること
        • FLIGHT_PLAN_STATE_EVENTREPORT_STARTREPORT_COMPLETEが順に追記されること
      • 通報済みの飛行計画を更新してUNREPORTEDに戻したあと、再度実行時
        • 模擬DIPSへ受付番号を指定して送信されること(新規登録ではなく更新として扱われること)
        • dipsFlightPlanIdが初回通報時の値から変わらないこと
    • 飛行計画削除(TP-7: deleteFlightPlan)実行時(UC-PLAN-04)
      • DRAFT状態の飛行計画を対象に実行時
        • FLIGHT_PLAN.deleted_atが設定され、行自体は物理削除されないこと
        • 一時保存内容(FLIGHT_PLAN_DRAFT)の行が物理削除されること
        • statusDRAFTのまま維持され、状態遷移イベント(FLIGHT_PLAN_STATE_EVENT)が追記されないこと
      • ACCEPTED状態の飛行計画を対象に実行時
        • FLIGHT_PLAN.deleted_atが設定され、リビジョン(FLIGHT_PLAN_REVISION以下)が残ること
      • 削除後に飛行計画詳細取得・飛行計画一覧取得を実行時
        • 詳細取得は404となり、一覧取得の結果に当該飛行計画が含まれないこと
      • Idempotency-Keyヘッダー指定で同一キーの再送時
        • 重複して削除処理が行われず、最初の実行結果が204で返ること
  • 異常系
    • 飛行計画仮登録(TP-1: createFlightPlan)実行時
      • リクエストボディのパース失敗(不正なJSON・型不一致等)時
        • 400エラーが返ること
      • DIPS必須項目のいずれかが未入力等のバリデーションエラー時
        • 422エラーが返ること
      • Idempotency-Keyヘッダー指定で同一キーを異なるリクエストボディで再利用した場合
        • 422エラーが返ること
    • 飛行計画本登録(TP-2: registerFlightPlan)実行時
      • パスパラメータflightPlanIdのパース失敗(不正な形式)時
        • 400エラーが返ること
      • 対象の飛行計画IDが存在しない場合
        • 404エラーが返ること
      • DRAFT以外の状態の飛行計画を対象に実行時
        • 409エラーが返ること
      • 他の処理によるロック中でアクセス不可の場合
        • 409エラーが返ること
      • 必須入力項目(name/flightPurposes/departurePoint/flightPeriod/flightSpec/flyRoute/destinationPoint/riskMitigation/pilotInfo/email)のいずれかが未入力のDRAFT状態の飛行計画を対象に実行時
        • 422エラーが返ること
      • 飛行開始予定日時(flightPeriod.startTime)が現在日時より過去のDRAFT状態の飛行計画を対象に実行時
        • 422エラーが返ること
      • 飛行目的flightPurposes[].codeOTHER_BUSINESS/OTHER_NON_BUSINESSの場合に対応するnoteを指定せず実行時
        • 422エラーが返ること
      • 飛行経路種別flyRoute.type=routeでバッファ幅flyRoute.bufferMが0以下または100を超えるDRAFT状態の飛行計画を対象に実行時
        • 422エラーが返ること
      • 飛行経路種別flyRoute.type=polygonで多角形が自己交差するDRAFT状態の飛行計画を対象に実行時
        • 422エラーが返ること
      • 飛行許可・承認情報(flightPermitApplicationInfo)の項目間の整合性(permitDate <= startDate <= finishDate、飛行開始予定日時のJST暦日が許可・承認期間内)に違反するDRAFT状態の飛行計画を対象に実行時
        • 422エラーが返ること
      • Idempotency-Keyヘッダー指定で同一キーを異なる飛行計画IDに対して再利用した場合
        • 422エラーが返ること
    • 飛行計画一覧取得(TP-3: listFlightPlans)実行時
      • クエリパラメータperiodFrom/periodToの形式が不正な場合
        • 400エラーが返ること
    • 飛行計画詳細取得(TP-4: getFlightPlan)実行時
      • パスパラメータflightPlanIdの形式が不正な場合
        • 400エラーが返ること
      • 対象の飛行計画IDが存在しない場合
        • 404エラーが返ること
    • 飛行計画更新(TP-5: updateFlightPlan)実行時
      • リクエストボディのパース失敗(不正なJSON・型不一致等)時、またはパスパラメータflightPlanIdの形式が不正な場合
        • 400エラーが返ること
      • 対象の飛行計画IDが存在しない場合
        • 404エラーが返ること
      • 更新できない状態(CANCELLEDENDED)の飛行計画を対象に実行時
        • 409エラーが返ること
      • 他の処理によるロック中でアクセス不可の場合
        • 409エラーが返ること
      • ACCEPTED以降の飛行計画で必須入力項目を明示的にnullでクリアして実行時
        • 422エラーが返ること
      • ACCEPTED以降の飛行計画で飛行開始予定日時(flightPeriod.startTime)が現在日時より過去になるよう実行時
        • 422エラーが返ること
      • nameに明示的なnullを指定して実行時
        • 422エラーが返ること
      • ACCEPTED以降の飛行計画で飛行許可・承認情報(flightPermitApplicationInfo)を項目間の整合性に違反する内容へ変更して実行時
        • 422エラーが返ること
      • ACCEPTED以降の飛行計画でflyRouteを自己交差する多角形へ変更して実行時
        • 422エラーが返ること
    • 飛行計画通報(TP-6: reportFlightPlan)実行時
      • パスパラメータflightPlanIdの形式が不正な場合
        • 400エラーが返ること
      • 対象の飛行計画IDが存在しない場合
        • 404エラーが返ること
      • ACCEPTED以外の状態の飛行計画を対象に実行時
        • 409エラーが返ること
      • 通報済み(reportStatus=REPORTED)の飛行計画を対象に実行時
        • 409エラーが返ること
    • 飛行計画削除(TP-7: deleteFlightPlan)実行時
      • パスパラメータflightPlanIdの形式が不正な場合
        • 400エラーが返ること
      • 対象の飛行計画IDが存在しない場合、または論理削除済みの飛行計画をIdempotency-Keyなしで再度削除した場合
        • 404エラーが返ること
      • 削除できない状態(ACTIVATEDENDED)の飛行計画を対象に実行時
        • 409エラーが返ること
      • 他の処理によるロック中でアクセス不可の場合
        • 409エラーが返ること
      • Idempotency-Keyヘッダー指定で同一キーを異なる飛行計画IDに対して再利用した場合
        • 422エラーが返ること

上記以外の異常系・準正常系のパターンはテスト対象外とする。forDemoでは以下もあわせてテスト対象外とする(ArchitecturePolicy_Flightplanning_forDemo.md12節参照)。

  • 一時保存(DRAFTの部分入力保存フロー自体): UC-PLAN-01/UC-PLAN-03「代替フロー:一時保存の場合」は10月デモ実装対象外のため、createFlightPlanは常にDIPS必須項目をすべて指定した状態でのみテストする
  • DIPSの許可承認情報取得(UC-PLAN-01-6)・GCSミッションのインポート(UC-PLAN-01-12): 対象API外のため対象外
  • 飛行開始/終了・キャンセル: 対象API外のため対象外
  • 通報の失敗(502/503/504)・模擬DIPSが通報を受理しなかった場合: モックが4xx/5xxを返せないためEnd-to-Endテストでは対象外とし、Unitテストで検証する
  • 通報時の重複ありの応答・模擬DIPSへ送ったリクエストの内容: いずれもPrismでは検証できないためEnd-to-Endテストでは対象外とし、Unitテストで検証する(PrismはPreferヘッダーの指定がなければ最初のexampleしか返さず、DipsApiClientImplは同ヘッダーを送らない。またPrismは受信したリクエストをテストへ返さない)
  • 通報の取り下げ: 模擬DIPSに削除APIが無く10月デモでは実装しないため対象外。飛行計画削除(UC-PLAN-04)のうちDIPS通報済み(reportStatus=REPORTED)を対象とした取り下げ(WITHDRAWINGWITHDRAWNへの遷移、503・504時の扱い)も同じ理由で対象外(BusinessLogicSpecifications.md4.6節)
  • FlightPlanRepository(Repository層のPort)をMockitoでモック化し、Handler層・UseCase層をそれぞれ単体で検証する。
  • FlightPlanningHandlerを対象に、UseCaseをモック化した状態でMockMvc(GlobalExceptionHandlerを実装と同一のものとして組み込む)により検証する。
  • 観点は、リクエスト/レスポンスDTOとドメインオブジェクト間の変換、UseCaseへの引数の受け渡し、およびHTTPステータスの反映の3点とする。
  • CreateFlightPlanUseCaseRegisterFlightPlanUseCaseListFlightPlansUseCaseGetFlightPlanUseCaseUpdateFlightPlanUseCaseReportFlightPlanUseCaseDeleteFlightPlanUseCaseを対象に、FlightPlanRepositoryをMockitoでモック化した状態で検証する。
  • 観点は状態遷移可否判定・必須項目検証等の業務ロジックの分岐網羅とする。前述のとおりHTTPステータスの決定はHandler層の責務であるため、UseCase層のテストではスローされる例外の型のみを検証し、ステータスコードは検証しない。
  • 通報のUseCaseはDipsApiClient(Port)もモック化する。End-to-Endテストで再現できない異常系はここで網羅する(模擬DIPSのモックが4xx/5xxを返せないため。1節参照)。観点は次のとおり。
    • 失敗3種(ExternalDipsInvalidResponseExceptionExternalDipsUnavailableExceptionExternalDipsTimeoutException)それぞれで、通報状態が仕様どおりに確定すること(前2者はUNREPORTEDへ戻し、タイムアウトはREPORTINGを維持してREPORT_TIMEOUTを記録する)
    • 模擬DIPSが通報を受理しなかった場合(flightPlanRegistrationResult1以外)に、通報失敗として扱われること
    • 事前条件(statusreportStatus)を満たさない場合に、対応する例外が送出されること。あわせてreportRequired=falseでも通報できること(同フラグは通報の可否を決めない)
    • 模擬DIPSの呼び出しが、REPORTINGへの遷移をコミットした後・確定の前に行われること(ArchitecturePolicy_Flightplanning_forDemo.md20.3節。InOrderで呼び出し順序を検証する)
    • 確定の直前に対象を取り直した結果、通報状態がREPORTINGでなくなっていた場合に確定させないこと(外部呼び出し中に他の操作が割り込んだケース)
    • 送信する通報リクエストが、現行リビジョンとマスタから組み立てた値になっていること(コード値・フラグ・日時の変換を含む。ArgumentCaptorで捕捉して検証する)
    • 初回通報では飛行計画IDを送らず、再通報では受付番号を送ること

通報では新しい状態遷移イベントの追記とDIPS_REPORTのINSERTを行うため、SQLを追加する。docs/implementation-guide.md「テスト」に従い次の2種を用意する。

  • BoundSqlテスト(DB接続なし。FlightPlanMapperBoundSqlTestと同型): FOR UPDATEの有無・<if>による句の付与/省略・UPDATE対象列の範囲など、機能テストが通っていても検出できない性質を検証する。新しいマッパーXMLを追加した場合はsrc/test/resources/mybatis-config-test.xml<mappers>にも登録する。
  • DB往復テストFlightPlanRepositoryImplIntegrationTestと同型): 実スキーマとの整合(列名・型・resultMapの引数順・ENUMキャスト)を検証する。状態遷移イベントの追記がFLIGHT_PLAN.statuscurrent_revision_idを変えず、updated_atだけを追随させることもここで確認する。
  • テストメソッド名はCodingConventions_Flightplanning_forDemo.md7節の命名規約(<対象メソッド>_when<条件>_<期待結果>)に従う。
  • C1(分岐)カバレッジ90%を目標値とする。
  • 網羅率はJaCoCoレポート(build/reports/jacoco/test/html/index.htmljacocoTestReportタスクで生成)から確認する。
  • 未達の分岐のうち、ユースケース・OASの設計から抽出できない分岐(防御的コード等)については、評価の必要性を検討したうえで前述のテストパターンへの追加を検討する。目標値達成のためだけの機械的なテスト追加は行わない。
  • ./gradlew testで実行する。
  • JUnit Platform(JUnit 6)上で実行される(build.gradle.ktsuseJUnitPlatform())。同ファイルのtasks.testfinalizedBy(tasks.jacocoTestReport)が設定されているため、testタスク完了後にJaCoCoレポートが自動生成される(後述のカバレッジについて参照)。カバレッジ取得のための別コマンド実行は不要。
  • CI(.github/workflows/ci.ymlbuild-and-testジョブ)では、OASドリフト検査・Lint等と合わせて./gradlew build --no-daemonの一部としてtestタスクが実行される。
  • テストコード(src/test/java/com/intent_exchange/utm/配下)。
  • テスト実行結果レポート(build/reports/tests/test/index.html、JUnit XML: build/test-results/test/)。CIではtest-reportsという名前で成功・失敗を問わず常にArtifactとしてアップロードされる(ci.ymlUpload test reportsステップ)。
  • カバレッジレポート(JaCoCo。build/reports/jacoco/test/html/index.html、XML: build/reports/jacoco/test/jacocoTestReport.xml)。