20210228のdockerに関する記事は16件です。

Pythonからテスト用コンテナを手軽に作成する方法(testcontainers-python)

概要

pythonコード内でMySQLやPostgres、Seleniumなどのテストコンテナを作成することができる、testcontainersというライブラリがあるらしいというので試してみた結果です。
最初はDocker環境もいらないとかまさかないよね、と思いつつちょっと期待して試しましたが、予想通り、単にどこかにある既存のDockerホストに対して命令するだけだったのですが、それでも手動で上げ下げするよりは楽だと思われます。

対応範囲も広く、以下に対応しています。
- MySQL
- MariaDB
- PostgreSQL
- ElasticSearch
- Oracle DB
- MongoDB

MongoはなぜかReadmeに記載ないですが、以下のように対応しています。
https://testcontainers-python.readthedocs.io/en/latest/database.html#testcontainers.mongodb.MongoDbContainer

前提

自分のPCにDocker Toolboxを使ってコンテナ実行環境を作成しています。WindowsなのでDocker Desktop for Windowsを使いたいところですが、Hyper-Vを有効化できないため、インストールできません。またWindowsのバージョンも縛りがあり、WSL2も不可。
そこで少し古い方法ですが、Docker Toolboxというツールを使用しています。
https://docs.docker.com/toolbox/toolbox_install_windows/

Oracle Virtualboxという仮想化ソフトをHypervisorとして使用し、そのうえでLinuxを動かし、その上でDockerを起動する形になる。そのため、前提としてVirtualBoxをインストールする必要がある。(Toolboxをインストールする際に一緒にインストールすることもできる)

Host: Windows10(1803)
VirtualBox:6.0.12
Docker: 19.03.1

またDocker-Toolbox自体は起動された状態にしておく必要がある。(testcontainersからDocker-Toolboxの起動はできない。たぶん)

ちなみにコンテナ起動環境は、Windowsの場合、DOCKER_HOSTという環境変数を元に探しているとのこと。

echo %DOCKER_HOST%
tcp://192.168.99.100:2376

インストール

必要モジュールのインストールを実施。今回つかう範囲で必要だった上2つは追加で入れています。

pip install PyMySQL
pip install sqlalchemy
pip install testcontainers

公式にあるrequirementsファイルをpythonのバージョンに合わせて使用してもよいのですが、分量が多すぎたので個別にいれています。
https://github.com/testcontainers/testcontainers-python/tree/master/requirements

テスト

公式のテストコードそのままですが、以下のコードを記載したファイルを作成し、実行します。

app.py
import sqlalchemy
from testcontainers.mysql import MySqlContainer

with MySqlContainer('mysql:5.7.17') as mysql:
    engine = sqlalchemy.create_engine(mysql.get_connection_url())
    version, = engine.execute("select version()").fetchone()
    print(version)  # 5.7.17

イメージがない場合はpullしてきて起動してくれます。新規にコンテナを起動した場合は、コード実行後にコンテナも削除するようです。

テスト(MongoDBバージョン)

前提としてPyMongoをインストールしておきます。

pip install pymongo

以下のコードを実行。MySQL同様にバージョンを返しています。

app.py
from testcontainers.mongodb import MongoDbContainer

with MongoDbContainer("mongo:4.4.4") as mongo:
    db = mongo.get_connection_client()
    version = db.server_info()['version']
    print(version)  # 4.4.4

感想

今までは開発環境の操作とコーディング、テストは別々に行っていましたが、これをうまく使えば統合できそうだなと。ただまだそこまで至っていないので、まずはテストをしっかり書くところから取組みたいと思います。

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

fitbit senseの心拍データをリアルタイムで取得する

アプリケーション例: 心拍数に応じて強さを調整する五目並べAI

IMAGE ALT TEXT HERE
モンテカルロ木探索による五目並べAIにて紹介した五目並べAIの強さを、心拍数に応じて調整する。具体的には、心拍数が上がるとAIは弱くなり、逆に心拍数が下がるとAIは強くなる。

目的

fitbitデバイスを用いて、リアルタイムに心拍データを取得する
ただし後述する「現時点での制限」のように、実行時におけるいくつかの制限がある。
主な使用用途
心拍データを用いたゲームのプロトタイプ作成
心理学的実験のための簡易心拍取得装置

環境

心拍の取得(身に付けるもの)
fitbit sense (他のfitbitでもたぶん可)
スマートフォンのfitbit app (ここで試したのはAndroid)
開発、実行の制御(開発するもの。また現時点では実行時にも必要なもの)
windows 10 home (開発だけでなく実行の制御も現時点ではしている)
docker (websocket serverの構築のために使用)
websocket server(心拍データを受け取り、配信するもの)
Google Cloud Platform (この中のContainer Registryでdocker imageを管理し、Cloud Runでそのimageを実行している)

構成

  • 図の黒い矢印は開発時の関係、黄色の矢印は実行時のデータ経路を示す。
  • fitbitのデバイスから直接、サーバ等に心拍データを送信することができないため、スマートフォンのfitbit appにデプロイしたcompanion(fitbit companion apiを用いて開発したfitbit appの中で動くもの)経由でサーバにデータを送信する。
  • サーバへの接続方法は、http get/postwebsocketが用意されている。ここでは通信効率の良いと思われるwebsocketを使用する。
  • websocketのサーバには、googleのcloud runを使用する。cloud runは、dockerのイメージをそのまま実行できる環境である。dockerとは、OSを含めて実行環境をひとまとめにパックできるようなもので、ローカルPCのOSに依存せずにサーバの開発などができる。今回は、debianディストリビューションのlinuxをOSとしたpythonの環境をdocker hubのpython imagesから参照して、そこにpython上でASGI(asynchronous server gateway interface): 非同期サーバゲートウェイインターフェースを構築するためのuvicornによってwebsocketサーバを構築する。

現時点での制限

  • 約2分間の実行後にfitbitデバイスのアプリが終了する。現時点では、fitbitデバイスをタップしてスリープさせないようにする必要がある。fitbit device apiのappTimeoutEnabledをfalseにしてアプリのタイムアウトを無効化しても、なぜか約2分でアプリは落ちる。fitbitデバイスの画面設定で「常に画面をON」をつけると、少し実行できる時間は伸びて、一度試してみたところ、3分半ぐらいになった。
  • Websocketサーバとして使用するGoogle Cloud Runは、デフォルトで5分間で接続が切れる。Cloud Runのマニュアルには、非アクティブが5分続くとタイムアウトするとの説明があるが、Cloud Runのwebsocketでの使用に関するマニュアルにも示されているように、Websocketを通信し続けていても、5分で接続は切れる。Cloud Runの設定でタイムアウトを15分までなら伸ばすことができるとの説明があるが試してはいない。クライアントでwebsocketの接続断イベントを取得し、再接続を行うことによって疑似的な永続的接続を構築することは出来るが、再接続までの受信データをロストするなどの問題はある。
  • スマホのfitbit appを立ち上げてスリープさせない方が良い。スリープ状態になると、websocketの通信が途絶えることがある。

fitbitデバイスとスマートフォンで動作するfitbit app内のアプリの開発

fitbit studioによるアプリ開発


fitbit studioにアクセスし、fitbitアカウントでログインする。


Starterテンプレートを用いて新規プロジェクトを作成する。


左側のappはfitbitデバイスで動作するもの、companionはスマートフォンのfitbit appの中で動作するもの。resourcesはスタイル等の設定である。


左側で app / index.js を選択して、以下のコードを入力する。

index.js
/*
  fitbitデバイスで動作する。
*/
import * as messaging from "messaging";
import { HeartRateSensor } from "heart-rate";
import { me } from "appbit";

// fitbit appとの通信を開始する。
messaging.peerSocket.addEventListener("open", (evt) => {
  start_heartRateSensor();
});
messaging.peerSocket.addEventListener("error", (err) => {
  console.error(`Connection error: ${err.code} - ${err.message}`);
});
// fitbit appと接続後に、心拍データの取得を行う。
function start_heartRateSensor(){
  const hrm = new HeartRateSensor({ frequency: 1, batch: 1 });
  hrm.addEventListener("reading", () => {
    sendMessage({
      heartRate: hrm.heartRate ? hrm.heartRate : 0
    });
  });
  hrm.start();
  //なぜか効かない。
  //me.appTimeoutEnabled = false;
}
// fitbit appに心拍データを送信する。
function sendMessage(data) {
  if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
    messaging.peerSocket.send(data);
  }
}

importされているmessagingは、スマートフォン fitbit appとの通信を行う、HeartRateSensorは心拍の取得、meはこのアプリのタイムアウトの設定を行うためのものである。ただし、meを用いたタイムアウトの設定はなぜか効果がないため、現在はコメントアウトして使用していない。

HeartRateSensorの生成時に frequencyで心拍取得の間隔を指定している。現在は1秒間隔で心拍情報を取得する。心拍情報とは、1分間に波打つ回数である。


次に、companion / index.jsを選択する。

index.js
/*
  スマートフォンのfitbit app内で動作する。
*/
import * as messaging from "messaging";

// websocketの接続先
const wsUri = "wss://_______________________________________/xx";
const websocket = new WebSocket(wsUri);

// それぞれのリスナを設定する。
websocket.addEventListener("open", onOpen);
websocket.addEventListener("close", onClose);
websocket.addEventListener("message", onMessage);
websocket.addEventListener("error", onError);
function onOpen(evt) {
  console.log("CONNECTED");
  websocket.send("connected")
}
function onClose(evt) {
  console.log("DISCONNECTED");
}
function onMessage(evt) {
  console.log(`from websocket server: ${evt.data}`);
}
function onError(evt) {
  console.error(`ERROR: ${evt.data}`);
}

// デバイスからのデータを受信したときに、同データをwebsocketサーバに送信する。
messaging.peerSocket.addEventListener("message", (evt) => {
  if(websocket.readyState===WebSocket.OPEN){
    const data = JSON.stringify(evt.data);
    websocket.send(data);
  }
});

import の messagingは、fitbitデバイスのアプリと通信を行う。wsUriにwebsocketのサーバのアドレスとエンドポイントを設定する。デバイスからの心拍情報を取得したら、そのままwebsocketでサーバに送信するというプログラムになっている。


最後に、package.jsonにて Heart RateとInternetのRequested Permissionsのチェックを付ける。

アプリのデプロイと実行確認

スマートフォンでの設定


スマートフォンのfitbitアプリを起動して、左上のアカウントをタップする。


心拍を取るデバイスをタップする。


開発者メニューをタップする。


開発者用ブリッジ接続をONにする。

fitbitデバイスでの設定


画面を横にスライドする。


設定をタップする。


開発者用ブリッジ接続をタップする。


オンにする。

fitbit studioでの設定


fitbitデバイス、スマートフォンのfitbit appと接続するために、Select a phone Select a deviceメニューを開く。Phones Devicesが見えていなければ、Refreshを選択し、表示されるそれぞれのデバイスをクリックして接続する。


接続すると、それぞれのデバイス名の横に Connectedと表示される。

実行確認


fitbit studioの Runボタンを押すと、コンソールに心拍情報が表示される。ただし、現時点での制限で述べたように約2分間で、アプリは終了する。その時には、もう一度 Runボタンを押す必要がある。
またfitbitデバイスには、fitbit studioのresourcesで設定したデフォルトのHello World!が表示されている。

一度、fitbit studioでアプリをデプロイした後には、fitbitデバイスからアプリを起動することができる。この際に、スマートフォンのfitbit appは、起動しておいた方が良い。


fitbitデバイスで横にスクロールさせていくと、最後にデプロイしたアプリが表示される。


このアプリをタップすると、起動する。この時に、自動的にスマートフォンのfitbit app内の開発したアプリも起動する。

dockerによるwebsocketサーバの構築

windows 10 homeへの dockerのインストール


Docker DesktopからDownload for Windowsをクリックしてダウンロード、インストールを開始する。インストールオプションは、基本デフォルトで行う。


インストール終了後にWSL 2のインストールができていないとのメッセージが表示されるので、そのメッセージにあるURLをクリックする。


表示されるサイトのx64マシン用 WSL2 Linuxカーネル更新プログラム パッケージをクリックし、インストールする。


Dockerが立ち上がるとチュートリアルの開始となるが、とりあえずスキップして問題はない。


インストールが完了する。左側のContainers / Appsは、それぞれのコンピューティング環境、Imagesはその環境を構築するための元(昔のインストールCDのようなもの)である。

websocketサーバの構築

適当なフォルダを作成して、次に示す3つのファイルを作成する。

Dockerfile
FROM python:3.8-alpine

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN apk add --no-cache build-base \
 && pip install --no-cache-dir --trusted-host pypi.python.org -r requirements.txt \
 && apk del build-base

COPY . .

CMD [ "uvicorn", "simple_main:app", "--reload", "--host", "0.0.0.0", "--port", "8080" ]

Dockerによるコンピューティング環境構築用のDockerファイルであり、FROM python:3.8-alpineという指定によってdocker hubのpythonの上に構築している。このpythonは、DebianディストリビューションのlinuxをOSとし、pythonをインストールしてくれる。タグと呼ばれる3.8-alpineは、pythonのバージョンを3.8とし、alpineというタグ付けされたpythonの最小構成を作るものである。
最後のCMDで指定された uvicornの実行は、ポート番号を 8080 にする必要がある。これは、このwebsocketサーバを最終的にはgoogle cloud runで実行するために必要なことであり、cloud runはデフォルトで8080ポートを使用するために、これに合わせるものである。cloud runは、外向きのインターフェースは、https / wss の433であるが、そこに到達したデータは、内部の8080に転送されるという仕組みになっている。よって開発するサーバは、8080ポートで待っている必要がある。

requirements.txt
uvicorn[standard]
fastapi

requirements.txtファイルはDockerファイルから参照されて、必要なpythonコンポーネントを記述しておく。

simple_main.py
from fastapi import FastAPI
from starlette.websockets import WebSocket, WebSocketDisconnect

# fast app
app = FastAPI()

# client websockets list
clients = []

# notify to all clients
async def notify(msg:str):
  for websocket in clients:
    await websocket.send_text(msg)

# notification generator
async def notification_generator():
  while True:
    msg = yield
    await notify(msg)
notification = notification_generator()

# websocket endpoint
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
  await websocket.accept()
  clients.append(websocket)
  try:
    while True:
      data = await websocket.receive_text()
      await notification.asend(data)
  except WebSocketDisconnect:
    clients.remove(websocket)

# startup (preparing notification generator)
@app.on_event("startup")
async def startup():
  await notification.asend(None)

Websocketサーバのプログラムで、fastapiと呼ばれるpythonのwebフレームワーク、uvicornというASGI(Asynchronous Server Gateway Interface) :非同期サーバーゲートウェイインターフェイスのインプリメンテーションを使用する。
Websocketの接続先であるエンドポイントは、 @app.websocket("/ws")で指定した /ws とし、そこに接続されるwebsocketをすべて保存し、あるwebsocketから送られてきたデータは、その送信元websocketも含めて、全ての接続先websocketへ配信するようになっている。

docker build

Dockerfileのあるフォルダで、次のコマンドを実行してdocker imageを作成する。

docker build -t websocket_server .

-tオプションのwebsocket_serverは作成するdocker imageの名前である。


ビルドが成功すると、Docket画面のimagesに、websocket_serverというイメージが作成されている。

実行確認

次のコマンドを実行すると、websocket_server イメージからコンテナが作成されてコンピューティング環境が実行される。

docker run -p 80:8080 websocket_server

オプションの -p は、dockerの外であるwindowsPCからdockerの中にあるアプリに接続するためにポートを転送する設定であり、この場合は、windows pcの80ポートを、dockerの8080ポートに転送するとなる。

websocket_client_test.html
<!DOCTYPE html>
<!--------------------------
    websocket client html
    ・フォームからサーバにデータを送信できる。
    ・サーバから受信したデータを表示する。
-->
<html>
    <head>
        <title>My Heart Rate</title>
    </head>
    <body>
        <label for="test_form">テストデータの送信</label>
        <form action="" onsubmit="sendMessage(event)" id="test_form">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            //const ws = new WebSocket("wss://_______________________________________/xx");
            const ws = new WebSocket("ws://localhost/ws");
            ws.onmessage = function(event) {
                const messages = document.getElementById('messages')
                const message = document.createElement('li')
                const content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                const input = document.getElementById("messageText")
                ws.send(input.value)
                event.preventDefault()
            }
        </script>
    </body>
</html>

localhostのwsに接続するhtml, javascriptを作成し、ブラウザで表示する。


サーバに送信したデータがループバックで戻ってきて、リスト表示される。同時に複数のブラウザを開けば、1つのブラウザから送信したデータは、全てのブラウザに戻ってくることが確認できる。


この時に、dockerのコンソールには、このような感じで表示されている。

Google Cloud Runによるwebsocketサーバの公開

Google Cloud Platformでプロジェクトの作成


Google Cloud Platformのプロジェクト選択から新規プロジェクトを作成する。もちろん、既にあるプロジェクトに新たなサービスを追加する場合は、その既存プロジェクトを選択してもよい。


プロジェクト名を入力して作成する。

おそらく課金の設定についてもする必要がある。

Container Registryの設定

直接、Cloud Runを操作してdocker imageからアプリを起動することも可能であるが、ここでは、docker imageのcontainer registryへのアップロード、そしてcontainer registryにあるimageからcloud runのアプリを生成、と段階を踏んで行っていくこととする。


左側からContainer Registryを選択する。


Container registryを有効化する。

docker tag [docker image id] gcr.io/[google cloud project id]/docker image name]

dockerコマンドによって、先に作成したdocker imageにタグ付けを行う。image id, project id, image nameの確認方法は、次に示す。またgcr.ioは、とりあえずcloud runのデプロイ先をus-central1 (アイオア)にすることを前提としている。


google cloud project idの確認は、google cloud platformのプロジェクトの選択等で行うことができる。


docketのイメージ名、image idの確認は、docker画面の左側のimagesタブから行う。


docker imageへのタグ付けが成功すると、docker画面ではこのように表示されている。

docker push gcr.io/[google cloud project id]/websocket_server

タグ付けされたimageをdocker pushでgoogleにアップしようとすると、以下のように権限が無いとのエラーが表示される。


次に、権限付与のためにGoogle Cloudの認証方法のサイトの説明に従ってまず、Google Cloud SDKのインストールを行う。


Cloud SDKのドキュメントに接続し、Google Cloud SDKのインストールを選択する。


windowsのタブを選択し、Cloud SDKをダウンロードしてインストールする。インストールは基本的にデフォルト設定のままで行う。


インストール後に、Start Google Cloud SDK ShellとRun gcloud init to configure the Cloud SDKのチェックを付けてFinishボタンを押す。


コンソールでログインを要求されるので、 y を入力して enterを押す。


ブラウザが表示されて許可を求めるためにGoogleアカウントの選択が表示される。使用するアカウントを選択する。


次に表示される画面で許可を押す。


コンソールでは、プロジェクトを選択するところで止まっているので、先に作成したプロジェクトを選択する。

次に、container registryにアップロードをするdockerにgoogleの権限を与えるために、gcloud 認証情報ヘルパーを参考に設定を進めていく。

gcloud auth login

gcloudにログインするために、上記のコマンドを実行する。表示されるブラウザでは、先ほどと同様にgoogleアカウントを選択して、許可を押す。

gcloud auth configure-docker

gcloudコマンドで、dockerを構成する。


このような感じに実行できていれば、たぶん、大丈夫。

docker push gcr.io/[google cloud project id]/[docker image name]

あらためてdockerコマンドでdocker imageをcontent registryにアップロードする。


docker pushコマンドの結果として、このような感じで表示される。


Google Cloud Platformのcontainer regstryで確認すると、websocket_serverという名前のimageが追加されている。

Cloud Runの設定

gcloud run deploy websocket-server --image gcr.io/[google cloud project id]/websocket_server

gcloudコマンドによって、container regstryにアップしたdocker imageからサービスを構成する。deploy websocket-serverのwebsocket-serverはcloud run上のサービス名となる。このサービス名では _ (アンダースコア)が使用できないため、- (ハイフン) としている。


target platformは[1] Cloud Run (fully managed)を、projectを有効化するかには y を回答する。


リジョンには[18] us-central1を、unauthenticated invocationsには y を選択する。


google cloud platformのcloud runで確認すると、websocket-serverという新しいサービスが登録されている。この時点で既に動作している。URLがサービスのURLであるため、動作確認のために控えておく。

実行確認

websocketの送受信


先に示したwebsocketクライアント用のhtmlファイルの接続先urlを、cloud runのサービスurlに変更し、プロトコルもwsからwssに変えてブラウザで表示する。


送信したデータが、全てのクライアントに返ってくることを確認する。また、クラウド上のwebsocketサーバなので、例えば、スマートフォンから参照しても同様の結果を得ることができる。

心拍データの送受信


fitbit studioのcompanion / index.js の wsUriを cloud runのサービスURLに変更する。


fitbit studioでfitbitデバイス、fitbit appのcompanionを起動すると、websocketサーバ経由でリアルタイムに心拍データを取得することができる。

Cloud Runのタイムアウト対策

次のは、websocketが5分でタイムアウトしたときにクライアント側から接続し直すjavascriptである。ついでに、受信時にリストを下に増やすのではなく、上に追加する形で最新が常に一番上になるように変更している。

websocket_client.html
<!DOCTYPE html>
<!--------------------------
    websocket client html
    ・フォームからサーバにデータを送信できる。
    ・サーバから受信したデータを表示する。
-->
<html>
<head>
    <title>My Heart Rate</title>
</head>

<body>
    <label for="test_form">テストデータの送信</label>
    <form action="" onsubmit="sendTextInput(event)" id="test_form">
        <input type="text" id="messageText" autocomplete="off" />
        <button>Send</button>
    </form>
    <ul id='messages'>
    </ul>
    <script>
        const url = "wss://_______________________________________/xx";
        //const url = "ws://localhost/ws";

        function sendTextInput(event) {
            const input = document.getElementById("messageText");
            sock.send(input.value);
            event.preventDefault();
        }
        const sock = new function () {
            const self = this;
            let ws = undefined;
            self.init = () => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    ws.close();
                }
                ws = new WebSocket(url);
                ws.onmessage = (event) => {
                    const messages = document.getElementById('messages');
                    const message = document.createElement('li');
                    const content = document.createTextNode(event.data + " [" + (new Date()) + "]");
                    message.appendChild(content);
                    messages.prepend(message);
                };
                ws.onclose = (event) => {
                    console.log("closed and re-init");
                    self.init();
                };
            }
            self.send = (msg) => {
                if (ws.readyState === WebSocket.OPEN) {
                    ws.send(msg);
                } else {
                    console.log("cannot send " + msg + " [" + ws.readyState + "]");
                }
            };
            self.init();
        }
    </script>
</body>
</html>

sock変数では、wsという名前のwebsocketのインスタンスを保持し、それをサーバに接続するinitという名称のメソッドを有する。そのinitメソッドは、websocketが接続断されたときのイベント onclose の中で再度呼び出される。

参考にさせて頂いたサイト

Windows 10 HomeへのDocker Desktop (ver 3.0.0) インストールが何事もなく簡単にできるようになっていた (2020.12時点)

FastAPIでWebSockets

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

DockerでPHPUnitを少し使ってみる(Composer使用)

はじめに

Dockerをある程度使える前提で書いていきます。

『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

★テスト記述する際のお決まり事項
・Class という名前のクラスのテストは、ClassTest という名前のクラスに記述
・ClassTest は、(ほとんどの場合) PHPUnit\Framework\TestCase 
・テストは、test* という名前のパブリックメソッドとなります。(なのでtestSample()やtestA()という書き方になる)
・テストメソッドの中で assertSame() のようなアサーションメソッド (アサーション を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。

★ユニットテストを書くそもそもの目的
バグの発見と修正や コードのリファクタリングを開発者がやりやすくすること。
ソフトウェアのドキュメントとしての役割を果たすこと。
私的には同じバグを繰り返さないために書くものという感じで理解しています。

★どこをテストすべきか
ひとつのユニットテストがカバーするのは、 通常はひとつの関数やメソッド内の特定のルートだけとなる

上記内容はほぼ以下URLからの参考です。
https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html

様々なチェックメソッドは以下
https://phpunit.readthedocs.io/ja/latest/assertions.html#appendixes-assertions

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

全体の構成参考画像

スクリーンショット 2021-02-28 19.49.46.png
index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #Dockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html
スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

コンテナに入ってもう一仕事する

:sun_with_face:以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

:sun_with_face:コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

:sun_with_face:コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

:sun_with_face:composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

:sun_with_face:全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

テスト実行結果参考以下URL
https://phpunit.readthedocs.io/ja/latest/textui.html

表示 意味
. テストが成功した際に表示されます。
F アサーション失敗
E テストメソッドで何かしら失敗
I テストがふかんぜんまたは未実装

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

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

DockerでPHPUnitを少しさわってみる

はじめに

Dockerをある程度使える前提で書いていきます。

これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #apachのDockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html

コンテナに入ってもう一仕事する

・以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

・コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

・コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

・composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

・全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

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

DockerでPHPUnitを少しさわってみる(Composer使用)

はじめに

Dockerをある程度使える前提で書いていきます。

これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。

よろしくお願いします!:bow_tone1:

後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!

では早速

環境作成

以下のようにファイルを作成して、『docker-compose.yml』ある場所でdocker-compose up --build -dを叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip defaultで調べられます。意味は『defaultとして作成したドッカーマシンのip』

スクリーンショット 2021-02-28 19.51.59.png

※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。

全体の構成参考画像

スクリーンショット 2021-02-28 19.49.46.png
index.php
<?php
require_once 'sample.php';

$sample = new sample\Sample();

echo $sample->a();
sample.php
<?php
namespace sample;

class Sample
{
    public function a()
    {
        return 'あ';
    }

    public function i()
    {
        return 'い';
    }
}
SampleTest.php
<?php
use PHPUnit\Framework\TestCase;

require_once 'sample.php';

class SampleTest extends TestCase
{
    public function testSample()
    {
        $sample = new sample\Sample();

        //同じ値、同じ型でない場合エラー
        $this->assertSame($sample->a(), 'あ');
    }
}
docker-compose.yml
version: '3'
services:
  apach:
    container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更
    build: ./  #apachのDockerfileを使用してコンテナ作成
    ports:
      - 81:80
    volumes:
      - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れる
From php:7.4-apache

RUN apt-get update \
&& apt-get install -y \
zip \
unzip

WORKDIR /var/www/html

コンテナに入ってもう一仕事する

:sun_with_face:以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps

:sun_with_face:コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash

:sun_with_face:コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php

:sun_with_face:composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html

:sun_with_face:全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる

スクリーンショット 2021-02-28 20.03.20.png

最後に

以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!

参考

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

コマンドライン(command line)からDockerのコンテナ(container)をvscodeで直接開く

containerを直接vscodeで開く

ちょっと感動したので記載。

起動したdocker containerをvscodeで開く場合、コマンドパレットやサイドバーから開くことが一般的だと思いますが、container名さえわかれば、下記コマンドでコマンドラインから直接開くことができます。

code --folder-uri=vscode-remote://attached-container+{container_hex}{path_to_open_folder}

上記のcontainer_hexの計算方法は以下(例としてbashです。)

text="{\"containerName\":\"/${1}\"}"
container_hex=printf $text | od -A n -t x1 | tr -d '[\n\t ]'

${1}には起動したいコンテナの名前を指定してください。

これでdocker containerをvscodeで直接開くことができます。
開発環境にscript化しておいておけば手間が省けるかもしれません。

参考

script化してくれてる方がいらっしゃいます。
https://github.com/geircode/vscode-attach-to-container-script

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

docker内でコマンド実行すると「-bash: apt-get: コマンドが見つかりません」と怒られる

例えば、psコマンドを使いたい場合に以下を実行すると、

$ docker-composer exec container_name apt-get update && apt-get install -y procps

以下のようなメッセージが表示される

-bash: apt-get: コマンドが見つかりません

bash経由で実行したらできた。

$ docker-compose exec container_name bash -c 'apt-get update && apt-get install -y procps'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vagrant+VirtualBoxでのDocker環境

目的

WindowsでDocker環境を構築するに際して「Docker Desktop for Window」を利用してみようとしましたが、
諸々の環境依存の問題に当たることがあり、過去の経験上で比較的問題のでることが少なかった
「Vagrant+VirtualBox」の環境上にDocker環境を作成したものです

方針

  • Windowsの依存問題を避けるため、VMはLinux系のもの
  • VM/コンテナを両方起動することの無駄には目をつぶる

Vagrant起動スクリプト

Vagrant.configure("2") do |config|

  config.vm.box = "centos/7"
  config.ssh.insert_key = false

  config.vm.network "forwarded_port", guest: 80, host: 80   # HTTP
  config.vm.network "forwarded_port", guest: 443, host: 443  # HTTPS
  config.vm.network "forwarded_port", guest: 3306, host: 3306  # MySQL
  config.vm.network "forwarded_port", guest: 6379, host: 6379  # Redis

  config.vm.synced_folder "./workDir", "/home/vagrant/mnt/workDir"

  # Docker
  config.vm.provision :docker, run: "always"

  # docker-compose
  config.vm.provision :docker_compose,
    compose_version: "1.27.4",
    run: "always"
end

個別の補足

  config.vm.box = "centos/7"
  config.ssh.insert_key = false

今回はCentOS7を採用
2行目はSSHのキーを省略する設定、ローカル環境でのみ使用するためこの設定としていますが、
あまり好ましくない設定と思われるので、本記事を参考にする際はきちんと調べて適切に設定してください

  config.vm.network "forwarded_port", guest: 80, host: 80   # HTTP

VirtualBoxのポートフォワーディングに関する設定
開発環境に必要なポートを定義しています

  config.vm.synced_folder "./workDir", "/home/vagrant/mnt/workDir"

ホストマシンとゲストマシンのディレクトリの同期設定です
「/home/vagrant/mnt/」は「Vagrant+VirtualBox+CentOS7」の環境のマウント用パスです

  config.vm.provision :docker, run: "always"
(※中略)
  config.vm.provision :docker_compose,

Dockerおよびdocker-composeをインストールするための設定です

あとがき

かなりザックリすぎに記事を書いた自覚はあるので、後日時間があれば追記修正する気持ちはあります……

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

Windows10 Home に Docker Desktop をインストールしてMySQLサーバを起動してSQLで動作確認するまでの手順2021/02

Windows を最新にする

バージョンが降るとインストールできない場合があるので、最新かどうか確認します。
Windows キーを押して設定ボタン
image.png
を押します。
image.png
表示された設定画面の右上の Windows Update 部分をクリックします。
image.png
image.png

Docker ホームページを開く

下記 URL の Docker 公式ページを開きます。(英語ページです)
https://www.docker.com
image.png

インストーラーをダウンロード

Get Started ボタンを押します。
image.png

Docker Desktop の Download for Windows ボタンを押します。
image.png
image.png

インストーラーを実行

Docker Desktop Installer.exe がダウンロードされますので、
ダウンロードが完了したら実行します。
image.png

チェックは全てつけた状態で Ok ボタンを押します。
image.png

しばらく待ちます。
image.png

終了したら Close and restart ボタンを押して再起動します。
image.png

カーネル更新プログラムのインストール

再起動後以下の画面が出たら https://aka.ms/wsl2kernel のリンクを開きます。
※この画面は後で使うので消さないでおいておきます。
image.png

x64 マシン用 WSL2 Linux カーネル更新プログラムパッケージ のリンクをクリックします。
image.png
保存ボタンを押してダウンロードします。
image.png
ダウンロードした wsl_update_x64.msi を実行します。
image.png

Next ボタンを押します。
image.png

Finish ボタンを押します。
image.png

Restart ボタンを押します。
image.png

しばらく待つとDockerが起動します。
image.png

この画面は今回特に使わないので閉じます。

MySQLで動作確認

まずバージョン情報を見てみます。

Windows+R キーを押します。
image.png

cmd と入力して OK ボタンを押します。

image.png

Docker バージョン情報表示

docker version と入力し Enter キーを押して実行します。
image.png

ずらずらと情報が表示されます。
image.png

つぎに、試しにMySQLを動かしてみます。
以下のコマンドを入力して実行します。

MySQLサーバのDockerコンテナを起動

MySQLサーバの起動
docker run -e MYSQL_ROOT_PASSWORD=password mysql

image.png
ずらずらと表示が出てきて ready for connections. という行が表示されれば起動完了です。

docker run で docker コンテナという仮想サーバ的なものを起動します。
mysql というのが立ち上げるコンテナの種類になります。
-e MYSQL_ROOT_PASSWORD=password は環境変数の設定で MySQL の root ユーザのパスワードを指定しています。

次にサーバに入ってみます。
Windows+R キーでもう一つ別のコマンドプロンプトを起動します。
image.png

以下のコマンドで動作中のコンテナ一覧が確認できます。

Dockerコンテナ一覧表示

実行中Dockerコンテナ一覧を表示
docker ps

image.png

この一覧にある mysql の CONTAINTER ID をコピーしておきます。
※IDは毎回変わります。今回は 11576ddb996a です。

そのID を指定して以下のコマンドを実行します。

Dockerコンテナのシェルに入る

Dockerコンテナのシェルに入る
docker exec -it 11576ddb996a bash

image.png

そうすると root@11576ddb996a:/# が表示されます。
これは mysql サーバ内のシェルに入った状態です。

次に以下のコマンドでMySQLクライアントを起動します。

MySQLクライアント起動

MySQLクライアント起動
mysql -uroot -p

パスワードを聞かれるので環境変数で指定した password を指定します。

以下のように、mysql> が表示されればOKです。

MySQLクライアント起動後
root@11576ddb996a:/# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.0.23 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

データベース一覧

データベース一覧を表示してみます。

データベース一覧表示
show databases;
データベース一覧表示後
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.01 sec)

データベース作成

データベースを作成してみます。

データベース作成
create database testdb;
データベース作成結果
mysql> create database testdb;
Query OK, 1 row affected (0.04 sec)

データベース切り替え

testdbに入ります。

データベース切り替え
use testdb;
データベース切り替え結果
mysql> use testdb;
Database changed

テーブル作成

テーブルを作ってみます。

テーブル作成
create table test_users(id int auto_increment primary key, name varchar(10), birthday date);
テーブル作成結果
mysql> create table test_users(id int auto_increment primary key, name varchar(10), birthday date);
Query OK, 0 rows affected (0.04 sec)

レコード挿入

レコードを挿入してみます。

レコード挿入
insert into test_users(name,birthday) values('tanaka', '1990-01-02');
レコード挿入結果
mysql> insert into test_users(name,birthday) values('tanaka', '1990-01-02');
Query OK, 1 row affected (0.04 sec)

テーブル内容確認

テーブルの内容を確認します。

テーブル内容確認
select * from test_users;
テーブル内容確認結果
mysql> select * from test_users;
+----+--------+------------+
| id | name   | birthday   |
+----+--------+------------+
|  1 | tanaka | 1990-01-02 |
+----+--------+------------+
1 row in set (0.00 sec)

問題なく使えますね。(日本語を使っていないのは実は現状では扱えないためです。この後日本語も扱う方法を紹介します。)

後片付け

mysql> が表示されている状態で exit を実行し、MySQL クライアントを終了します。

root@11576ddb996a:/# が表示されている状態で exit を実行し、シェルを終了します。

コンテナ停止

次に以下のコマンドでMySQLサーバを停止します。
※ 11576ddb996a は コンテナID です。docker ps で確認できるものです。

Dokcerコンテナ停止
docker stop 11576ddb996a

停止されているか docker ps で確認します。

実行中コンテナ一覧
C:\Users\nakaz>docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

一覧が空になっているのでコンテナが停止したことが確認できました。

コンテナの起動

再度起動したいときは以下のコマンドを実行します。

コンテナ起動
docker start 11576ddb996a

起動したいときにコンテナID がわからなくなった場合は、以下のコマンドで確認できます。

停止中も含めたコンテナ一覧
docker ps -a
停止中も含めたコンテナ一覧実行結果
C:\Users\nakaz>docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                     PORTS     NAMES
11576ddb996a   mysql     "docker-entrypoint.s…"   41 minutes ago   Exited (0) 4 minutes ago             naughty_curran

コンテナ削除

もしコンテナが不要になって消したい場合は以下のコマンドを実行します。

コンテナ削除
docker rm 11576ddb996a
コンテナ削除結果
C:\Users\nakaz>docker rm 11576ddb996a
11576ddb996a

コンテナ削除後確認

再度 docker ps -a を実行すると消えていることが確認できます。

コンテナ削除後確認結果
C:\Users\nakaz>docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

MySQL で日本語入力に対応する

先ほどのやり方だと MySQL で日本語が正しく扱えない状態ですので、日本語対応の環境を作ってみます。
また、タイムゾーンも世界標準時(UTC)となっているので日本時間にしてみます。

メモ帳を開いて以下の内容を入力します。

Dockerfile 作成

FROM mysql
RUN apt-get update && apt-get install -y locales-all
ENV LC_ALL="ja_JP.UTF-8" TZ="Asia/Tokyo"

image.png

FROM mysql は先ほど使っていた mysql のイメージを元にするという意味です。
RUN...の行は世界で使われている言語設定をインストールしています。その中に日本語も含まれています。
ENV...の行は言語設定(ロケール)を日本、タイムゾーンを東京に設定しています。

ファイル名は Dockerfile と入力してファイルの種類は「すべてのファイル」としてユーザフォルダに保存します。
image.png

Docker ファイルから Docker イメージを作成

コマンドプロンプトを開いて dir コマンドで Dockerfile があることを確認します。
image.png

以下のコマンドを実行して、Dockerfile から Docker イメージを作成します。
※もともと用意されているイメージ(mysql)に追加でインストールや設定を行ったもので新しいイメージを作ることができます。

Dockerイメージ作成
docker build -t mysqlj .

-t mysqlj で作成するイメージに名前を付けることができます。(ここでは mysqlj という名前を付けています。)

Dockerイメージ作成結果
C:\Users\nakaz>docker build -t mysqlj .
[+] Building 0.2s (6/6) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.1s
 => => transferring dockerfile: 306B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/mysql:latest                                                    0.0s
 => [1/2] FROM docker.io/library/mysql                                                                             0.0s
 => CACHED [2/2] RUN apt-get update && apt-get install -y locales-all                                              0.0s
 => exporting to image                                                                                             0.0s
 => => exporting layers                                                                                            0.0s
 => => writing image sha256:9708290e1f00c169ec210361aa1dcebe60744b694b8eb3325bf7cf862e8af7b5                       0.0s
 => => naming to docker.io/library/mysqlj                                                                          0.0s

作成したイメージを使ってコンテナを起動

作成したイメージからコンテナを起動します。

日本語対応MySQLサーバ起動
docker run --name=mysqlj1 -e=MYSQL_ROOT_PASSWORD="password" mysqlj mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

※--name=mysqlj1 でコンテナに名前を付けることができます。ここでは mysqlj1 という名前を付けています。
※mysqld 以降は MySQL で日本語を扱う設定にするためにつけています。

日本語動作確認

日本語が入力できるか確認してみます。

MySQLクライアント起動
docker exec -it mysqlj1 mysql -uroot -p

※コンテナIDの代わりにコンテナ起動時につけた名前を指定することができます。
※bash の部分を mysql にすることでいきなりクライアントを起動することができます。

日本語が扱える設定かどうか確認します。

文字コード設定確認

文字コード設定確認
show variables like '%chara%';
文字コード設定確認結果
mysql> show variables like '%chara%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8                           |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.02 sec)

ちなみに、最初の日本語が扱えない時の設定は以下のようになっていました。

mysql> show variables like '%chara%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | latin1                         |
| character_set_connection | latin1                         |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | latin1                         |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8                           |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.03 sec)

※latin1 になっていると日本語が文字化けしたりします。

日本語レコード作成

データベース一覧を確認します。

データベース一覧確認結果
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

さっき作った testdb がないことがわかります。
これは、コンテナごとに別々のデータ領域が作られるためです。
コンテナは docker run を実行するごとに作成され、 docker ps -a で一覧を確認できます。

先ほどと同じように、データベースとテーブルを作って日本語のレコードを挿入してみます。

データベース作成結果
mysql> create database testdb;
Query OK, 1 row affected (0.04 sec)
データベース切り替え結果
mysql> use testdb;
Database changed
テーブル作成結果
mysql> create table test_users(id int auto_increment primary key, name varchar(10), birthday date);
Query OK, 0 rows affected (0.04 sec)

日本語でレコードを挿入してみます。

レコード挿入
insert into test_users(name,birthday) values('山田太郎', '1990-01-02');
レコード挿入結果
mysql> insert into test_users(name,birthday) values('山田太郎', '1990-01-02');
Query OK, 1 row affected (0.04 sec)

日本語レコード確認

挿入したデータを確認してみます。

テーブル内容確認
select * from test_users;
テーブル内容確認結果
mysql> select * from test_users;
+----+--------------+------------+
| id | name         | birthday   |
+----+--------------+------------+
|  1 | 山田太郎     | 1990-01-02 |
+----+--------------+------------+
1 row in set (0.00 sec)

正しく表示されました。

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

負荷試験でlocustを使ってみる

locustについて

試してみる

前提

  • Docker、docker-composeがインストール済み

環境構築

locustのコンテナと、試しに負荷をかけてみるコンテナを作ります。

docker-compose.yml
version: '3.7'

services:
  master:
    image: locustio/locust
    ports:
      - "8089:8089"
    volumes:
      - ./:/mnt/locust
    # 負荷をかける環境のドメインを指定する
    command: -f /mnt/locust/main.py -H http://test_web:80

  # お試し用の負荷をかけられるコンテナ
  test_web:
    image: httpd
    tty: true

テストシナリオ

テストシナリオを作ります。

main.py
from locust import HttpUser, task, constant_pacing

class StressTestUser(HttpUser):
    wait_time = constant_pacing(1)

    @task
    def top_page_access(self):
        # 負荷をかける対象のパスを指定する
        self.client.get("/")

ユーザクラスを作ります。ユーザクラスは1人のユーザーが行う行動をwait_timeやtaskなどで定義していきます。
https://docs.locust.io/en/stable/writing-a-locustfile.html#wait-time-attribute

起動

コンテナを起動します。

docker-compose up

実行

  1. http://localhost:8089 をブラウザで開く
  2. Number of total users to simulate に同時アクセスを行う最大ユーザ数を入力する
  3. Spawn rate にユーザ数の増加速度を入力する
  4. Start swarming をクリックする 1.png

テスト状況

テストを実行すると状況が確認できます。
2.png
3.png

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

コンテナの検証環境をためしてみる~ ネコンテナ3回

コンテナの検証環境

コンテナを勉強していくには検証環境が必要である。今回はローカルマシンでつかうコンテナ検証環境を比較してみる。

コンテナの検証環境

  1. docker
  2. minikube
  3. crc (code ready container)

難しいので今回は確実に覚えなくてもいいが、コンテナオーケストレーションはAIO環境という1台で構成される環境であり、外部から参照するのが難しい環境のため、商用リリース用で考えるものではない。

比較内容

種類 何の検証環境 windows環境 windows pro環境 mac linux 最低必要SPEC
docker docker 仮想環境を使えば導入可能 導入可能 導入可能 導入可能 考慮必要なし
minikube kubernetes 導入可能 導入可能 導入可能 導入可能 cpu:2,memory:2GB,20GB disk
CRC Openshift4 導入不可能 導入可能 導入可能 導入可能 cpu:4v,memory:8GB,35GB disk

※windows home では昔はdockerインストールできなかったが、202102時点では対応されている情報アリ
※最低必要スペックのためのセルアプリケーションのリソースは追加で必要。

1. docker

いろんな書籍で聞くdockerです。RHEL(centos)環境ではdocker.systemで動かすものです。クライアントもdocker、サービスもdockerなので初めての人だと言葉が混在してしまいます。
デファクトスタンダードではありますが最近下火になっているdockerです。2022年を生き延びるのは難しいかもしれません。
docker -> kubernetes には移行はありますが、逆はないに等しいです。

2.minikube

商用利用したり、コンテナ開発の優位性を生かすためのオーケストレーションのkubernetesの検証環境がminikubeです。kubernetesは管理ツールの役目が強いため、いきなり商用を使うと外部設定の箇所で躓いてしまします。
少し前までは最初からyamlファイルを書かないとアプリケーションのテストもできなかったため、最初の『Hello,world』までに時間がかかって嫌われてきました。

kubectl create 'kind'

kindはdeployment(runみたいなもの)やservice(ネットワーク紐づけ)等を使うと、簡単に最初のデプロイができます。

crc (code ready container)  1押し(コンテナのメリットを考えるなら)

kubernetesにコンテナの有用機能をたっぷり付けたOpenshiftのローカル検証用、優秀なGUIがついているもの。
商用運用はredhatのわかりにくく高い料金体系を考えないといけませんが、コンテナ初期学習において一番わかりやすいです。

ただ動作環境がかなり縛られているため、最初の初期構築で悩んでしまいます。
※低スペックPCで試してはいけない。
※現バージョンはvirtualBOXでは動きません。

  • crcを使ったデプロイ例(S2Iデプロイ)
    1. 下記のようなカタログから自分の環境を選ぶ
    2. githubにソースをアップロード
    3. GUIの手順に従ってデプロイ

crcカタログ画像.png

以上.png

CRCはデプロイのみ考えると、こんなに簡単なんです。
基盤の設計はアーキテクトに詳しい人に任せてコンテナ開発をCRCを使って遊んでください。
商用利用は商用利用の時に考えればいい、コンテナのメリットを考えるなら一押し

最後に

コンテナの開発環境も各種クラウドマネージドサービスでたくさん出ています。そちらを使って最初からやる方法もありです。どのサービスでもいいので使いやすいのを使っていただければと思います。今回はあくまで費用の掛からないローカル環境での構築3つを上げさせていただきました。

コンテナ開発は一度きりの開発ではないため、初期の学習コストが低いからdockerで勉強したが、最終的にk8s環境の勉強をしないといけないという無駄勉強が発生することがあります。そうならないように注意してください。

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

LaradockでDuskやろうとしたらちょっと手間が必要だった件

そもそもの話

最近、Laradock触ってないなぁ、と思ったので、試しにDuskを動かそうと思ったことから始まりました。
環境はすでに作成しているものを使っています。

やってみたいこと

  • Laravelをサブディレクトリとして構築
  • Duskのテストが通る

とりあえずの前提条件

  • Webサーバのrootとしては、 /var/www/public を指定
  • Laravelのrootは /var/www/public/lara1 、URLも http://localhost/lara1 でアクセスできるようにする
  • 他の設定は標準(Seleniumアクセスのポートは4444で、など)

Laradock起動コマンド

Webサーバはnginx、データベースはmysqlを使います。
Chromeはseleniumコンテナに入っているので同時に起動します。
あとは適当に

docker-compose up workspace nginx mysql selenium

Dusk導入まで

やり方は2つの参考記事をもとに対応。

参考1: https://qiita.com/amymd/items/0a5f2705e29972d0d22e
参考2: https://qiita.com/hrkbyc/items/755e9799c205c092c323

コマンドは以下の順番で処理していきます。

cd /var/www/public
composer create-project laravel/laravel lara1 --prefer-dist
cd lara1
composer install
composer require --dev dusk
composer update # 念の為
npm install
npm run dev
php artisan dusk:install

キモは、

AppServiceProvider.phpの修正

Duskをプロバイダに登録するコードを追加します。

AppServerProvicer.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Dusk\DuskServiceProvider; // Duskの参照を追加

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // Duskのプロバイダを登録
        if ($this->app->environment('local', 'testing')) {
            $this->app->register(DuskServiceProvider::class);
        }
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

DuskTestCase.php の修正

baseUrlを所定のURLに、driverをSeleniumのサーバーに変更します。

DuskTestCase.php
<?php

namespace Tests;

use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;

abstract class DuskTestCase extends BaseTestCase
{
    use CreatesApplication;

    // baseUrlは別サーバのサブディレクトリになるため明記
    protected function baseUrl()
    {
        return 'http://nginx/lara1';
    }

    /**
     * Prepare for Dusk test execution.
     *
     * @beforeClass
     * @return void
     */
    public static function prepare()
    {
        if (! static::runningInSail()) {
            static::startChromeDriver();
        }
    }

    /**
     * Create the RemoteWebDriver instance.
     *
     * @return \Facebook\WebDriver\Remote\RemoteWebDriver
     */
    // seleniumコンテナのChromeを使用する
    protected function driver()
    {
        return RemoteWebDriver::create(
            'http://selenium:4444/wd/hub',
            DesiredCapabilities::chrome()
        );
    }

    /**
     * Determine whether the Dusk command has disabled headless mode.
     *
     * @return bool
     */
    protected function hasHeadlessDisabled()
    {
        return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
               isset($_ENV['DUSK_HEADLESS_DISABLED']);
    }
}

さあテスト開始…ところが!?

php artisan dusk

ところが、エラーを出してしまうわけです。
Duskはちゃんと動いているようですが、どうやらLaravelのアプリがまともに動いていないと予想しました。
そこで、参考記事を元にnginxの設定をいじることにしました。

参考:https://qiita.com/saba1024/items/2d50fb8284d699924360

対策とってみた

default.confの設定変更

vi /etc/nginx/sites-available/default.conf

先の記事で参照していたnginxの設定ではサブディレクトリを考慮していなかったので、色々設定を変えました。

最終的にはこういう形になりました。

特にハマったのはSCRIPT_FILENAME。
index.phpの場所が変わっているのにすぐに気づきませんでした…。
パスの指定を現在の配置に合わせました。

fastcgi_param SCRIPT_FILENAME $document_root/public/index.php;

あと、fastcgi_passの指定。
もともとあったLaravel向けの設定を読んでみて、php-upstreamコンテナを指定すれば良いことがわかりました。

fastcgi_pass php-upstream;

あとは、locationやtry_filesの設定に注意して出来上がりました。

default.conf
    location ~ ^/lara1((/)?(.+))?$ {
        root /var/www/public/lara1;

        try_files $1 /lara1/index.php?$query_string;

        location ~ ^/lara1/index.php$ {
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root/public/index.php;
            fastcgi_param PATHINFO $fastcgi_path_info;
            fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_pass php-upstream;
            fastcgi_index index.php;
        }
    }

そして、nginxの再起動…エラーなし。

nginx -s reload

これでLaravelが動くだろう…世の中そんなに甘くありませんでした。

書き込み権限の変更

…Dockerのコマンドラインではrootなのに、nginx上ではnginxユーザで動かしているのをすっかり忘れていました。
storageディレクトリに書き込み権限を付記します。

cd /var/www/public/lara1
chmod -R a+w storage

さあリベンジ!

ブラウザでは無事Laravelのアプリが表示されました。
続いてDuskでは…

root@6580d8cd10f5:/var/www/public/lara1# php artisan dusk
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:10.353, Memory: 20.00 MB

OK (1 test, 1 assertion)

動いたー! やったー!

まとめ

過去の知見となんとか駆使すればなんとかなるということです、はい。

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

Flutter Webの単体テストはdart:htmlライブラリを含まないように作る【CI構築】

Flutter Webで動画プレイヤーアプリを作成したのですが、PUSHする毎に単体テストを動かすCIを構築するときに、Riverpodの使い方でひと工夫必要だったので記事にしました。

dart:htmlライブラリを含むと単体テストが難しい

アプリの概要

スプラトゥーン2のプレイヤー向けのシーン自動頭出し機能付きの動画プレイヤーアプリです。自動検出された特定シーンの時刻ボタンを押すと、その場所に飛びます。

※ 左上の動画は任天堂株式会社のスプラトゥーン2からの引用です。

使ってみたい方はこちら

単体テストの例

秒数をコロン区切りの時刻テキストに変換するutil系メソッドの単体テストがあります。
例: 192.0 → 3:12

実装

util.dart
import 'package:sprintf/sprintf.dart';
/// 秒数を時間テキストにする
String toTimeString(double seconds) {
  int hour = seconds.toInt() ~/ 3600;
  int minute = (seconds.toInt() - hour * 3600) ~/ 60;
  int second = seconds.toInt() % 60;
  if (hour >= 1) {
    return sprintf("%d:%02d:%02d", [hour, minute, second]);
  } else {
    return sprintf("%d:%02d", [minute, second]);
  }
}

単体テスト

util_test.dart
void main() {
  test('test toTimeString', () async {
    expect(toTimeString(0.0), '0:00');
    expect(toTimeString(59.9), '0:59');
    expect(toTimeString(60.0), '1:00');
    expect(toTimeString(9 * 60 + 59.9), '9:59');
    expect(toTimeString(10 * 60.0), '10:00');
    expect(toTimeString(59 * 60.0 + 59.9), '59:59');
    expect(toTimeString(3600.0), '1:00:00');
    expect(toTimeString(3600 + 59 * 60.0 + 59.9), '1:59:59');
    expect(toTimeString(7200.0), '2:00:00');
  });
}

単体テストの実行

これだけならば、このコマンドで単体テストを実行することができます。

flutter test

しかし、実は同じファイルにHTMLのvideo要素を作成、取得するメソッドが同居していました。なので、dart:htmlライブラリがインポートされています。

util.dart
import 'dart:html';
// ↑ これが問題

import 'package:sprintf/sprintf.dart';

String toTimeString(double seconds) {
  int hour = seconds.toInt() ~/ 3600;
  int minute = (seconds.toInt() - hour * 3600) ~/ 60;
  int second = seconds.toInt() % 60;
  if (hour >= 1) {
    return sprintf("%d:%02d:%02d", [hour, minute, second]);
  } else {
    return sprintf("%d:%02d", [minute, second]);
  }
}

/// 1つしか無いvideo要素
VideoElement _ikutVideoElement;

/// 1つしかないvideo要素を取得する
VideoElement getVideoElement(BuildContext context) {
  if (_ikutVideoElement == null) {
    final videoElement = VideoElement();
    videoElement.controls = true;
    // リスナ設定などがあるが省略
    _ikutVideoElement = videoElement;
  }
  return _ikutVideoElement;
}

その場合は flutter test コマンドでテスト実行するとエラーになってしまいます。

lib/util/util.dart:1:8: Error: Not found: 'dart:html'
import 'dart:html';

--platform chrome 引数を付けるとエラーになりません。

flutter test --platform chrome

dart:html ライブラリの動作にはGoogle Chromeが必要なようです。

Google Chrome入りFlutter Webビルド環境をDockerで作成する

CIでの動作を前提に、Dockerイメージを作成します。DockerHubで一番使われているFlutterのイメージcirrusci/flutterをベースに作成しました。

FROM cirrusci/flutter
# Setup Flutter Web
RUN flutter channel beta
RUN flutter upgrade
RUN flutter config --enable-web
# Install Google Chrome
RUN apt-get update
RUN apt-get install -y fonts-liberation libgbm1 xdg-utils
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i google-chrome-stable_current_amd64.deb

docker-composeを使ってビルドして flutter doctor を実行します。

docker-compose.yml
flutter:
  build: .
  # プロジェクトディレクトリを読み書きできるようにする
  volumes:
    - /your_project_directory:/work
  working_dir: /work
docker-compose build
docker-compose run flutter flutter doctor

一見良さそうに見えます。

[✓] Flutter (Channel beta, 1.26.0-17.6.pre, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[✓] Chrome - develop for the web
[!] Android Studio (not installed)
[✓] Connected device (1 available)

Docker環境でGoogle Chromeを使用した単体テストを実行する(失敗)

docker-composeを使って実行しました。freezedを使っているので、そのためのビルドコマンドを含んでいます。

docker-compose run flutter sh -c "flutter pub get && flutter pub run build_runner build && flutter test --platform chrome"

しかし10分以上待たされてエラーになってしまいました。

Downloading Web SDK...

12:00 +0 -1: compiling /work/test/presenter/battle_list_provider_test.dart [E]                                               
  TimeoutException after 0:12:00.000000: Test timed out after 12 minutes.

原因はよく分かりませんでした。

そこでGoogle Chrome入りのDockerイメージを使って flutter test --platform chrome コマンドを実行する方針から、 dart:html ライブラリに依存しないようにアプリの設計とテストコードの作成を行う方針に変更しました。

単体テストにdart:htmlライブラリを含まないようにしてCIを作成

dart:htmlライブラリを含むメソッド等は別ファイルに移動する

  • toTimeStringメソッド(dart:html不使用、テスト対象)はutil.dartファイルに設置。
  • getVideoElementメソッド(dart:html使用、テスト対象ではない)はweb_util.dartファイルに設置。

CIの設定を行う

DockerファイルはGoogle Chromeを含めないで作成しました。

FROM cirrusci/flutter
RUN flutter channel beta
RUN flutter upgrade
RUN flutter config --enable-web

私は日頃CIにAWS CodeBuildを使っているので、その設定を行いました。DockerイメージはElastic Container RegistryにPUSHしてbuildspec.ymlはこのようにしました。

buildspec.yml
version: 0.2
phases:
  build:
    commands:
      - flutter pub get
      - flutter pub run build_runner build
      - flutter test

できあがり

スクリーンショット 2021-02-27 20.54.51.png

別のdart:htmlライブラリを含むProviderを使っているStateNotifierProviderの単体テストを作成する

このケースの場合、Riverpodの使い方に工夫が必要だったので、それを紹介します。

該当部分の仕様

該当アプリには設定画面があり、頭出しされたシーンの再生は何秒前から開始するかや、スキップするときの秒数をスライダーを左右にドラッグすることで設定出来ます。設定はブラウザローカルに保存されて、次回同じブラウザでアプリを開いたときは復帰されます。

スクリーンショット 2021-02-25 23.31.33.png

単体テスト指針

  • 設定画面を構築するためのStateNotiferをテストする。
  • StateNotifierは設定をブラウザローカルに保存する担当オブジェクトを持っている。それをモック化して呼び出され方を確認する。

アプリの実装

設定のモデルはこのようになっています。(freezedで作成)

ikut_config.dart
@freezed
abstract class IkutConfig with _$IkutConfig {
  /// 設定情報
  /// [skipTime] スキップで進んだり戻ったりする秒数
  /// [deathBeforeTime] やられたシーンはN秒前から見たい
  /// [endBeforeTime] 試合終了へのスキップはN秒前から見る
  /// [slowRatio] スロー再生のレシオ(分母)
  /// [fastRatio] 早送り再生のレシオ(分子)
  factory IkutConfig(int skipTime, int deathBeforeTime,
      int endBeforeTime, int slowRatio, int fastRatio) = _IkutConfig;
}

endBeforeTimeフィールドとfastRatioフィールドは固定値を入力して使っています。

それを使ってこのようにWidgetを構築しています。横にスライドして値を変更する部品にはSliderを使用しています。

config.dart
HookBuilder _buildConfigContent() {
  return HookBuilder(builder: (context) {
    // 現在設定値
    final config = useProvider(ikutConfigStateNotifierProvider.state);
    // このStateNotifierのメソッドを呼び出して設定値を更新する
    final configStateNotifier = useProvider(ikutConfigStateNotifierProvider);
    // 見た目の定義
    final nameTextStyle = TextStyle(fontSize: 14, color: IkutColors.blackText);
    final valueTextStyle = TextStyle(
        fontSize: 14, color: IkutColors.blackText, fontWeight: FontWeight.bold);
    final sliderWidth = 400.0;
    return Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 進むときと戻るときの秒数
          SizedBox(height: 16),
          Text(sprintf(Strings.configSkipMinutes, [config.skipTime]),
              style: nameTextStyle),
          Container(
            width: sliderWidth,
            child: Slider(
                activeColor: Colors.pinkAccent.shade400,
                inactiveColor: Colors.pinkAccent.shade100,
                value: config.skipTime.toDouble() /* 現在値設定 */,
                min: 1.0,
                max: 10.0,
                divisions: 9,
                label: config.skipTime.toString() + Strings.seconds,
                onChanged: (value) {
                  // スライダーが移動したときのイベント
                  configStateNotifier.setSkipTime(value.toInt());
                }),
          ),
          // あと2つの設定は省略
        ]);
  });
}

StateNotifierと、そのProviderはこのようになっています。

ikut_config_state_notifier_provider.dart
class IkutConfigStateNotifier extends StateNotifier<IkutConfig> {
  IkutConfigStateNotifier(this._dataStore, IkutConfig config)
      : super(config);

  /// Window.localStorageで設定値を読み書きする担当オブジェクト
  LocalStorageDataStore _dataStore;

  /// 進むときと戻るときの秒数を設定する
  void setSkipTime(int skipTime) {
    state = state.copyWith(skipTime: skipTime);
    _dataStore.setInt(Strings.configNameSkipTime, skipTime);
  }

  /// やられたシーンは何秒前から見るを設定する
  void setDeathBeforeTime(int deathBeforeTime) {
    state = state.copyWith(deathBeforeTime: deathBeforeTime);
    _dataStore.setInt(Strings.configNameDeathBeforeTime, deathBeforeTime);
  }

  /// スロー再生速度を設定
  void setSlowRatio(int slowRatio) {
    state = state.copyWith(slowRatio: slowRatio);
    _dataStore.setInt(Strings.configNameSlowRatio, slowRatio);
  }
}

// ignore: top_level_function_literal_block
final ikutConfigStateNotifierProvider = StateNotifierProvider((context) {
  // Window.localStorageで設定値を読み書きする担当オブジェクトを取得
  final dataStore = context.read(localStorageDataStoreProvider);
  // 現在設定値読込
  int skipTime = dataStore.getInt(Strings.configNameSkipTime, 5);
  int deathBeforeTime = dataStore.getInt(Strings.configNameDeathBeforeTime, 4);
  int slowRatio = dataStore.getInt(Strings.configNameSlowRatio, 2);
  // 設定更新用のStateNotiferを作成する
  return IkutConfigStateNotifier(dataStore,
      IkutConfig(skipTime, deathBeforeTime, 3, slowRatio, 2));
});

設定値はWindow.localStorageを使い、ブラウザローカルに保存しています。しかしそのためには問題の dart:html ライブラリのインポートが必要です。よって、インターフェースと何もしない実装を作り、それを提供するProviderを作成します。

local_storage_data_store_provider.dart
/// Window.localStorageからデータを出し入れする機能のインターフェース
abstract class LocalStorageDataStore {
  /// int型の値を取得する
  /// [name] 保存キー
  /// [defaultValue] 未設定の時の返却値
  int getInt(String name, int defaultValue);

  /// 設定を保存する
  /// [name] 設定名
  /// [intValue] 設定値
  void setInt(String name, int intValue);
}

/// 上記のなにもしない実装
class LocalStorageDataStoreNoop implements LocalStorageDataStore {
  @override
  int getInt(String name, int defaultValue) {
    return defaultValue;
  }

  @override
  void setInt(String name, int intValue) {}
}

/// 上記の何もしない実装を提供する
/// ここで本番実装を提供すると、単体テストでdart:htmlが取り込まれてしまう。
// ignore: unnecessary_cast
final localStorageDataStoreProvider =
    Provider((_) => LocalStorageDataStoreNoop() as LocalStorageDataStore);

こちらが dart:html ライブラリを使う、本番で使われる実装です。

local_storage_data_store_impl.dart
import 'dart:html';

import 'package:ikut/localDataStore/local_storage_data_store.dart';

/// Window.localStorageからデータを出し入れする機能の本番実装
class LocalStorageDataStoreImpl implements LocalStorageDataStore {
  /// int型の値を取得する
  /// [name] 保存キー
  /// [defaultValue] 未設定の時の返却値
  @override
  int getInt(String name, int defaultValue) {
    String value = window.localStorage[name];
    if (value == null) {
      return defaultValue;
    } else {
      return int.parse(value);
    }
  }

  /// 設定を保存する
  /// [name] 設定名
  /// [intValue] 設定値
  @override
  void setInt(String name, int intValue) {
    window.localStorage[name] = intValue.toString();
  }
}

こうすることで、単体テストでは間接的にも問題の dart:html を取り込みません。

単体テストの実装

単体テストはこのようになります。モックオブジェクトの作成のために、mockitoを使用しました。

ikut_config_state_notifier_provider_test.dart
class MockLocalStorageDataStore extends Mock implements LocalStorageDataStore {}

void main() {
  test('test ikutConfigStateNotifier', () async {
    // Window.localStorageへの保存をモック化したオブジェクト
    final dataStore = MockLocalStorageDataStore();
    // 呼ばれるメソッドの実装を定義する
    when(dataStore.getInt(Strings.configNameSkipTime, 5)).thenReturn(5);
    when(dataStore.getInt(Strings.configNameDeathBeforeTime, 4)).thenReturn(4);
    when(dataStore.getInt(Strings.configNameSlowRatio, 2)).thenReturn(2);
    // LocalStorageDataStoreの実装をモックに差し替える
    final container = ProviderContainer(overrides: [
      localStorageDataStoreProvider.overrideWithValue(dataStore)
    ]);
    // StateNotifierを取得する
    final configStateNotifier =
        container.read(ikutConfigStateNotifierProvider);
    // 更新された状態を取得するラムダ
    final getState =
        () => container.read(ikutConfigStateNotifierProvider.state);
    // 初期値を確認
    expect(getState(), IkutConfig(5, 4, 3, 2, 2));
    // skipTime設定を更新する
    configStateNotifier.setSkipTime(3);
    // dataStoreが適切に呼び出されたことを確認
    verify(dataStore.setInt(Strings.configNameSkipTime, 3));
    // 状態も更新されたことを確認
    expect(getState(), IkutConfig(3, 4, 3, 2, 2));
    // 以下略    
  });
}

本番用実装のオーバーライド

localStorageDataStoreProviderがデフォルトで何もしない実装を返却するようになっているので、アプリを起動するときには、本番実装を返却するようにRiverpodのProviderScopeクラスのoverridesフィールドを設定します。

main.dart
void main() {
  runApp(ProviderScope(
      overrides: [
        /// 本番実装を返却するようにProviderの返却値を上書きする。
        localStorageDataStoreProvider.overrideWithValue(LocalStorageDataStoreImpl())],
      child: IkutApp()));
}

まとめ

dart:html ライブラリを含んだメソッド等の、CI環境での単体テストは上手くいきませんでしたが、Riverpodのoverridesを使い実装の差し替えを行うことで dart:html ライブラリを含まない実装を作り、単体テストを行うことができました。

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

dockerでexpressとmysqlをつなげたい

お品書き

1.はじめに
2.expresss-generatorでアプリの準備
3.Dockerfileとdocker-composeを準備
4.mysqlのコンテナを追加する
5.おわりに

1. はじめに

dockerを使ってexpressとMySQLをつなげようとしたときにハマってしまったのでメモしておきます。
dockerを使うと簡単に環境構築ができまた潰すのも簡単です。
つまり初学者こそdockerを使いいろんな環境を構築すべきなんです!
しかしdockerと聞くと難しそうな技術だなと尻込みしてしまいます。
そんな人のためにこの記事を書きました。
難しいことはわかりませんが、dockerを使って開発できるようになりましょう。

1-1 目標

dockerを使ってexpress(のコンテナ)とMySQL(のコンテナ)をつなげる
完成コードはこちら

1-2 そもそもdockerってなに

パソコン内に仮想環境を作る技術の1つで最近人気になっています。
dockerの良いところは気軽に様々な環境をつくることができることです。
コンテナという仮想環境を作ってそこでやりたい放題できる。
コンテナ内に好きなOSを入れてそれを好きなように操作するって感じです。
基本的は軽量のlinuxベースのもが多いのでlinuxコマンドを知っていると便利です。
そして要らなくなったらそのコンテナを捨てばOKってな感じです。

1-3 dockerがわかりにくい理由

とはいったものの、dockerを初めて触ったときに???がいっぱい浮かびました。
仮想環境やコンテナなどの概念などはわかるのですが、実際にどう使えばいいの??的な。
なぜならdockerを扱う上で似たようなコマンドが出てくるし、オプションも多いからです。
特にdockerとdocker-composeが初学者にはややこしい。
一通り調べた結果、とりあえずdocker-composeを使えばなんとかなりそうという結論になりました。(笑)
こちらの動画がかなり参考になりました。
docker-compose入門をチェックしてください。
難しいことは考えずに気軽に開発環境をつくっていきましょう。

1-4 今回使うdocker-composeのコマンドは2つだけ

  • docker-compose up 複数のコンテナを起動する
  • docker-compose down 起動しているコンテナを停止する とりあえずこれだけ知っていれば今回は大丈夫です。 その他のコマンドは後で覚えればOKです。

2. expresss-generatorでアプリの準備

まずはお好きな方法でディレクトリーを作ってください。
今回はdocker_express_mysqlという名前にしておきます。

terminal
mkdir docker_express_mysql

今回はexpress-generatorを使ってサクッとアプリを作っていきます。
アプリの名前はdockerExpressにします。
express-generatorの使い方はこちらを参考にしてください。
express-generatorを使いたい

terminal
#ディレクトリーへの移動
cd docker_express_mysql

#express-generatorを使ってdockerExpressというアプリを作る今回はテンプレートエンジンにejsをつかいます。
express --view=ejs dockerExpress

#dockerExpressに移動し立ち上げまで
cd dockerExpress
npm install
npm start

以下のように表示されれば成功です。
スクリーンショット 2021-02-27 10.20.53.png

次にdockerExpressの中身をいじっていきます。
することは
- nodemonのインストール 保存時に自動で更新してくれるスグレモノ
- mysql2のインストール mysqlに接続するのに必要
- sequelizeのインストール クエリーを簡単にするためのもの。ORMとよばれるもの

terminal
npm install nodemon mysql2 sequelize
package.json
"scripts": {
    // "start": "node ./bin/www" <-を下のように変える
    "start": "nodemon ./bin/www"
  }

これで準備が整いました。

3. Dockerfileとdocker-composeを準備

これはめっちゃ簡単です。というかvscodeの"Docker"というエクステンションを使えば簡単にできます。
詳しいことは省略しますが、Dockerfileはコンテナのもとになるものを作るときに必要なものです。
docker-composeは複数のコンテンなを立ち上げるときに必要になるものです。
この辺の書き方が、初心者には難しいと感じるので、是非本記事で感覚だけでも掴んでください。
とりあえず最低限必要な設定のみにしています。
拡張機能のDockerについて詳しくはこちら

3-1 vscode拡張機能"Docker"を導入する

"Docker"という拡張機能を使って作業します。もしvscodeを使っていな人や拡張機能を使いたくない人はあとに出てくるDockerfileとdocker-compose.ymlを自分で作成し、コードをコピーしてください。

導入方法は至って簡単です。
拡張機能から"Docker"と検索してインストールするだけです。
スクリーンショット 2021-02-27 10.42.34.png

3-2 拡張機能を使う

インストールできたら、拡張機能を使いましょう。
vscodeでcmd+shift+pを押してひたすらenterをおしてください。(詳しく知りたい人はこちら
Image from Gyazo
そうすると自動でDockerfileとdocker-composeのファイルをができます。
これらの自動生成されたファイルを多少変更していきます。

3-3 Dockerfileの編集

まずDockerfileから編集していきます。
いらない設定を削除していきます。

Dockerfile
FROM node:12.18-alpine
# ENV NODE_ENV=production ←を削除
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
# RUN npm install --production --silent && mv node_modules ../ ←を下のよう変更する
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

3-4 docker-compose.ymlを編集

続いてdocker-compose.ymlを編集していきます。
こちらは不要な設定を消しておきます。

docker-compose.yml
version: '3.4'

services:
  dockerexpress:
    image: dockerexpress
    build:
      context: dockerExpress
      dockerfile: ./Dockerfile
    #environment: ←を削除
      #NODE_ENV: production ←を削除
    ports:
      - 3000:3000 #接続するポート番号。お好きな番号でいけます。

これでdockerを使う準備が整いました。

3-5 コンテナを立ち上げてみる

試しにコンテナを動かして見ましょう。
注意点はdocker-compose.ymlがあるディレクトリーでdocker-compose upというコマンドを打つことです。

terminal
#docker-compose.ymlが入っているフォルダに移動してから
#コンテナを立ち上げるコマンド
docker-compose up

スクリーンショット 2021-02-27 11.41.09.png
問題なければhttp://localhost:3000/にアクセスできるはずです。
*もしdocker-compose upをしてエラーが出た場合はコードを確認してdocker-compose buildを行ってからdocker-compose upを行ってください。

ここまででdockerを使ってnodeのアプリをコンテナ内に立ち上げることができました。

4. mysqlのコンテナを追加

ここまで来たらあとはmysqlのコンテナを立ち上げるだけです。
基本的にすることはdocker-compose.ymlにmysqlのコンテナの設定を書くだけです。
今回は最小限の設定でコードを書いていきます。
dockerでコンテナをつくるときには1つのコンテナにすべての機能を入れるのではなくて、最小限の機能に分けて作るのがベストプラクティスらしいです。
なので今回はアプリ部分のコンテナ(expressで作ったもの)とDB部分のコンテナ(今から作るmyslqのもの)に分けて作っています。

4-1 docker-compose.ymlにmysqlのコンテナ設定を書く

docker-compose.ymlに次のように追記してください。
ポイントはDBのデータを永続化させるために第3の場所にデータの置き場を作ることです。
詳しくは別の記事で説明は別の記事でできれば。。

docker-compose.yml
version: '3.4'

services:
# ここから
  my_mysql:
    image: mysql #もととなるイメージ。ここではmysql
    environment: #最小限の記述にしています。他にも環境設定は可能です。
      MYSQL_ROOT_PASSWORD: root #rootパスワード。今回はrootにしました。
      MYSQL_DATABASE: my_mysql_db #作成するデータベース。今回はmy_mysql_dbにしました。
    ports:
      - '3300:3300' #接続するポート番号。お好きな番号でいけます。
    volumes:
      - my_volume:/var/lib/mysql     #第3の場所my_volumeをコンテナ内に同期するコード
# ここまで追記 下の部分も忘れずに追記してください。

  dockerexpress:
    image: dockerexpress
    build:
      context: dockerExpress
      dockerfile: ./Dockerfile
    ports:
      - 3000:3000

#第3の場所にデータを置いておく場所をつくる。こちらも忘れずに追記してください。
volumes: 
  my_volume:

これでmysqlのコンテナを起動する準備ができたので起動させてみましょう。

terminal
# docker-composeがあるディレクトリーで
docker-compose up

これでエラーがでなかれば起動しているはずです。

4-2 mysqlコンテナ内に入って操作してみよう

一応myslqのコンテナが起動しているか確認しましょう。
イメージとしては先程立ち上げたコンテナ内に入ってmysqlを使うというかたちです。
ここがちょっとややこしいです。とりあえずコマンドを打ちましょう。

別のterminalを開く
# my_mysqlコンテナ内に入るためのコマンド
docker-compose exec my_mysql /bin/bash
my_mysqlのコンテナ内
--プロンプトが変わったらコンテナ内に入れた証拠です。続いてmysqlを起動させていきます。
mysql -u root -p 
--docker-composeで設定したパスワードを入力

下のような感じになれば成功です。
スクリーンショット 2021-02-27 17.23.53.png

my_mysqlのコンテナ内
show databases;

スクリーンショット 2021-02-27 17.28.06.png
データベースが作られていることを確認!
*他にもdocker-composeでいろんな指定ができますが省略します。
あとはdockerexpressからmy_mysqlにデータを取りにいけるようにしましょう。

4-3 コンテナ間の接続を確認する

expressのコンテナとmysalのコンテナはそれぞれ別のコンテナとして立ち上げました。
なので今は2つのコンテナ立ち上がっているわけです。
それらのコンテナをつなげることに挑戦します。とはいってもとても簡単です。
host名はローカルではlocalhostになっていますが、これをdocker-compose.ymlで設定したDBの名前(今回はmy_mysql)にするだけです。
これを知らなくてかなり時間を無駄にしてしまいました。。。

mysql2を使ってクエリを書いても確認できますが、今回はsequelizeを使ってみます。
sequelizeの本家サイトはこちら

usersのページに遷移したときにdbにつながっているかをしらべてみます。
なのでroutes内のusers.jsを開き次のようにsequelizeの設定と確かめるためのコードを追記しましょう。

users.js
var express = require('express');
var router = express.Router();

// sequelizeの設定を追加
const { Sequelize } = require('sequelize');
// databaseやuser, passwordをdcoker-compose.ymlで設定したものを使う↓
const sequelize = new Sequelize('my_mysql_db', 'root', 'root', {
  host: 'my_mysql', // hostの名前をdocker-compose.ymlで設定したmy_mysqlに変更する
  dialect: 'mysql',
});

/* GET users listing. */
router.get('/', async (req, res, next) => {
  // 忘れずに上に"async"を追加する。
  // my_mysqlに接続されているかテスト
  try {
    await sequelize.authenticate();
    console.log('Connection has been established successfully.');
  } catch (error) {
    console.error('Unable to connect to the database:', error);
  }
  res.send('respond with a resource');
});

module.exports = router;

URLをhttp://localhost:3000/usersとして、usersページに遷移したときに以下のようなメッセージがターミナルに出たら成功です。

terminal
dockerexpress_1  | Executing (default): SELECT 1+1 AS result
dockerexpress_1  | Connection has been established successfully.
dockerexpress_1  | GET /users 304 45.706 ms - -

とりあえずこれでdockerを使ってexpressとmyqlのコンテナ同士をつなげることができました。
あとはアプリを作り込んだり、DBにデータをどんどん追加していくだけです!

5. おわりに

githubにコードを載せておきます。
初学者のため、不十分な理解やコードのミス等があるとおもいます。もしあれば優しく教えてくいただければ幸いです。

sequelizeの使い方に関しては後日別の記事を書きたいと思います。
もう少しい詳しいdockerの解説も後日別の記事を書きたいと思います。

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

マルチステージビルドしたらStreamlitが動かなくなった

Streamlitはpythonのアプリケーションフレームワークで、簡単にデータの可視化や機械学習モデルのアプリを作成できるて便利。

今回はStreamlitをDocker上で構築して動かしていたのですが、機械学習のDockerイメージはどうしてもイメージサイズがデカくなってしまうのでマルチステージビルドで軽くしちゃおうと思ったら動かなくなりました。というお話。

修正前

requirements.txt
streamlit
plotly
pandas
scikit-learn
FROM python:3.7
USER root

EXPOSE 8501

WORKDIR /streamlit-docker

COPY requirements.txt /streamlit-docker

RUN pip install -r requirements.txt

CMD streamlit run app.py

ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8

RUN mkdir -p /root/.streamlit
RUN bash -c 'echo -e "\
            [general]\n\
            email = \"\"\n\
            " > /root/.streamlit/credentials.toml'

RUN bash -c 'echo -e "\
            [server]\n\
            enableCORS = false\n\
            " > /root/.streamlit/config.toml'

この状態でもしっかり動くのですが、今回はdockerイメージサイズを減らすために変更しました。

マルチステージビルド

FROM python:3.7-buster as builder

COPY requirements.txt /streamlit-docker

RUN pip install -r requirements.txt

FROM python:3.7-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages
COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit

WORKDIR /streamlit-docker

USER root

EXPOSE 8501

CMD streamlit run app.py

ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8

RUN mkdir -p /root/.streamlit
RUN bash -c 'echo -e "\
            [general]\n\
            email = \"\"\n\
            " > /root/.streamlit/credentials.toml'

RUN bash -c 'echo -e "\
            [server]\n\
            enableCORS = false\n\
            " > /root/.streamlit/config.toml'

pythonのマルチステージビルドは以下の記事を参考にしました。
Pythonの機械学習用Docker imageのサイズ削減方法の紹介
仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編

この状態でコンテナを立ち上げるとエラーが出ます。

/bin/sh: 1: streamlit: not found

どこで引っかかってるかというと、Dockerfileのこの部分です。

CMD streamlit run app.py

要するにpathを通せてないよって言われてます。

解決法

streamlitの通すパスを見つけるためにマルチステージビルド前の動くコンテナを使って調べます。
whichコマンドでコマンドのフルパスを表示させます。

>> which streamlit
/usr/local/bin/streamlit

通すパスがわかったので、dockerfileを修正します。
1. ビルドステージから/usr/local/bin/streamlitをコピー
2. ENVコマンドでPATHを通す
これで動きました。

FROM python:3.7-buster as builder

WORKDIR /streamlit-docker

COPY requirements.txt /streamlit-docker

RUN pip install -r requirements.txt

FROM python:3.7-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages
# 追加
COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit
ENV PATH $PATH:/usr/local/bin/streamlit

WORKDIR /streamlit-docker

USER root

EXPOSE 8501

CMD streamlit run app.py

ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8

RUN mkdir -p /root/.streamlit
RUN bash -c 'echo -e "\
            [general]\n\
            email = \"\"\n\
            " > /root/.streamlit/credentials.toml'

RUN bash -c 'echo -e "\
            [server]\n\
            enableCORS = false\n\
            " > /root/.streamlit/config.toml'

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

マルチステージビルドした時のMeCabエラーを回避する

概要

MeCabを使ったマルチステージビルドで詰まったのでメモを残します。
今回はasariというパッケージを使いたかったためmecab-python3のバージョンが低いです。
今度時間があればバージョン上げても動くか検証してみます。

環境

python 3.7
mecab-python3 0.7

修正前

Dockerfile
FROM python:3.7-buster as builder

WORKDIR /work

RUN apt-get update \
    && apt-get install -y mecab mecab-utils libmecab-dev \
    && pip install -U pip

COPY requirements.txt /work

RUN pip install -r requirements.txt

FROM python:3.7-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages

WORKDIR /work

CMD python sample.py

sample.pyの中身はmecabを使用したファイルとします

sample.py
import MeCab

実行するとimportの際にエラーが出ます。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/MeCab.py", line 26, in <module>
    _MeCab = swig_import_helper()
  File "/usr/local/lib/python3.7/site-packages/MeCab.py", line 22, in swig_import_helper
    _mod = imp.load_module('_MeCab', fp, pathname, description)
  File "/usr/local/lib/python3.7/imp.py", line 242, in load_module
    return load_dynamic(name, filename, file)
  File "/usr/local/lib/python3.7/imp.py", line 342, in load_dynamic
    return _load(spec)
ImportError: libmecab.so.2: cannot open shared object file: No such file or directory

原因

エラー文にある通りlibmecab.so.2がないのが原因っぽいです。
一度マルチステージビルドしていない正しく動いたコンテナでどこにlibmecab.so.2があるのか調べてみます

>> find / | grep libmecab
/usr/share/doc/mecab/libmecab.html
/usr/share/doc/libmecab-dev
/usr/share/doc/libmecab-dev/changelog.Debian.gz
/usr/share/doc/libmecab-dev/copyright
/usr/share/doc/libmecab2
/usr/share/doc/libmecab2/changelog.Debian.gz
/usr/share/doc/libmecab2/copyright
/usr/lib/x86_64-linux-gnu/libmecab.so.2.0.0
/usr/lib/x86_64-linux-gnu/libmecab.a
/usr/lib/x86_64-linux-gnu/libmecab.so.2
/usr/lib/x86_64-linux-gnu/libmecab.so
/var/lib/dpkg/info/libmecab-dev.md5sums
/var/lib/dpkg/info/libmecab-dev.list
/var/lib/dpkg/info/libmecab2:amd64.triggers
/var/lib/dpkg/info/libmecab2:amd64.conffiles
/var/lib/dpkg/info/libmecab2:amd64.md5sums
/var/lib/dpkg/info/libmecab2:amd64.list
/var/lib/dpkg/info/libmecab2:amd64.shlibs

色々出てきましたが、とりあえずlibmecab.so.2は/usr/lib/x86_64-linux-gnu/libmecab.so.2にあることがわかったので、これをbuild用のコンテナからコピーすれば行けそうです

修正後

Dockerfile
FROM python:3.7-buster as builder

WORKDIR /work

RUN apt-get update \
    && apt-get install -y mecab mecab-utils libmecab-dev \
    && pip install -U pip

COPY requirements.txt /work

RUN pip install -r requirements.txt

FROM python:3.7-slim-buster as runner

COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages
# 追加部分
COPY --from=builder /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu

WORKDIR /work

CMD python sample.py

これで動きました。

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