コピペコードで快適生活

明日使えるソースを自分のために

ES2015のコードをBabel+Jestでテストする

まだ試したことがなかったので、やり方をメモ。

ライブラリのインストール

# bableのインストール
npm install --save-dev @babel/core @babel/cli @babel/preset-env

# jestのインストール
# babel-jestも一緒にインストールされる
npm install --save-dev jest

# importはNode.jsで使えないため
# requireに変換するライブラリをインストール
npm install --save-dev @babel/plugin-transform-modules-commonjs

babelの設定

babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env"
    ]
  ],
  env: {
    test: {
      // テスト環境でimport->require変換を有効にする
      plugins: [
        "transform-es2015-modules-commonjs",
      ],
    },
  },
};

jestの設定

jestの設定ファイル(jest.config.js)作成

npx jest --init

jest.config.jsは初期状態のままでOK。

設定詳細 https://jestjs.io/docs/ja/configuration

scriptの追加

package.json

"scripts": {
  "test": "jest",
  "test:w": "jest --watch",
  "test:coverage": "jest --coverage"
},

テストを書いてみる

app/models/user.js

class User {
  constructor(attrs = {}) {
    this.firstName = attrs.firstName;
    this.lastName = attrs.lastName;
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

export default User;

test/models/user.test.js

import User from '../../app/models/user';

describe('getFullName', () => {
  it('firstName + lastName の値が返される', () => {
    let user = new User({firstName: 'taro', lastName: 'yamada'});
    let fullName = user.getFullName();
    expect(fullName).toEqual('taro yamada');
  });
});

テスト実行

# ファイルを指定して実行
npm run test test/models/user.test.js

# テスト後にカバレッジ率を表示
npm run test:coverage test/models/user.test.js

メモ

しばらくbabelから離れている間に色々変わっていた。

  • babelが7系からパッケージ名が @bable/xxx になっていた。
  • babelの設定ファイルが babel.config.js になっていた。
  • babelrcは個別設定用という位置づけに変わっていた。
  • preset-2015が不要になり、preset-envが設定に応じて適切なライブラリを選択してくれるようになっていた。

参考にさせていただきました
Babel + Jest で JavaScript のテストをする - かもメモ

シェルでヒアドキュメントを使う

そういればやり方知らなかったのでメモ。

# これで標準出力できる。
cat << EOS
hoge
fuga
piyo
EOS

# ヒアドキュメント内で変数展開できる。
# 標準出力になるので変数に代入したいときはバッククォート使う。
params=`cat << EOS
{
    "login_id": "${LOGIN_ID}",
    "password": "${PASSWORD}"
}
EOS
`
res=`curl -X POST \
-H "Content-Type: application/json; charset=utf-8" \
-H "Authorization: ${ACCESS_TOKEN}" \
-d "${params}" "${API_HOST}/api/v1/users/auth"`

Javascriptのオブジェクト指向について

Javascript書くときに雰囲気でオブジェクト指向してたので復習。

function構文使う

// 関数オブジェクトはnew演算子でインスタンスを作ることができる。
// インスタンスは、this.xxxで定義したプロパティにアクセスできる。
const Human = function(name){
  this.name = name;
  this.say = function() {
    return "My name is " + this.name;
  }
};
const human = new Human('taro');
human.say(); // -> My name is taro

// 関数オブジェクトのprototype代入したオブジェクトのプロパティは、
// 生成したインスタンス同士で"共有"されて使われる。
const prototypeObject = {
    type: 'Human',
    toString: function() {
        return this.type + ': ' + this.name;
    }
}
Human.prototype = prototypeObject;
const jiro = new Human('jiro');
jiro.toString(); // -> Human: jiro
const hanako = new Human('hanako');
hanako.toString(); // -> Human: hanako

// 共有されているので書き換えると、すべてのインスタンスに影響がある。
Human.prototype.type = 'Homo sapiens';
jiro.toString(); // -> "Homo sapiens: jiro"
hanako.toString(); // -> "Homo sapiens: hanako"

// prototypeを使ってErrorオブジェクトを継承をする
const MyError = function(msg) {
    this.message = msg || 'Exception occured';
    this.name = 'MyError';
};
MyError.prototype = new Error();
e = new MyError();
e.toString(); // -> "MyError: Exception occured"

class構文使う

classはプロトタイプベース継承の糖衣構文。ECMAScript 2015 で導入。
今まで出来ていたことを、class構文を使っても書けるようになりましたよ的なもの。

// class構文を使って関数オブジェクトを定義。
// インスタンスの作成/プロパティへのアクセスは同じ。
class HumanKlass {
  constructor(name) {
    this.name = name;
  }
  say() {
     return "My name is " + this.name;
  }
}
tom = new HumanKlass('tom');
tom.say(); // -> My name is tom

// extendsを構文を使ってErrorオブジェクトを継承
class MyErrorKlass extends Error {
  constructor(msg) {
    super();
    this.message = msg || 'Exception occured';
    this.name = 'MyError';
  }
}
ek = new MyErrorKlass();
ek.toString(); // -> "MyError: Exception occured"
MyErrorKlass.prototype // -> Errorのインスタンスが入っている。

db:migrateで巨大なテーブルへadd_column+default値設定をする

Rails + PostgreSQL環境での話。
数千万行あるような巨大なテーブルに対して、add_column+default設定をまとめて設定すると、サービスを止めてしまうほどに長時間テーブルロックかかってしまう。AccessExclusiveLockなのでSELECTも通らない。
原因は、ALTER_TABLEのテーブルロックかかった中で、カラム追加+デフォルト設定に加えて全レコードに対してUPDATE(デフォルト値で上書き)が走るため。

こんな書き方すると発生する。

class AddXxxIdToBigRecords < ActiveRecord::Migration
  def change
    add_column :big_records, :xxx_id, :integer, null: false, default: 0
  end
end

対応方法としては下記となる。
ただ、テーブルサイズがでかいので、1行ごとUPDATEするのに相当の時間がかかる。

class AddXxxIdToBigRecords < ActiveRecord::Migration
  # まずこれを設定しないと、upメソッド内全部にトランザクションかかってしまう。
  disable_ddl_transaction!

  def up
    # 最初にカラムを足す。AccessExclusiveLockかかるけど一瞬で終わる。
    # (MySQLと違って一瞬で終わる。)
    add_column :big_records, :xxx_id, :integer

    # 次にデフォルト値を設定する。AccessExclusiveLockかかるけど一瞬で終わる。
    # カラムの値は更新されない。
    change_column :big_records, :xxx_id, :integer, default: true

    # 全レコードに対してUPDATEを実行する
    update_all_with_default_value

    # NOT NULLのフタをする
    change_column :big_records, :xxx_id, :integer, default: true, null: false
  end

  def down
    remove_column :big_records, :xxx_id
  end

  #
  # 全レコードにたいして一つずつUPDATEをかけていく。
  # Rails5以上であれば、
  # in_batchedとupdate_allで複数行をまとめて更新かけたほうがいいかも。
  #
  def update_all_with_default_value
    total = BigRecord.count
    return true if total == 0

    start_f = Time.zone.now.to_f
    BigRecord.find_each.with_index do |record, i|
      n = i + 1
      record.update_columns(xxx_id: 0)
      if n % 1000 == 0 || n == total
        STDOUT.puts "-- update_all_with_default_value (#{i+1} / #{total})"
      end
    end
    end_f = Time.zone.now.to_f

    STDOUT.puts "  -> #{end_f - start_f}s"
    return true
  end
end

Grape+Rspec環境でAPIをテストする

既存RailsアプリにRspecを入れる - コピペコードで快適生活
の続き。

まず、JSONのテストを簡単にするgemを入れる。

gem 'json_expressions'

テストコードはこんな感じで書ける。

require 'rails_helper'
require 'json_expressions/rspec'

describe "GET api/orders/:order_ids" do
  context '存在しないIDを指定するとき' do
    it '空の配列が返る' do
      order_ids = "aaaa,bbbb"
      get "/api/v1/orders/#{order_ids}"
      pattern = {
        orders: [],
        total_count: 0
      }
      expect(response.status).to eq 200
      expect(response.body).to match_json_expression(pattern)
    end
  end
end

describe 'PATCH api/orders/:order_id/cancel' do
  context '存在しないIDを指定するとき' do
    it '204が返る' do
      order_id = "aaaa"
      params = {xxx: 2}
      patch "/api/v1/orders/#{order_id}/cancel", params
      expect(response.status).to eq 204
    end
  end
end