💭

Dry Runを活用してBigQueryのクエリのユニットテスト環境を作る

2023/08/23に公開

TL;DR

本記事では、BigQueryを利用したアプリケーションの開発段階で、Dry Run(シミュレーションモード)を駆使してクエリの動作をテストする手法を紹介します。

使用言語: Node.js(TypeScript)
フレームワーク: Nest.js

開発時にBigQueryのクエリの動作をテストしたい動機

BigQueryなどのクラウド上のDWH (データウェアハウス) に格納されたデータを使ったアプリケーションの開発において、開発用リソースを構築して開発を進めることは可能ですが、それらのロジックの意味のあるテストを書くことは課題の一つとして挙げられると思います。
さらにBigQueryには公式が出しているエミュレーターがないため、開発やテストのための環境を準備することがやはり増えると思いますが、もっと気楽にクエリが適切かどうかを確認したいと思いました。

Dry Run

Dry Runは、BigQueryの機能の1つであり、クエリを実際に実行せずにその実行計画やスキャンするデータの量を確認するためのモードです。クエリを実行せずに実行計画を確認できるため、データベースへの影響を与えずにクエリの動作を評価することができます。

Dry Runの主な利用目的

  • クエリの正確性の確認: クエリが正しくフォーマットされ、データへのアクセス方法が適切かどうかを確認できます。クエリに文法エラーやテーブルへのアクセスエラーがある場合、Dry Runはエラーメッセージを返します。
  • クエリの実行計画の評価: クエリが実行される際の計画やステップの詳細を確認できます。これにより、クエリの実行がどのように進行し、どのテーブルやデータが参照されるかを理解できます。
  • 処理対象データの確認: Dry Runはクエリがどの程度のデータをスキャンするかを示す統計情報を提供します。これにより、クエリが大規模なデータセットをスキャンする場合のリソース消費や処理時間の見積もりが可能です。

Dry Runの使用方法

Dry Runを使用するには、クエリを実行する際に dryRun パラメータを設定します。これを true に設定することで、クエリは実行されずにDry Runモードで動作します。実際のクエリ実行時と同様に、クエリオブジェクトやパラメータ、タイムアウトなどを設定することができます。

実際にDry Run実行するコードは以下のページから確認できます。
https://cloud.google.com/bigquery/docs/samples/bigquery-query-dry-run?hl=ja#bigquery_query_dry_run-nodejs

実際のコード例とテスト

ここで、Node.jsとNest.jsを使用したBigQueryサービスの実装例を紹介します。また、それをユニットテストする際にDry Runを活用したテストケースも示します。これにより、開発者は変更や新機能の追加時にもクエリの動作を確認し、バグの発生を防ぐことができます。

実際のクエリを渡して実行するservice

実行したいクエリのオブジェクトを渡して実行するためのserviceを書きます。
parameterでDry Run実行か本実行かを出し分けられるように引数にdryRun: booleanを渡せるようにしておきます。
Googleのドキュメントにクエリを実行する関数の例があるのでそれを参考に以下のように定義します。

big-query.service.ts
import { BigQuery, Job, Query } from "@google-cloud/bigquery";
import { Injectable } from "@nestjs/common";

const bigQuery = new BigQuery();

@Injectable()
export class BigQueryService {
  async runQuery({
    query,
    dryRun = false,
  }: {
    query: Query;
    dryRun?: boolean;
  }) {
    const job = await this.createJob({ query, dryRun });
    const [rows] = await job.getQueryResults();

    return { data: rows };
  }

  async createJob({ query, dryRun }: { query: Query; dryRun: boolean }): Promise<Job> {
    const [job] = await bigQuery.createQueryJob({
      location: "asia-northeast1",
      ...query,
      dryRun,
    });
    return job;
  }
}

実際のクエリをserviceに渡してDry Runするtest

上記で書いたserviceに検証したいqueryオブジェクトを渡してDry Runするtestを書きます。
今回は実際のBigQuery classを使用するのでmockも作成はせずに直接呼び出します。

big-query.service.spec.ts
import { Test } from "@nestjs/testing";
import { AppModule } from "../../app.module";
import { BigQueryService } from "./big-query.service";

const BIGQUERY_JOB_VALID_STATUS = { state: "DONE" };

describe("BigQueryService", () => {
  const prepareService = async () => {
    const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
    const service = await module.resolve(BigQueryService);

    return { service };
  };

  describe("postsQuery", () => {
    it("query should correct", async () => {
      const { service } = await prepareService();
      const query = {
        query: `SELECT userId, title, content, publishedAt
          FROM posts
          WHERE userId = @_userId`,
        params: { _userId: 1 },
      }

      const postsQueryJob = await service.createJob({ query, dryRun: true });
      expect(postsQueryJob.metadata.status).toMatchObject(
        BIGQUERY_JOB_VALID_STATUS,
      );
    });
  });
});

クエリが正しければ生成されるjobのmetadata.status.stateが DONE になるのでそれでtestをします。
正しくないクエリを実行するとDry Runによって以下のようにクエリのエラーが返されます。

  BigQueryService
    postsQuery
      ✕ query should correct (1491 ms)

  ● BigQueryService › postsQuery › query should correct

    Unrecognized name: userIdd; Did you mean userId? at [3:11]

このテストを事前に実行することによって、クエリによってBigQueryから得られるデータを使ったアプリケーションの画面の挙動を確認せずとも事前にクエリが正しいかをチェックすることができます。

Discussion