飛行計画機能 forDemo テスト方針
10月デモ向けのテスト方針を示す。
以下2つの観点からテストを行う。
- End-to-Endテスト : Testcontainersを利用し、APP・DB(PostgreSQL/PostGIS)をコンテナ起動した実環境に対してAPIを実行する疎通テストとして実施する。
- Unitテスト : Repository層をモック化し、関数単体(UseCase等)の評価を行うテストとして実施する。
1. End-to-Endテスト
Section titled “1. End-to-Endテスト”- ユースケース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)を行う。
テストパターン
Section titled “テストパターン”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_REVISION、revision_no=1・change_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で返ること
- DIPS必須項目(
- 飛行計画本登録(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)を指定して実行時- 飛行時間帯(
startTime〜startTime+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の経路)
- 頂点が終点(始点との重複)を含まない配列で返ること(OASの
- 詳細取得の
flightPlanをそのまま「飛行計画更新」のリクエストボディへ送って実行時- 200が返り、内容が変わらないこと(レスポンス専用の項目
pilotInfo[].pilotName/aircraftNamesが除かれ、他の項目は往復しても値が変わらないこと)
- 200が返り、内容が変わらないこと(レスポンス専用の項目
- jsonb(
dips_report_detail・draft_fields)の値がAPIの型・コードの値域・OASのrequiredに適合しない飛行計画IDを指定して実行時- 当該項目のみが未設定(キー省略)で返り、他の項目と応答全体は成功すること(1項目の不適合で飛行計画が取得不能にならないこと)
- どの飛行計画のどの項目が落ちたかがログから特定できること
- 本登録済み(
- 飛行計画更新(TP-5:
updateFlightPlan)実行時(UC-PLAN-03手順7〜9)DRAFT状態の飛行計画を対象に一部の項目のみを指定して実行時- 指定した項目のみが
FLIGHT_PLAN_DRAFT.draft_fieldsに反映され、キーを省略した項目は変更されないこと statusはDRAFTのまま維持され、リビジョン(FLIGHT_PLAN_REVISION)が作成されないことflightPeriodを変更した場合にplanned_start_at/planned_end_atが再計算されること
- 指定した項目のみが
- 明示的に
nullを指定して実行時- 当該項目が未入力の状態(キーごと削除)に戻ること
ACCEPTED状態の飛行計画を対象に実行時revision_noが+1されchange_type=UPDATE・parent_revision_id=更新前の現在リビジョンの新リビジョンが作成されることFLIGHT_PLAN.current_revision_idが新リビジョンに差し替わり、statusはACCEPTEDのまま維持されること- 指定しなかった項目が更新前のリビジョンから引き継がれること
- 飛行経路(
flyRoute)の種別を変更して実行時- 新しい種別の
FLIGHT_PLAN_AREA(およびそのサブタイプ行)で置き換わり、旧種別のサブタイプ行が残らないこと
- 新しい種別の
- DIPS通報済み(
reportStatus=REPORTED)の飛行計画を対象に、内容を変更して実行時reportStatusがUNREPORTEDに戻ること(statusは変わらないこと)
ACCEPTED状態の飛行計画を対象に、内容が変わらないリクエスト(空のボディ{}・現在値と同じ値の指定)で実行時- 200が返ること
- リビジョン(
FLIGHT_PLAN_REVISION)・状態遷移イベント(FLIGHT_PLAN_STATE_EVENT)が増えず、FLIGHT_PLAN.current_revision_id・updated_atも変わらないこと - DIPS通報済み(
reportStatus=REPORTED)の場合はREPORTEDのまま維持されること - 飛行経路が
polygonの場合も、復元(toPatchableFields)による差でリビジョンが作成されないこと
- 飛行計画通報(TP-6:
reportFlightPlan)実行時(UC-DIPS-03手順2〜6)- 未通報(
reportStatus=UNREPORTED)のACCEPTEDの飛行計画を対象に実行時reportStatusがREPORTEDになり、statusはACCEPTEDのまま変わらないこと- 模擬DIPSが採番した受付番号が
dipsFlightPlanIdとして返り、DIPS_REPORTに1行追加されること FLIGHT_PLAN_STATE_EVENTにREPORT_START・REPORT_COMPLETEが順に追記されること
- 通報済みの飛行計画を更新して
UNREPORTEDに戻したあと、再度実行時- 模擬DIPSへ受付番号を指定して送信されること(新規登録ではなく更新として扱われること)
dipsFlightPlanIdが初回通報時の値から変わらないこと
- 未通報(
- 飛行計画削除(TP-7:
deleteFlightPlan)実行時(UC-PLAN-04)DRAFT状態の飛行計画を対象に実行時FLIGHT_PLAN.deleted_atが設定され、行自体は物理削除されないこと- 一時保存内容(
FLIGHT_PLAN_DRAFT)の行が物理削除されること statusはDRAFTのまま維持され、状態遷移イベント(FLIGHT_PLAN_STATE_EVENT)が追記されないこと
ACCEPTED状態の飛行計画を対象に実行時FLIGHT_PLAN.deleted_atが設定され、リビジョン(FLIGHT_PLAN_REVISION以下)が残ること
- 削除後に飛行計画詳細取得・飛行計画一覧取得を実行時
- 詳細取得は404となり、一覧取得の結果に当該飛行計画が含まれないこと
Idempotency-Keyヘッダー指定で同一キーの再送時- 重複して削除処理が行われず、最初の実行結果が204で返ること
- 飛行計画仮登録(TP-1:
- 異常系
- 飛行計画仮登録(TP-1:
createFlightPlan)実行時- リクエストボディのパース失敗(不正なJSON・型不一致等)時
- 400エラーが返ること
- DIPS必須項目のいずれかが未入力等のバリデーションエラー時
- 422エラーが返ること
Idempotency-Keyヘッダー指定で同一キーを異なるリクエストボディで再利用した場合- 422エラーが返ること
- リクエストボディのパース失敗(不正なJSON・型不一致等)時
- 飛行計画本登録(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[].codeがOTHER_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エラーが返ること
- 更新できない状態(
CANCELLED・ENDED)の飛行計画を対象に実行時- 409エラーが返ること
- 他の処理によるロック中でアクセス不可の場合
- 409エラーが返ること
ACCEPTED以降の飛行計画で必須入力項目を明示的にnullでクリアして実行時- 422エラーが返ること
ACCEPTED以降の飛行計画で飛行開始予定日時(flightPeriod.startTime)が現在日時より過去になるよう実行時- 422エラーが返ること
nameに明示的なnullを指定して実行時- 422エラーが返ること
ACCEPTED以降の飛行計画で飛行許可・承認情報(flightPermitApplicationInfo)を項目間の整合性に違反する内容へ変更して実行時- 422エラーが返ること
ACCEPTED以降の飛行計画でflyRouteを自己交差する多角形へ変更して実行時- 422エラーが返ること
- リクエストボディのパース失敗(不正なJSON・型不一致等)時、またはパスパラメータ
- 飛行計画通報(TP-6:
reportFlightPlan)実行時- パスパラメータ
flightPlanIdの形式が不正な場合- 400エラーが返ること
- 対象の飛行計画IDが存在しない場合
- 404エラーが返ること
ACCEPTED以外の状態の飛行計画を対象に実行時- 409エラーが返ること
- 通報済み(
reportStatus=REPORTED)の飛行計画を対象に実行時- 409エラーが返ること
- パスパラメータ
- 飛行計画削除(TP-7:
deleteFlightPlan)実行時- パスパラメータ
flightPlanIdの形式が不正な場合- 400エラーが返ること
- 対象の飛行計画IDが存在しない場合、または論理削除済みの飛行計画を
Idempotency-Keyなしで再度削除した場合- 404エラーが返ること
- 削除できない状態(
ACTIVATED・ENDED)の飛行計画を対象に実行時- 409エラーが返ること
- 他の処理によるロック中でアクセス不可の場合
- 409エラーが返ること
Idempotency-Keyヘッダー指定で同一キーを異なる飛行計画IDに対して再利用した場合- 422エラーが返ること
- パスパラメータ
- 飛行計画仮登録(TP-1:
上記以外の異常系・準正常系のパターンはテスト対象外とする。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)を対象とした取り下げ(WITHDRAWING/WITHDRAWNへの遷移、503・504時の扱い)も同じ理由で対象外(BusinessLogicSpecifications.md4.6節)
2. Unitテスト
Section titled “2. Unitテスト”FlightPlanRepository(Repository層のPort)をMockitoでモック化し、Handler層・UseCase層をそれぞれ単体で検証する。
Handler層
Section titled “Handler層”FlightPlanningHandlerを対象に、UseCaseをモック化した状態でMockMvc(GlobalExceptionHandlerを実装と同一のものとして組み込む)により検証する。- 観点は、リクエスト/レスポンスDTOとドメインオブジェクト間の変換、UseCaseへの引数の受け渡し、およびHTTPステータスの反映の3点とする。
UseCase層
Section titled “UseCase層”CreateFlightPlanUseCase・RegisterFlightPlanUseCase・ListFlightPlansUseCase・GetFlightPlanUseCase・UpdateFlightPlanUseCase・ReportFlightPlanUseCase・DeleteFlightPlanUseCaseを対象に、FlightPlanRepositoryをMockitoでモック化した状態で検証する。- 観点は状態遷移可否判定・必須項目検証等の業務ロジックの分岐網羅とする。前述のとおりHTTPステータスの決定はHandler層の責務であるため、UseCase層のテストではスローされる例外の型のみを検証し、ステータスコードは検証しない。
- 通報のUseCaseは
DipsApiClient(Port)もモック化する。End-to-Endテストで再現できない異常系はここで網羅する(模擬DIPSのモックが4xx/5xxを返せないため。1節参照)。観点は次のとおり。- 失敗3種(
ExternalDipsInvalidResponseException・ExternalDipsUnavailableException・ExternalDipsTimeoutException)それぞれで、通報状態が仕様どおりに確定すること(前2者はUNREPORTEDへ戻し、タイムアウトはREPORTINGを維持してREPORT_TIMEOUTを記録する) - 模擬DIPSが通報を受理しなかった場合(
flightPlanRegistrationResultが1以外)に、通報失敗として扱われること - 事前条件(
status・reportStatus)を満たさない場合に、対応する例外が送出されること。あわせてreportRequired=falseでも通報できること(同フラグは通報の可否を決めない) - 模擬DIPSの呼び出しが、
REPORTINGへの遷移をコミットした後・確定の前に行われること(ArchitecturePolicy_Flightplanning_forDemo.md20.3節。InOrderで呼び出し順序を検証する) - 確定の直前に対象を取り直した結果、通報状態が
REPORTINGでなくなっていた場合に確定させないこと(外部呼び出し中に他の操作が割り込んだケース) - 送信する通報リクエストが、現行リビジョンとマスタから組み立てた値になっていること(コード値・フラグ・日時の変換を含む。
ArgumentCaptorで捕捉して検証する) - 初回通報では飛行計画IDを送らず、再通報では受付番号を送ること
- 失敗3種(
Repository層(マッパーSQL)
Section titled “Repository層(マッパーSQL)”通報では新しい状態遷移イベントの追記と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.status・current_revision_idを変えず、updated_atだけを追随させることもここで確認する。
- テストメソッド名は
CodingConventions_Flightplanning_forDemo.md7節の命名規約(<対象メソッド>_when<条件>_<期待結果>)に従う。
- C1(分岐)カバレッジ90%を目標値とする。
- 網羅率はJaCoCoレポート(
build/reports/jacoco/test/html/index.html、jacocoTestReportタスクで生成)から確認する。 - 未達の分岐のうち、ユースケース・OASの設計から抽出できない分岐(防御的コード等)については、評価の必要性を検討したうえで前述のテストパターンへの追加を検討する。目標値達成のためだけの機械的なテスト追加は行わない。
3. テスト実行
Section titled “3. テスト実行”./gradlew testで実行する。- JUnit Platform(JUnit 6)上で実行される(
build.gradle.ktsのuseJUnitPlatform())。同ファイルのtasks.testにfinalizedBy(tasks.jacocoTestReport)が設定されているため、testタスク完了後にJaCoCoレポートが自動生成される(後述のカバレッジについて参照)。カバレッジ取得のための別コマンド実行は不要。 - CI(
.github/workflows/ci.ymlのbuild-and-testジョブ)では、OASドリフト検査・Lint等と合わせて./gradlew build --no-daemonの一部としてtestタスクが実行される。
4. 成果物
Section titled “4. 成果物”- テストコード(
src/test/java/com/intent_exchange/utm/配下)。 - テスト実行結果レポート(
build/reports/tests/test/index.html、JUnit XML:build/test-results/test/)。CIではtest-reportsという名前で成功・失敗を問わず常にArtifactとしてアップロードされる(ci.ymlのUpload test reportsステップ)。 - カバレッジレポート(JaCoCo。
build/reports/jacoco/test/html/index.html、XML:build/reports/jacoco/test/jacocoTestReport.xml)。