kickflow Tech Blog

株式会社kickflowのプロダクト開発本部によるブログ

Autifyで効率的なE2Eテストシナリオを作ってみた 〜チケット削除機能の例〜

こんにちは!kickflowのプロダクト開発本部QAチームです。

私たちは、kickflowの品質向上のため、日々様々なテストに取り組んでいます。
その中でもE2E(End-to-End)テストは、ユーザーが実際に操作する流れを通して機能全体の動作を保証する上で非常に重要です。

しかし、E2Eテストは網羅性が高い反面、手動で行うと多くの時間と手間がかかるという課題もあります。
そこでkickflowでは、E2Eテスト自動化プラットフォームであるAutifyを活用し、テストの効率化を進めています。

今回は、Autifyを使って「チケット削除機能」のE2Eテストシナリオを作成した際の工夫やポイント、そしてその効果についてご紹介します。

今回作成したテストシナリオの概要

今回作成したシナリオは、kickflowの「チケット削除」機能が正しく動作することを検証するものです。

主な流れは以下の通りです。

Autifyを活用した効率的なE2Eテストシナリオ作成のポイント

AutifyでE2Eテストシナリオを作成するにあたり、より効率的で信頼性の高いテストを実現するために、いくつかの工夫を凝らしました。

1. 必要最低限の確認内容に絞った画面レコーディング

E2Eテストでは、つい多くのことを一度に確認したくなりますが、シナリオが複雑になりメンテナンス性が低下する可能性があります。

今回のチケット削除機能のテストでは、

  • 「削除されたチケットにアクセスすると404エラーになる」

  • 「削除されたチケットが検索結果に表示されない」

  • 「削除処理に伴い、関連するデータが適切に更新される(例:紐づくデータのタイトル変更など)」

といった、削除機能において本当にクリティカルな確認項目に絞ってシナリオを設計しました。
これにより、テストの意図が明確になり、変更にも強いシナリオを目指しました。

2. JSステップを活用したデータ準備・クリーンアップの自動化

Autifyの強力な機能の一つに、JavaScriptを実行できる「JSステップ」があります。
これを活用することで、テストデータの準備や後片付けをAPI経由で自動化しました。

特に、テスト対象となるチケットの作成や、テスト実施後に関連データを元の状態に戻すといった処理は、画面操作で行うと非常に多くのステップが必要になります。

テストデータ準備

例えば、チケットを作成し承認フローを経てテスト可能な状態にするまで、画面操作では10ステップ以上かかることも珍しくありません。
これを以下のようにJSステップでkickflowの内部APIを直接呼び出すように実装することで、わずか1ステップでテストデータの準備が完了します。

実際のJSステップ
const basicAuth = 'Basic ************'

// チケット作成APIを呼び出す関数
const createTicket = () => {
  const headers = new Headers()
  headers.append('X-Authorization', AUTHOR_TOKEN)
  headers.append('Content-Type', 'application/json')
  headers.append('Authorization', basicAuth)

  const body = JSON.stringify({
    status: 'in_progress',
    workflowId: WORKFLOW_ID,
    authorTeamId: TEAM_ID,
    title: 'test',
    inputs: [
      {
        formFieldId: FORM_FIELD_ID,
        value: INPUT_VALUE,
      },
    ],
  })

  const requestOptions = {
    method: 'POST',
    headers: headers,
    body: body,
  }

  return fetch('https://api.kickflow.test/v1/tickets', requestOptions)
    .then(async response => {
      if (response.status !== 200) {
        const message = await response.text()
        throw new Error(`HTTP ${response.status} - ${message}`)
      }
      return response.json()
    })
    .then(result => {
      console.log('Ticket created successfully:', result)
      return result.id
    })
    .catch(error => {
      console.error('Error creating ticket:', error)
      throw error
    })
}

// チケット承認APIを呼び出す関数
const approveTicket = ticketId => {
  const headers = new Headers()
  headers.append('X-Authorization', APPROVER_TOKEN)
  headers.append('Content-Type', 'application/json')
  headers.append('Authorization', basicAuth)

  const requestOptions = {
    method: 'POST',
    headers: headers,
  }

  return fetch(
    `https://api.kickflow.test/v1/tickets/${ticketId}/approve`,
    requestOptions,
  )
    .then(async response => {
      if (response.status !== 200) {
        const message = await response.text()
        throw new Error(`HTTP ${response.status} - ${message}`)
      }
      return response.text()
    })
    .then(result => {
      console.log('Ticket approved successfully:', result)
    })
    .catch(error => {
      console.error('Error approving ticket:', error)
      throw error
    })
}

// 作成したチケットをそのまま承認する
const createAndApproveTicket = async () => {
  try {
    const ticketId = await createTicket()
    await approveTicket(ticketId)
    console.log('Ticket created and approved successfully.')
    return ticketId // チケットIDを返す
  } catch (error) {
    console.error('Error:', error)
    throw error // エラーを再スロー
  }
}

return createAndApproveTicket()

テストデータクリーンアップ

同様に、テスト完了後のデータクリーンアップもJSステップでAPIを呼び出すことで効率化しています。

実際のJSステップ
const basicAuth = 'Basic ************'

// ワークフロー名からワークフローIDを取得する
const getWorkflowId = workflowName => {
  const wfUrl = 'https://api.kickflow.test/v1/workflows?perPage=100'
  const headers = new Headers()
  headers.append('X-Authorization', ADMINISTRATOR_TOKEN)
  headers.append('Authorization', basicAuth)

  const requestOptions = {
    method: 'GET',
    headers: headers,
  }

  return fetch(wfUrl, requestOptions)
    .then(async response => {
      if (response.status !== 200) {
        const message = await response.text()
        throw new Error(
          `Error fetching workflows: HTTP ${response.status} - ${message}`,
        )
      }
      return response.json()
    })
    .then(workflows => {
      let targetWorkflowId = null
      for (let i = 0; i < workflows.length; i++) {
        if (workflows[i].name === workflowName) {
          targetWorkflowId = workflows[i].id
          break
        }
      }

      if (!targetWorkflowId) {
        throw new Error("Workflow with name '" + workflowName + "' not found.")
      }
      console.log('Found workflow id: ' + targetWorkflowId)
      return targetWorkflowId
    })
    .catch(error => {
      console.error('Error fetching workflow id:', error)
      throw error
    })
}

// チケット一覧取得 → 対象ワークフローのチケットのうち、status が "archived" でないものの id を全件取得
const getTicketIds = targetWorkflowId => {
  const ticketUrl = `https://api.kickflow.test/v1/tickets?perPage=100&workflowId=${targetWorkflowId}`
  const headers = new Headers()
  headers.append('X-Authorization', ADMINISTRATOR_TOKEN)
  headers.append('Content-Type', 'application/json')
  headers.append('Authorization', basicAuth)

  const requestOptions = {
    method: 'GET',
    headers: headers,
  }

  return fetch(ticketUrl, requestOptions)
    .then(async response => {
      if (response.status !== 200) {
        const message = await response.text()
        throw new Error(
          `Error fetching tickets: HTTP ${response.status} - ${message}`,
        )
      }
      return response.json()
    })
    .then(tickets => {
      if (!Array.isArray(tickets)) {
        throw new Error('Invalid tickets response format.')
      }

      let ticketIds = []
      for (let j = 0; j < tickets.length; j++) {
        // ステータスが "archived" のチケットはスキップする
        if (tickets[j].status === 'archived') {
          console.log('Skipping archived ticket id: ' + tickets[j].id)
          continue
        }
        if (tickets[j].id) {
          ticketIds.push(tickets[j].id)
        }
      }

      console.log(`Found ${ticketIds.length} ticket(s) to archive: `, ticketIds)
      return ticketIds
    })
    .catch(error => {
      console.error('Error fetching ticket ids:', error)
      throw error
    })
}

// チケットIDを指定してチケットをアーカイブする
const archiveTicket = ticketId => {
  const archiveUrl = `https://api.kickflow.test/v1/tickets/${ticketId}/archive`
  const headers = new Headers()
  headers.append('X-Authorization', ADMINISTRATOR_TOKEN)
  headers.append('Content-Type', 'application/json')
  headers.append('Authorization', basicAuth)

  const requestOptions = {
    method: 'POST',
    headers: headers,
  }

  return fetch(archiveUrl, requestOptions)
    .then(async response => {
      if (response.status !== 200) {
        const message = await response.text()
        throw new Error(
          `Error archiving ticket: HTTP ${response.status} - ${message}`,
        )
      }
      return response.json()
    })
    .then(archiveResponse => {
      if (archiveResponse.status !== 'archived') {
        throw new Error(
          `Assertion failed: Ticket id ${ticketId} did not archive properly. Response status: ${archiveResponse.status}`,
        )
      }
      console.log('Ticket id ' + ticketId + ' archived successfully.')
    })
    .catch(error => {
      console.error(`Error archiving ticket id ${ticketId}:`, error)
      throw error
    })
}

// ワークフロー名を指定してチケットを全件アーカイブする
const archiveTickets = async () => {
  try {
    const targetWorkflowId = await getWorkflowId(WORKFLOW_NAME);
    const ticketIds = await getTicketIds(targetWorkflowId);

    // 各チケットをアーカイブする
    for (let k = 0; k < ticketIds.length; k++) {
      const ticketId = ticketIds[k];
      await archiveTicket(ticketId);
    }

    console.log('All tickets archived successfully.');
  } catch (error) {
    console.error('Error archiving tickets:', error);
  }
}

archiveTickets()

これにより、毎回クリーンな状態でテストを実行でき、他のテストへの影響やデータの残留を防ぐことができます。

3. データの作成からクリーンアップまでを1シナリオで完結

前述のJSステップの活用により、テストデータの作成、機能の確認、そしてテストデータのクリーンアップまでを一つのシナリオ内で完結させています。
これにより、テストの独立性が高まり、E2Eテストでありながら網羅的かつ安定した検証が可能になります。
各テストシナリオが自己完結しているため、並列実行や部分的な再実行も容易です。

4. ログイン処理のステップグループ化

ログイン処理は多くのテストシナリオで共通して必要となるステップです。
メールアドレス入力、パスワード入力、ログインボタンのクリックといった一連の操作をAutifyのステップグループとしてまとめることで、シナリオ作成の効率が向上し、メンテナンス性も高まります。

5. 動的なデータ取得と活用

テストの途中で生成されるID(例:JSステップで作成したチケットのID)や、特定の要素のテキスト内容などを動的に取得し、後続のステップで検証や操作対象として利用しています。
今回のユースケースでは、STEP4で新規に作成したチケットIDをJSステップの return で取得し、STEP5でこのIDを引数として使用してチケット詳細画面のURLを動的に生成しています。

実際のコード例は以下です。

return `https://${subDomain}.kickflow.test/dashboard/tickets/${ticketId}`

このようにして生成したURLをSTEP8で利用し、対象チケットの詳細画面に遷移して検証を行っています。
これにより、テストごとに異なるデータでも柔軟にシナリオを構築できています。

6. エラーハンドリングによる網羅性の向上

正常系だけでなく、準正常系のテストも重視しています。

今回のシナリオでは、削除済みチケットのURLに直接アクセスした際に404エラーページが表示されることを検証しています。
この404ページの確認については、Autifyのビジュアルリグレッションテストを活用し、見た目が想定通りであることもチェックしています。

また、キャプチャタイミングのズレによる誤検出を防ぐため、差分の許容値を1%に設定しています。
さらに、このビジュアルリグレッションのステップは「エラーとしてテストを続行する」設定にしており、仮に差分が出ても後続ステップまで実行が可能です。
これにより、網羅的かつ効率的なエラー検証を実現しています。

具体的な効果測定:JSステップ導入の成果

上記のポイント、特にJSステップを活用したシナリオ構築の結果、テストの効率に大きな改善が見られました。

項目 JSステップを利用しなかった場合 JSステップを利用した場合(今回の取り組み) 改善効果
実行時間 13分45秒 3分09秒 約77%短縮 (10分36秒削減)
総ステップ数 61 STEP 22 STEP 約64%削減 (39ステップ削減)

画像左:ステップを利用しなかった場合 / 画像右:JSステップを利用した場合

ご覧の通り、JSステップを導入することで、実行時間は約1/4に、総ステップ数は約1/3に削減されました。

この効果は非常に大きく、以下のメリットに繋がっています。

  • テストの信頼性向上:
    JSステップによるAPI連携で、UIの変更に左右されにくい安定したデータ準備・クリーンアップ処理が実現できました。
  • メンテナンスコストの削減:
    確認項目を絞り、複雑な画面操作をAPIに置き換えることで、シナリオの可読性が向上し、将来的なメンテナンスも容易になりました。UIの細かな変更によるテスト失敗のリスクも低減します。
  • 実行時間の大幅な短縮:
    データ準備や後処理をAPIで行うことで、シナリオ全体の実行時間は約3分程度と非常に高速になりました。これにより、開発サイクルの中で頻繁にE2Eテストを実行し、早期にフィードバックを得ることが可能になります。

【おまけ】非エンジニアによるJSステップ作成

今回ご紹介したJSステップはQAチームがAIを活用して作成しています。
AIを使ったJSステップ作成の具体的なノウハウについては、また次回の機会に詳しく記事にできればと考えています!

まとめ

Autifyの画面レコーディング機能と、JSステップによるAPI活用の組み合わせは、効率的で信頼性の高いE2Eテストシナリオを作成する上で非常に有効であると実感しました。
特に、複雑なデータの事前準備や事後処理が必要なテストケースにおいて、JSステップは強力な武器となります。
これにより、「本当に確認したいこと」にテストの焦点を当てることができ、品質保証活動全体の効率化に繋がります。

さらに、AIを活用することで、プログラミングの専門知識がないメンバーでもJSステップの作成に貢献できる可能性が広がりました。

今後も、より複雑なシナリオや様々な機能のテストにおいて、AutifyとJSステップを積極的に活用し、kickflowの品質をさらに高めていきたいと考えています。


kickflowでは、ユーザーに安心してご利用いただけるプロダクトを目指し、品質向上に情熱を持って取り組むQAエンジニアを募集しています!

私たちと一緒に、テスト自動化や品質保証の仕組みづくりを通じて、プロダクトの信頼性を高めていきませんか?

品質へのこだわりとユーザー視点を持ち、より良い体験を一緒に実現してくれるQA仲間をお待ちしています!
ご興味のある方は、ぜひ採用サイトをご覧ください。

herp.careers