sekaie engineers' blog

セカイエ株式会社が主催するエンジニア勉強会について

集計テスト時の BigQuery のテストデータについて

はじめに

おはようございます。こんにちは。こんばんわ。佐々木です。

さっそくですが、皆さんテストしてますか?

自分もそんな得意ではないですが(むしろ苦手ですが)、サービスの規模も大きくなってきたので最近必死にテストを書いています。

サービス自体は Rails で書かれているので rspec を使ってコントローラやモデルなどのテストは例に倣ってそれっぽくテストが出来ています。

が、集計のテストがなかなか難しいです。

多分一番大変なのはテスト用のデータを用意したりするところだと思います。

特にログデータや MySQL snapshot のデータは BigQuery に入れていてそのデータを集計していて、BigQueryのデータを用意して rspec であれこれするのはなかなか大変です。

この辺をどういう感じでやるか迷っていましたが、なんとなくうまくいきそうなので今回はこの辺を紹介したいと思います。

集計のテストにおける今回の目標と今回のテスト対象

集計に関するテストが0だったので、今回は指定のデータから意図したデータを集計できているか。というところをゴールにしたいと思います。

多分他にも考えなくてはいけないところはあると思いますけど最初の一歩のゴールとしてはまあ良しとしましょう。

今回のテスト対象ですが、MySQL の snapshot データを集計しやすくする中間テーブルデータを生成するところを対象とします。

ので、複数のテーブルデータと中間テーブルのデータの整合性が保たれているかをチェック出来るようなテストを書いてみたいと思います。

テストデータのローダー

では、準備としてテストデータをローディングする所です。

rspec では FactoryGirl があるのでデータを簡単に用意することが出来ますが、 BigQuery には無いので自前で用意しました。

最初は BigQuery の load を使おうと思ったんですが、遅いです。結構遅いです。

なので schema, rows をもとに SQL を生成してその結果を指定のテーブルに挿入しています。

module FactoryBigquery

  def self.load(job)
    @_bigquery = GCP::BigQuery.new
    self.create_table(job[:dataset], job[:table], job[:schema], job[:rows])
  end

  def self.parallel_load(jobs)
    @_bigquery = GCP::BigQuery.new
    Parallel.map(jobs) do |job|
      self.create_table(job[:dataset], job[:table], job[:schema], job[:rows])
    end
  end

  def self.drop_table(dataset, table)
    @_bigquery = GCP::BigQuery.new
    res = @_bigquery.drop_table(dataset, table)
  end

  def self.create_table(dataset, table, schema, rows)
    @_bigquery.create_dataset(dataset)
    from_expr = rows.map{|row|
      select_expr = row.map{|k, v|
        s = schema.select{|s| s[:name] == k}.first
        if s[:type] == 'INTEGER'
          "#{s[:type]}(#{v}) AS #{s[:name]}"
        else
          "#{s[:type]}('#{v}') AS #{s[:name]}"
        end
      }.join(', ')
      "( SELECT #{select_expr} )"
    }.join(', ')

    sql = "SELECT #{schema.map{|s| s[:name] }.join(', ')} FROM #{from_expr}"
    res = @_bigquery.copy_table_by_query(sql, dataset, table, { 'query.writeDisposition': 'WRITE_TRUNCATE' })
    @_bigquery.wait_job(res[:job_id])
  end
end

テスト

上記のデータローダを使って実際にテストを行います

require 'rails_helper'
require 'importer/molding/user'

describe ::Importer::Molding::User do
  before :all do
    FactoryBigquery.parallel_load([
      {
        dataset: :snapshot,
        table: 'prefs_20160101',
        schema: [
          { name: 'id',               type: 'INTEGER' },
          { name: 'name',             type: 'STRING' },
        ],
        rows: [
          { 'id' => 1, 'name' => 'TEST_PREF' },
        ]
      }, {
        dataset: :snapshot,
        table: 'cities_20160101',
        schema: [
          { name: 'id',               type: 'INTEGER' },
          { name: 'name',             type: 'STRING' },
        ],
        rows: [
          { 'id' => 1, 'name' => 'TEST_CITY' },
        ]
      }, {
        dataset: :snapshot,
        table: 'users_20160101',
        schema: [
          { name: 'id',           type: 'INTEGER' },
          { name: 'family_name',  type: 'STRING' },
          { name: 'last_name',    type: 'STRING' },
          { name: 'pref_id',      type: 'INTEGER' },
          { name: 'city_id',      type: 'INTEGER' },
        ],
        rows: [
          {
            'id' => 1,
            'family_name' => 'TEST',
            'last_name' => 'NAME',
            'pref_id' => 1,
            'city_id' => 1,
          }
        ]
    ])
  end
    
  describe '#import' do
    before(:all) do
      FactoryBigquery.drop_table('moldings', 'users_20160101')
      importer = ::Importer::Molding::User.new
      importer.date = '20160101'
      importer.import
      bigquery = ::GCP::BigQuery.new
      res = bigquery.query("SELECT * FROM moldings.users_20160101 WHERE id = 1")
      @rows = bigquery.result_to_hash(res)
    end

    it do
      row = @rows.first
      expect(row['id'].to_i).to eq 1
      expect(row['name']).to eq 'TEST NAME'
      expect(row['pref_name']).to eq 'TEST_PREF'
      expect(row['city_name']).to eq 'TEST_CITY'
    end
  end

それでは実行してみましょう。

$ bundle exec rspec spec/importers/importer/molding/user_spec.rb
.
Finished in 42.02 seconds (files took 2.68 seconds to load)
1 examples, 0 failures

無事テストが通りました。

おわりに

今回はテスト化をする上で集計時のテストのデータを用意する方法と集計時のテストを行う方法について紹介しました。

まだ試行錯誤をしている段階です。

もっと良いテストの方法があればぜひ紹介していただければと思います。

ほな!