20190522のNode.jsに関する記事は2件です。

Google Cloud Run は WebRTCのシグナリングサーバーに使えるか?

はじめに

この資料は WebRTC Meetup Tokyo #21 のLT発表資料です。
(が、WebRTC成分はゼロです)

自己紹介

  • インフォコム株式会社 がねこまさし
  • @massie_g
  • WebRTC Meetup Tokyo / Begginers Tokyo スタッフ

Cloud Run とは

  • Cloud Run は Google Cloud Platform (GCP)の新しいサービス
  • 自分で作ったコンテナを、Googleが用意したサーバー環境上で動かせる
  • http(s) の口を持ったWebアプリ/Web APIサーバーを手軽に作れる
  • スケーリングも勝手に面倒見てくれる

Cloud Run でできないこと

  • コンテナを使っていても、できないことがある
  • 単体ではステートを持てない
  • データーの永続化はできない

→ 他のサービスを組み合わせて利用

※WebRTC的に気になること

  • シグナリングサーバーに使えるのか? (WebSocketは使えるか?)

Cloud Run を使うには

  • 準備
  • コンテナのビルド
  • コンテナのデプロイ

詳しくは別記事にて ... Google Cloud Run を使うまで


サンプルの準備

  • シンプルな、1対1のビデオチャットのサンプル
  • サーバー側(コンテナ)は、Webサーバーと、WebSocketサーバー機能を持つ
    • Node.js + express + ws
  • 同一ポートで、httpとwebsocketを使用
    • PORT環境変数で指定されたポート番号を使用

サーバーのソースコードの抜粋

GitHubはこちら https://github.com/mganeko/webrtc_1to1

// --- get PORT from env --
let port = process.env.PORT;

// --- prepare server ---
const http = require("http");
const WebSocketServer = require('ws').Server;
const express = require('express');

const app = express();
app.use(express.static('public'));
let webServer = null;
const hostName = 'localhost';

// --- http ---
webServer = http.Server(app).listen(port, function () {
  console.log('Web server start. http://' + hostName + ':' + webServer.address().port + '/');
});

// --- websocket signaling ---
const wsServer = new WebSocketServer({ server: webServer });

コンテナのビルド

Dockerfile

# Use the offical node.js image

FROM node:10.15.3-alpine

RUN mkdir /root/work 
WORKDIR /root/work/
RUN apk add git
RUN git clone https://github.com/mganeko/webrtc_1to1.git
WORKDIR /root/work/webrtc_1to1
RUN npm install


# Run the web service on container startup.
EXPOSE 8080
CMD [ "node", "server_1to1.js" ]

※本当は node を直接起動するのは良くないらしい
「Docker node PID 1」で検索


ローカルでコンテナを実行

コンテナのビルド
$ docker build -t mganeko/webrtc_1to1 .
コンテナの実行
$ docker run -d -e PORT=1323 -p 8002:1323 --name webrtc mganeko/webrtc_1to1

mganeko/webrtc_1to1 はイメージ名(適宜置き換えてください)

2つのブラウザで http://localhost:8002 に接続 → OK


Cloud Run で実行

CloudBuildでビルド
$ gcloud builds submit --project cloud-run-webrtc1to1 --tag gcr.io/cloud-run-webrtc1to1/webrtc-1to1
CloudRunにデプロイ
$ gcloud beta run deploy --project cloud-run-webrtc1to1 --image gcr.io/cloud-run-webrtc1to1/webrtc-1to1

それぞれの値は適宜置き換えてください。

cloud-run-webrtc1to1 ... プロジェクトID
webrtc-1to1 ... サービス名
gcr.io/cloud-run-webrtc1to1/webrtc-1to1 ... イメージのURL


結果は NG

Chrome DevConsoleのNetworkタブ
cloudrun_ws.png

WebSocketが繋がらない (400 Bad Request)


Cloud Run の制約

Container runtime contract より

  • The container must listen for requests on 0.0.0.0 on the port defined by the PORT environment variable.
  • Your container instances must start an HTTP server within 4 minutes after receiving a request.

どうやらHTTPのみ使えて、WebSocketは使えない


無理やりシグナリングするには

  • HTTPで無理やり → Long Polling (COMET) しかない

自分で Log Polling 実装するにはどうすれば..?

あ、それって Socket.io がフォールバックでサポートしているはず!


Socket.io版サンプル

  • シンプルな、1対1のビデオチャットのサンプル
  • サーバー側(コンテナ)は、Webサーバーと、Socket.ioサーバー機能を持つ
    • Node.js + express + socket.io
  • 同一ポートで、httpとsocket.io(websocketとフォールバック)を使用
    • PORT環境変数で指定されたポート番号を使用

サーバー側ソースコードの抜粋

https://github.com/mganeko/webrtc_1to1_socketio

// --- get PORT from env --
let port = process.env.PORT;

// --- prepare server ---
const http = require("http");
const express = require('express');

const app = express();
app.use(express.static('public'));
let webServer = null;
const hostName = 'localhost';

// --- http ---
webServer = http.Server(app).listen(port, function () {
  console.log('Web server start. http://' + hostName + ':' + webServer.address().port + '/');
});

// --- socket.io server ---
const io = require('socket.io')(webServer);
console.log('socket.io server start. port=' + webServer.address().port);

結果

  • websocket接続はエラー
  • xhr poll でシグナリングはできる

時間があったらデモ


厳密には

  • リクエストが増えると、自動的にスケールアウト
    • デフォルト設定では 80
  • 実運用では、同じインスタンスに繋がるとは限らない → シグナリングできない可能性あり
    • 試しに同時リクエスト数を 1 に設定 → シグナリングできない
    • ※どころか、コンソールの動きが一部おかしくなる??

シグナリングには、別の手段が順当

ちなみに Google App Engine FE は IaaSより高い


まとめ

  • Cloud Run 便利そう
  • が、シグナリングザーバーには向いてない

どうもありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sequelize + Jest のユニットテスト環境構築手順

はじめに

Sequelize な環境で開発を進めていくに当たり、
環境構築に手間取らず async/await なテストしたかったときに、
Jest を採用したらスグにテストを書き始められて良かったので記事にしました。

プロジェクトファイルは Github にアップしておきました↓
https://github.com/nuhs/sequelize-jest

Sequelize のセットアップ

まずは npm で Sequelize のインストールを行います。
今回は Sequelize の裏側に postgresql を使用するので、
ついでに pg ライブラリもインストールします。

⊨ npm install --save sequelize pg
⊨ npm install --save-dev sequelize-cli

インストール完了後、Sequelize の初期化コマンドを実行します。

⊨ npx sequelize init

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Created "config/config.json"
Successfully created models folder at "/Users/nika/Desktop/sequelize-jest/models".
Successfully created migrations folder at "/Users/nika/Desktop/sequelize-jest/migrations".
Successfully created seeders folder at "/Users/nika/Desktop/sequelize-jest/seeders".

初期化コマンドの実行に成功すると、Sequelize を使用するのに必要なファイル群がプロジェクトフォルダ内に生成されます。
プロジェクトルートに config/config.json が生成されているはずなので PostgreSQL と接続出来るように書き換えます。

config/config.json
{
  "development": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_development",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_test",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "username": "sequelize_jest",
    "password": "password",
    "database": "sequelize_jest_production",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

上記で設定した config/config.json そのままで動作確認するために、
テスト用に PostgreSQL ユーザを作成しました。
本記事のサンプルを利用するためだけの用途でご利用ください。

⊨ psql postgres
psql (11.2)
Type "help" for help.

postgres=# CREATE ROLE sequelize_jest with login password 'password';
CREATE ROLE
postgres=# alter role sequelize_jest CREATEDB;
ALTER ROLE
postgres=# \q

各種データベース周りの設定が完了次第、データベースの作成を行います。

⊨ npx sequelize db:create

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
Database sequelize_jest_development created.

データベースの作成に成功できれば、Sequelize のセットアップは完了です。

Sequelize でモデルを追加する

今回は Food というモデルを追加します。

⊨ npx sequelize model:create --name Food --attributes 'name:string, type:string'

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

New model was created at /Users/nika/Desktop/sequelize-jest/models/food.js .
New migration was created at /Users/nika/Desktop/sequelize-jest/migrations/20190522034233-Food.js .

実行に成功すると migrations というフォルダに PostgreSQL に Food モデル(テーブル)を追加するのに必要なファイルが生成されます。

早速 Sequelize 経由で PostgresSQL に Food モデル(テーブル)を追加します。

⊨ npx sequelize db:migrate

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
== 20190522034233-create-food: migrating =======
== 20190522034233-create-food: migrated (0.016s)

これで Sequelize を動作させるための最低限のセットアップが完了しました。
試しに Sequelize の動作確認してみます。

プロジェクトのルートに index.js を追加します。
index.js の中身は Food のデータを全て取得して標準出力するだけの処理です。

index.js
const { Food } = require('./models');

const main = async () => {
    const foods = await Food.findAll({raw: true});
    console.log(foods);
}

main();

index.js を追加したら早速実行してみます。

⊨ node index.js
Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food";
[]

現在は Food テーブルに何もデータが入っていないため、空の配列が出力されていますが、
正しく SQL が実行されていることが標準出力から確認出来ます。

Sequelize で初期データを用意する

空の配列だと正しくスクリプトが実行出来ているか分かりづらいため、
Food モデルのシードデータをいくつか用意します。

シードデータを追加するには seeders フォルダの中に追加する必要があります。
試しに seeders フォルダに food.js を追加して Food のデータをいくつか追加してみます。

seeders/food.js
module.exports = {
    up: async (queryInterface, Sequelize) => {
      const data = [{
        name: 'focaccia',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'french bread',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'muffin',
        type: 'bread',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'candy',
        type: 'sweets',
        updatedAt: new Date(),
        createdAt: new Date()
      }, {
        name: 'chocolate',
        type: 'sweets',
        updatedAt: new Date(),
        createdAt: new Date()
      }];
      return await queryInterface.bulkInsert('Food', data);
    },
    down: async (queryInterface, Sequelize) => {
      await queryInterface.bulkDelete('Food', null, {});
    },
  };

上記を追加した状態でコマンドを実行します。

⊨ npx sequelize db:seed:all

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "development".
== food: migrating =======
== food: migrated (0.008s)

コマンドの実行に成功したら、index.js を実行してみて、出力について確認してみます。

⊨ node index.js
Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food";
[ { id: 2,
    name: 'focaccia',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 3,
    name: 'french bread',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 4,
    name: 'muffin',
    type: 'bread',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 5,
    name: 'candy',
    type: 'sweets',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z },
  { id: 6,
    name: 'chocolate',
    type: 'sweets',
    createdAt: 2019-05-22T04:16:02.498Z,
    updatedAt: 2019-05-22T04:16:02.498Z } ]

無事にシードデータの追加に成功しました。

Jest のセットアップ

基本的には公式ページの手順に沿ってインストールしていきます。
https://github.com/facebook/jest

まずは jest をインストールします。

npm install --save-dev jest

jest を npm 経由で実行出来るよう package.json の scripts の test を設定します。

package.json
{
  "scripts": {
    "test": "jest"
  }
}

jest は実行できるようになったので、実際にテストファイルを追加します。
jest はテスト実行時に自動で __tests__ フォルダを見に行くため、
__tests__ フォルダの中にテストファイルを追加します。

まずは __tests__ フォルダをプロジェクトルートに追加します。
その後 sample.js を __tests__ フォルダに追加します。
sample.js は 1 + 2 は 3 かどうかをテストで確認しています。

__tests__/sample.js
test('adds 1 + 2 to equal 3', () => {
    const sum = 1 + 2;
    expect(sum).toBe(3);
});

実際に jest でテストを実行してみます。

⊨ npm run test

> sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest
> jest

 PASS  __tests__/sample.js
  ✓ adds 1 + 2 to equal 3 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.878s
Ran all test suites.

テストの実行に成功しました。

Sequelize のユニットテストを Jest で行うための準備

現状は開発環境下でテストを行っているため、
Sequelize でモデルを追加/更新を行うテストを行うたびに、
開発環境に影響が出てしまう状態となっています。

そのため、テストの実行が開発環境に影響を及ぼさないよう、
テスト環境下でテストをが実行できるように package.json を少し変更します。

package.json
{
  "scripts": {
    "test": "export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop",
    "db:setup": "sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all",
    "db:drop": "sequelize db:drop"
  }
}

__tests__ フォルダに Food モデルのテストを書くためのファイル food.js を追加します。
food.js は test 実行時に毎回シードデータが入るようになったため、その数が 5 であるかをテストしています。

__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    test('Total of Food is 5', async () => {
        const total = await Food.count();
        expect(total).toBe(5);
    });
});

テストを実行します。

⊨ npm run test

> sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest
> export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop


> sequelize-jest@1.0.0 db:setup /Users/nika/Desktop/sequelize-jest
> sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
Database sequelize_jest_test created.

Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
== 20190522034233-create-food: migrating =======
== 20190522034233-create-food: migrated (0.016s)


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
== food: migrating =======
== food: migrated (0.009s)

 PASS  __tests__/food.js
  ● Console

    console.log node_modules/sequelize/lib/sequelize.js:1176
      Executing (default): SELECT count(*) AS "count" FROM "Food" AS "Food";

 PASS  __tests__/sample.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.994s, estimated 1s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

> sequelize-jest@1.0.0 db:drop /Users/nika/Desktop/sequelize-jest
> sequelize db:drop


Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6]

Loaded configuration file "config/config.json".
Using environment "test".
Database sequelize_jest_test dropped.

出力が少し長くて見づらいですが、
テスト環境下でデータベースのセットアップからシードデータの追加、jest でのテスト実行、テスト実行後のデータベース削除が実行されているのが分かります。

テスト環境を用意する前の開発環境下でテストを実行してしまうと、
都度シードデータが開発環境の Food テーブルに追加されてしまい、
それらはテスト実行ごとにクリーンアップされてくれないため、
__tests__/food.js のテストは 2回目以降失敗するようになります。

Sequelize のユニットテストを Jest で書く

ここまで来ればあとは気軽にテストコードを追加していきます。

まずは models/food.js を少し改修します。
Food モデルの追加の際は type に bread か sweets しか設定出来ないようにしました。

models/food.js
'use strict';
module.exports = (sequelize, DataTypes) => {
  const Food = sequelize.define('Food', {
    name: DataTypes.STRING,
    type: {
      type : DataTypes.STRING,
      validate: {
        isValidType: function(value, next) {
          if(['bread', 'sweets'].includes(value)) {
            return next()
          }
          next('Unacceptable type of food.')
        }
      }
    }
  }, {});
  Food.associate = function(models) {
    // associations can be defined here
  };
  return Food;
};

Food モデルの type に bread か sweets を設定出来ないよう改修したので、その検証のためのテストコードを __tests__/food.js に追加してみます。

__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    //...
    describe('create', () => {
        describe('succeeded', () => {
            test('type of bread', async () => {
                const food = await Food.create({
                    name: 'jam bread',
                    type: 'bread',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('jam bread');
                expect(food.type).toBe('bread');
            });

            test('type of sweets', async () => {
                const food = await Food.create({
                    name: 'strawberry cake',
                    type: 'sweets',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('strawberry cake');
                expect(food.type).toBe('sweets');
            });
        });

        describe('failed', () => {
            test('type of chair', async () => {
                await expect(Food.create({
                    name: 'reclining chair',
                    type: 'chair',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true })).
                rejects.toThrow();
            });
        });
    });
});

テストを実行します。

⊨ npm run test
# jest のテスト結果の部分のみ出力
Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.123s
Ran all test suites.

全てのテストが無事に実行されました。試しにテストをわざと失敗させてみます。
__tests__/food.js に失敗するテストを追加します。

__tests__/food.js
const { Food } = require('../models');
describe('Food Model', () => {
    //...
        describe('failed', () => {
            //...
            test('type of chair(failed)', async () => {
                const food = await Food.create({
                    name: 'reclining chair',
                    type: 'chair',
                    updatedAt: new Date(),
                    createdAt: new Date()
                }, { validate: true });
                expect(food.id).not.toBeUndefined();
                expect(food.name).toBe('reclining chair');
                expect(food.type).toBe('chair');
            });
        });
    //...

テストを再度実行します。

⊨ npm run test
# jest のテスト結果の部分のみ出力
 FAIL  __tests__/food.js
  ● Food Model › create › failed › type of chair(failed)

    SequelizeValidationError: Validation error: Unacceptable type of food.

      at Promise.all.then (node_modules/sequelize/lib/instance-validator.js:74:15)

 PASS  __tests__/sample.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 5 passed, 6 total
Snapshots:   0 total
Time:        1.148s
Ran all test suites.

テストが失敗しました。

おわりに

Jest を使用することで楽にテスト環境をセットアップすることが出来ました。
豊富にマッチャーも用意されているので様々なパターンのテストが Jest のみで完結できそうな印象でした。

参考リンク
https://qiita.com/ckoshien/items/9afc60546ba1c9ce04f4
https://blog.honjala.net/entry/2018/08/08/022027
https://jestjs.io/ja/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む