- 投稿日:2020-03-31T23:59:19+09:00
Docker
趣味でプログラミングを勉強しています。
軽量に動作して高速に環境を構築できる「Docker」(ドッカー)を今回記事にしてみました。Dockerとは?
コンテナ技術を使ったアプリケーションの実行、運用をするためのプラットフォーム
コンテナとは?
ホスト上に論理的な区画(コンテナ)を作り、アプリケーションを動作させるのに必要なライブラリやアプリケーションを1つにまとめ、あたかも個別のサーバのように使うことができるようにしたもの
→ホストOSのリソースを論理的に分離して複数コンテナで共有して使うので、間接的にかかるコストが少なく軽量で高速に動作するのが特徴コンテナの歴史
FreeBSD jail
https://ja.wikipedia.org/wiki/FreeBSD_jail
Solaris Containers
https://www.oracle.com/technetwork/jp/server-storage/solaris10/containers-169727-ja.htmlDockerの機能
Dockerイメージ(コンテナの元)を作る機能
Dockerイメージ アプリケーションの実行に必要なファイル群が格納されたディレクトリDockerイメージを共有する機能
・Dockerレジストリ
イメージを持つリポジトリ(イメージの集まり)を預かるサービス
・DockerHub https://hub.docker.com/
Docker、コンポーネントで動くリソースを集めた場所Dockerコンテナを動かす機能
Dockerのエディションは2つ
コミュニティ版(CE; Community Edition)
エンタープライズ版(EE; Enterprise Edition)Dockerが動く仕組み
・コンテナという独立環境を作り、コンテナを区画化してアプリケーションの実行環境を作る
・Linuxカーネルのnamespaceという機能を使いコンテナを仮想的に隔離する
・Dockerコンテナと外部ネットワークが通信を行うときは、NAPTの機能を使って通信するLinux namespaces
https://en.wikipedia.org/wiki/Linux_namespacesNAPT
1つのIPアドレスを複数のコンピューターで共有する技術で、IPアドレスとポート番号を変換する機能Dockerのインストールと動作確認
環境
Windows 10 Pro 64bit メモリ 16G
Docker for Windows v 19.03.51. Docker Desktop for Windowsをインストール
https://hub.docker.com/editions/community/docker-ce-desktop-windows/2. コマンド実行
docker container run amazonlinux:latest //bin/echo 'Introduction to Docker'
説明
docker container run
コンテナを作成/実行
amazonlinux:latest
もとになるDockerイメージ
//bin/echo 'Introduction to Docker'
コンテナ内で実行するコマンド1回目はダウンロードするので時間がかかるが、
2回目以降はダウンロードされたDockerイメージをもとにDockerコンテナを起動するので高速に動くDockerコマンド
Dockerバージョン
docker version
実行環境の詳細設定
docker system info
ディスクの利用状況
docker system df
Dockerを使ってWebサーバを構築
コマンド実行
docker container run --name webserver -d -p 8000:80 httpd
説明
--name webserver -d
「webserver」という名前のコンテナをバックグラウンドで起動
-p
コンテナのポートをホスト側に公開
8000:80
ホスト8000番ポートとコンテナ80番ポートをマッピングバックグラウンド
相対的に低い優先度で動作し、入力がごく少ないか全くなく、最小限の出力を行うプロセスhttpdサーバの状況確認
docker container ps
コンテナ詳細
docker container stats webserver
httpd停止
docker stop webserver
httpd起動
docker start webserver
今回はDockerのさわりを記事にさせていただきましたので、
次回、「Docker Compose」を使った複数コンテナの環境構築を記事にしたいと思います。
- 投稿日:2020-03-31T23:50:59+09:00
dockerでGemfile(gem追加)更新したら後、データベースが消えた、gemが反映しない
サーバーを再起動してますか????
#dockerではない通常開発の場合
dockerなしcontrol c でサーバー終了 rails sこれでgem反映します。
ということはdockerもサーバーを再起動しなければいけない。
dockerあり$ docker-compose restartこれで反映されます。
- 投稿日:2020-03-31T23:37:03+09:00
2020年はCOBOL元年なので1時間でHello Worldに挑戦
はじめに
2020年は色々あってCOBOLが世界を制します。
1時間でチュートリアルしてみました。実行環境
Dockerで動くものを用意しました。
リポジトリCOBOLのランタイムはこちらからダウンロードしたものをあらかじめgit管理しています。
実行コマンド
$ git clone https://github.com/glassmonkey/hello-cobol $ cd hello-cobol $ docker-compose up出力
$ hello worldソースコード
QiitaにCOBOLのシンタックスなくてわろた。プログラムちょい替え(4)COBOLを40年ぶりにうごかしてみた:dockerでcobol, docker(81)からソースコードお借りしました。
000010 IDENTIFICATION DIVISION. 000020 PROGRAM-ID. SAMPLE-01. 000030* 000040 ENVIRONMENT DIVISION. 000050* 000060 DATA DIVISION. 000070* 000080 PROCEDURE DIVISION. 000090 MAIN. 000100 DISPLAY "Hello world!" UPON CONSOLE. 000110 STOP RUN.プログラムの構成
COBOLプラグラム入門様を元に参考にしました。
COBOLは部と段落から構成させるプログラム言語とのこと。部は段落から構成されており、それぞれ部には必要な段落を記載するとのこと。見出し部(IDENTIFICATION DIVISION)
プログラムのメタ情報を記述するところ。今回は内部的にSample-01と名前をつけている。
作成者などもここに書く模様。環境部(ENVIRONMENT DIVISION)
実行マシン名(SOURCE-COMPUTER)や環境変数の受け渡し情報やプログラムから読み書きするファイルの名前や種類を書くところ。今回は空。
データ部(DATA DIVISION)
変数や外部IOに関して記述するところ。今回は空。
手続き部(PROCEDURE DIVISION)
いわゆるロジックを書くところ。今回はこのような形でHello worldを標準出力することを記載。
000090 MAIN. 000100 DISPLAY "Hello world!" UPON CONSOLE. 000110 STOP RUN.感想
友人が仕事でCOBOLをやり始めたみたいだったので、エイプリルフールネタで入門してみました。学生時代にポケコンでBasic書いてた時代を思い出して、懐かしい気持ちになりました。
なんだかんだコンテナ化できれば、過去の資産は活かせそうかなと思いましたが、やはり使うのはやめましょう。人類には早すぎます。
適当にプログラミング関係のことやグラブル関係のことをつぶやいてたりします。もしよければ@glassmonekeyをフォローしてくれるとうれしいです。
参考
下記をお借りしました。
* ランタイム
https://sourceforge.net/projects/open-cobol/files/gnucobol/3.0/
- 投稿日:2020-03-31T22:38:56+09:00
Growiの定期バックアップ - AWSを利用しない場合
はじめに
どうも!生産技術部のエンジニアです。growiの定期バックアップ方法を検討しましたので、参考までにどうぞ。
環境
定期バックアップ
バックアップ用のスクリプトを作成
バックアップは、mongodumpコマンドを利用し、アーカイブファイルを作成します。作成するアーカイブファイルのファイル名には、日時とgrowiのバージョン番号を付けました。バックアップファイルからの復元には、mongorestoreコマンドを利用します。バックアップは一週間経過したファイルから削除します。
※growi/docker-compose.ymlのFILE_UPLOADをlocalに設定している場合は、別途バックアップが必要だと思われます。mongodbにしておけば不要かどうかも謎です。growi_backup.sh#!/bin/sh BKDIR=/srv/growi/data/backups # Creating backup archive docker exec -d growi_mongo_1 mongodump --archive=mongodb.archive docker cp growi_mongo_1:mongodb.archive $BKDIR/$(date --date '1 day ago' "+%s_%Y_%m_%d_3.7.1_mongodb.archive") # Create config backup archive # Delete old backups find $BKDIR -mtime +6 | xargs rm -rfcrontabでスクリプトを定期実行
crontabにバックアップを取りたい時刻を設定し、スクリプトを実行する。
# crontab -l 0 2 * * * /root/Docker/growi/growi_backup.sh CRON=1ご参考
- 投稿日:2020-03-31T22:31:02+09:00
プロキシ環境下でDocker Composeを用いてGrowiを立ち上げる
はじめに
どうも!生産技術部のエンジニアです。マニュアルドキュメント類を整理するため、growiを利用させていただきました。以下に、立ち上げ手順をまとめましたので参考にどうぞ。
環境
前提条件
git、docker、docker-composeの導入が実施済みであること。
立ち上げ手順
1.growi-docker-composeをGitHubから取得
git clone https://github.com/weseek/growi-docker-compose.git growi2.プロキシの設定
elastic-searchのプロキシ設定
elastic-searchのプラグインを導入する際に、プロキシの設定が必要な様です。
growi/elasticsearch/DockerfileFROM docker.elastic.co/elasticsearch/elasticsearch:6.6.1 LABEL maintainer Yuki Takei <yuki@weseek.co.jp> # 下記2行にご自身の環境に合わせたproxyの設定を追記してください。 RUN ES_JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8888 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8888" bin/elasticsearch-plugin install analysis-kuromoji RUN ES_JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8888 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8888" bin/elasticsearch-plugin install analysis-icugrowiのプロキシ設定
apkを利用する際に設定が必要だと思われますが、確認はしていません。
growi/DockerfileFROM weseek/growi:3 LABEL maintainer Yuki Takei <yuki@weseek.co.jp> # 下記2行を追記し、ご自身の環境に合わせたプロキシに書き換えてください。 ENV http_proxy 'proxy.example.com:8888' ENV https_proxy 'proxy.example.com:8888' ENV APP_DIR /opt/growi # install dockerize ENV DOCKERIZE_VERSION v0.6.1 USER root RUN apk add --no-cache --virtual .dl-deps curl \ && curl -sL https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ | tar -xz -C /usr/local/bin \ && apk del .dl-deps WORKDIR ${APP_DIR}(参考)crowiの場合
crowiを導入する場合は、上記の設定に加えて、npmのパッケージ導入する際のプロキシ設定が必要です。
crowi/DockerfileRUN npm -g config set proxy $http_proxy RUN npm -g config set https-proxy $https_proxy3.ポートおよびファイアウォールの設定
外部からアクセスできるように、
127.0.0.1:3000:3000
を3000:3000
に書き換える。growi/docker-compose.ymlservices: app: build: context: . dockerfile: ./Dockerfile ports: - 3000:3000 # localhost only by default : :ファイアウォールの3000番ポートを開ける。
$ firewall-cmd --add-port=3000/tcp --zone=public --permanent $ firewall-cmd --reload success4.各種設定
- FILE_UPLOAD=local/mongodb
growiページ上の添付ファイルを保存する先を設定。localは、growiのコンテナ内のローカルフォルダに保存、mongodbは、mongodbのDB上に保存されます。- MATHJAX=1
数式を美しく表現できる。growi/docker-compose.ymlversion: '3' services: app: build: context: . dockerfile: ./Dockerfile ports: - 3000:3000 # localhost only by default links: - mongo:mongo - elasticsearch:elasticsearch depends_on: - mongo - elasticsearch environment: - MONGO_URI=mongodb://mongo:27017/growi - ELASTICSEARCH_URI=http://elasticsearch:9200/growi - PASSWORD_SEED=changeme - FILE_UPLOAD=mongodb # activate this line if you use MongoDB GridFS rather than AWS # - FILE_UPLOAD=local # activate this line if you use local storage of server rather than AWS - MATHJAX=1 # activate this line if you want to use MathJax # - PLANTUML_URI=http:// # activate this line and specify if you use your own PlantUML server rather than public plantuml.com # - HACKMD_URI=http:// # activate this line and specify HackMD server URI which can be accessed from GROWI client browsers # - HACKMD_URI_FOR_SERVER=http://hackmd:3000 # activate this line and specify HackMD server URI which can be accessed from this server container # - FORCE_WIKI_MODE='public' # activate this line to force wiki public mode # - FORCE_WIKI_MODE='private' # activate this line to force wiki private mode command: "dockerize -wait tcp://mongo:27017 -wait tcp://elasticsearch:9200 -timeout 60s npm run server:prod" restart: unless-stopped volumes: - growi_data:/data : :5.起動
最後にdocker-composeで起動して終了です。起動後、しばらくしてから
http://<サーバのIP>:3000
にアクセスしサインインの画面が表示されれば完了です。# Dockerの起動およびリロード $ docker-compose up -d最後に
無事に立ち上がりました。バックアップ方法についても検討していきます。
ご参考
- 投稿日:2020-03-31T20:34:09+09:00
Dockerコンテナ内でGitコマンドを実行しようとすると「Operation not permitted」というエラーが出るときの対応
- 投稿日:2020-03-31T14:54:02+09:00
Docker Desktop 裏技プロキシ設定
社内開発だとプロキシ環境下でDocker Desktopを利用することも多いと思います。
プロキシ設定で手こずったので、裏技かどうかわかりませんが
色んなところで応用が利くんじゃないかと思い、メモ代わりに投稿します。http://host.docker.internal:3128設定画面とは別に config.json ファイルを編集する。
「C:¥Users¥ユーザー名¥.docker¥config.json」
.dockerフォルダが生成されていない、または config.jsonファイルが生成されていない場合は自分で作成すればいい。
編集内容
config.json{ "proxies": { "default": { "httpProxy":"http://host.docker.internal:3128", "httpsProxy":"http://host.docker.internal:3128", "noProxy":"127.0.0.*" } } }編集後は、Docker Desktopを再起動してプロキシの設定を反映。(完了)
CNTLMと併用して利用しています。
- 投稿日:2020-03-31T13:50:51+09:00
Mac上で動作するVirtualBoxのUbuntu上でGccクロスコンパイル環境をDockerで構築
Mac上で動作するVirtualBoxのUbuntu上でGccクロスコンパイル環境をDockerで構築
Gccのクロスコンパイル環境をMacOSで起動したVirtualbox上のUbuntuのDockerで構築しました。
なんでこんな複雑な構成にしているかというと、環境/作業によるかと思いますが、体感的にはMacOSのDockerより早い気がしているため。
構成
レイヤー 動作環境 OS ↓ Docker Ubuntu18.04(ここに構築) ↓ VirtualBox Ubuntu18.04 ↓ Macboo MacOS Docker環境のインストール
リンク先を参照ください。
MacOS の VirtualBox Ubntu上にDocker環境を構築VirtualBox上のUbuntu18.4環境
tagucchan@tagvbbuntugcc:~$ uname -a Linux tagvbbuntugcc 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux tagucchan@tagvbbuntugcc:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 18.04.4 LTS Release: 18.04 Codename: bionic tagucchan@tagvbbuntugcc:~$ sudo docker version Client: Docker Engine - Community Version: 19.03.8 API version: 1.40 Go version: go1.12.17 Git commit: afacb8b7f0 Built: Wed Mar 11 01:25:46 2020 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.8 API version: 1.40 (minimum version 1.12) Go version: go1.12.17 Git commit: afacb8b7f0 Built: Wed Mar 11 01:24:19 2020 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.2.13 GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0 GitCommit: fec3683Ubuntu18.04イメージをプル
tagucchan@tagvbbuntugcc:~$ sudo docker pull ubuntu:18.04 18.04: Pulling from library/ubuntu 5bed26d33875: Pull complete f11b29a9c730: Pull complete 930bda195c84: Pull complete 78bf9a5ad49e: Pull complete Digest: sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392b7546b43a051853a341d Status: Downloaded newer image for ubuntu:18.04 docker.io/library/ubuntu:18.04tagucchan@tagvbbuntugcc:~$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 18.04 4e5021d210f6 7 days ago 64.2MB hello-world latest fce289e99eb9 15 months ago 1.84kBDockerでubuntu 18.04のコンテナを作成
tagucchan@tagvbbuntugcc:~$ sudo docker run -it -d --name gcc-ubuntu ubuntu:18.04 af48c1a975a26096ca8b0df01bd33f1c083604916f23c562607635f03548deb0 tagucchan@tagvbbuntugcc:~$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES af48c1a975a2 ubuntu:18.04 "/bin/bash" 2 minutes ago Up 2 minutes gcc-ubuntu"/bin/bash”を起動
tagucchan@tagvbbuntugcc:~$ sudo docker exec -it gcc-ubuntu /bin/bash root@af48c1a975a2:/#Dockerの基本的な使い方
docker ps -a docker start <CONTAINER IDまたはNAME> # コンテナの起動 docker stop <CONTAINER IDまたはNAME> # コンテナの停止クロスコンパイラのビルドに必要なパッケージをインストール
root@af48c1a975a2:/# apt-get update root@af48c1a975a2:/# apt-get upgrade root@af48c1a975a2:/# apt-get install build-essential texinfo gawk root@af48c1a975a2:/# apt-get install vim-tiny less wget rsyncbisonをインストール
root@af48c1a975a2:/# mkdir ~/dev root@af48c1a975a2:/# mkdir ~/dev/bison root@af48c1a975a2:/# cd ~/dev/bisonGUN M4をインストール
root@af48c1a975a2:/# wget https://ftp.gnu.org/gnu/m4/m4-1.4.18.tar.gz root@af48c1a975a2:/# tar xf m4-1.4.18.tar.gz root@af48c1a975a2:/# cd m4-1.4.18 root@af48c1a975a2:/# ./configure --prefix=/usr/local root@af48c1a975a2:/# make root@af48c1a975a2:/# make install root@af48c1a975a2:/# cd ..bisonをインストール
root@af48c1a975a2:/# wget https://ftp.gnu.org/gnu/bison/bison-3.5.tar.gz root@af48c1a975a2:/# tar xf bison-3.5.tar.gz root@af48c1a975a2:/# cd bison-3.5 root@af48c1a975a2:/# ./configure --prefix=/usr/local root@af48c1a975a2:/# make root@af48c1a975a2:/# make installVirtualBoxのUbuntu内Dockerの情報
root@af48c1a975a2:/# gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. root@af48c1a975a2:/# uname -a Linux af48c1a975a2 5.3.0-42-generic #34~18.04.1-Ubuntu SMP Fri Feb 28 13:42:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux root@af48c1a975a2:/# lsb_release -a bash: lsb_release: command not found root@af48c1a975a2:/# apt-get install lsb-release root@af48c1a975a2:/# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 18.04.4 LTS Release: 18.04 Codename: bionic root@af48c1a975a2:/# /lib/x86_64-linux-gnu/libc libc-2.27.so libc.so.6 root@af48c1a975a2:/# /lib/x86_64-linux-gnu/libc.so.6 GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.物理 CPU、CPU コア、および論理 CPU の数を確認
root@af48c1a975a2:~/dev/xgcc# grep physical.id /proc/cpuinfo | sort -u | wc -l 1シェルスクリプトを作成
root@af48c1a975a2:/# mkdir ~/dev/xgcc root@af48c1a975a2:/# cd ~/dev/xgccシェルスクリプト(ソースコードをダウンロード)
root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_download.shxgcc_download.sh#!/bin/bash set -e BINUTILS_VERSION=binutils-2.34 GCC_VERSION=gcc-7.3.0 LINUX_KERNEL_VERSION=linux-5.3 GLIBC_VERSION=glibc-2.27 wget -nc http://ftpmirror.gnu.org/binutils/$BINUTILS_VERSION.tar.gz wget -nc http://ftpmirror.gnu.org/gcc/gcc-7.3.0/$GCC_VERSION.tar.gz wget -nc https://www.kernel.org/pub/linux/kernel/v5.x/$LINUX_KERNEL_VERSION.tar.xz wget -nc http://ftpmirror.gnu.org/glibc/$GLIBC_VERSION.tar.xz for f in *.tar*; do tar xfk $f done cd $GCC_VERSION ./contrib/download_prerequisitesシェルスクリプト(ソースコードをビルド)
root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_build.shxgcc_build.sh#!/bin/bash set -e PREFIX=/usr/local # CPU 1 PARALLEL_MAKE= #CONFIGURATION_OPTIONS="--disable-multilib --disable-nls" CONFIGURATION_OPTIONS="--disable-multilib --disable-nls --disable-libmpx" BINUTILS_VERSION=binutils-2.34 GCC_VERSION=gcc-7.3.0 LINUX_KERNEL_VERSION=linux-5.3 GLIBC_VERSION=glibc-2.27 build() { local TARGET="$1" local LINUX_ARCH="$2" # Step 1. Binutils mkdir -p build-binutils-$TARGET cd build-binutils-$TARGET ../$BINUTILS_VERSION/configure --prefix=$PREFIX --target=$TARGET $CONFIGURATION_OPTIONS make $PARALLEL_MAKE make install cd .. # Step 2. Linux Kernel Headers cd $LINUX_KERNEL_VERSION make ARCH=$LINUX_ARCH INSTALL_HDR_PATH=$PREFIX/$TARGET headers_install cd .. # Step 3. C/C++ Compilers mkdir -p build-gcc-$TARGET cd build-gcc-$TARGET ../$GCC_VERSION/configure --prefix=$PREFIX --target=$TARGET --enable-languages=c,c++ $CONFIGURATION_OPTIONS make $PARALLEL_MAKE gcc_cv_libc_provides_ssp=yes all-gcc make install-gcc cd .. # Step 4. Standard C Library Headers and Startup Files mkdir -p build-glibc-$TARGET cd build-glibc-$TARGET ../$GLIBC_VERSION/configure --prefix=$PREFIX/$TARGET --build=$MACHTYPE --host=$TARGET --target=$TARGET --with-headers=$PREFIX/$TARGET/include $CONFIGURATION_OPTIONS libc_cv_forced_unwind=yes make install-bootstrap-headers=yes install-headers make $PARALLEL_MAKE csu/subdir_lib install csu/crt1.o csu/crti.o csu/crtn.o $PREFIX/$TARGET/lib $TARGET-gcc -nostdlib -nostartfiles -shared -x c /dev/null -o $PREFIX/$TARGET/lib/libc.so touch $PREFIX/$TARGET/include/gnu/stubs.h cd .. # Step 5. Compiler Support Library cd build-gcc-$TARGET make $PARALLEL_MAKE all-target-libgcc make install-target-libgcc cd .. # Step 6. Standard C Library & the rest of Glibc cd build-glibc-$TARGET make $PARALLEL_MAKE make install cd .. # Step 7. Standard C++ Library & the rest of GCC cd build-gcc-$TARGET make $PARALLEL_MAKE all make install cd .. rm -rf build-binutils-$TARGET build-gcc-$TARGET build-glibc-$TARGET } build i686-linux-gnu x86 build arm-linux-gnueabi arm build arm-linux-gnueabihf arm build aarch64-linux-gnu arm64 build mips-linux-gnu mips build mipsel-linux-gnu mips build mips64el-linux-gnuabi64 mips # 以下は私の環境ではエラーが出て駄目でした。 #build powerpc-linux-gnu powerpc #build powerpc64le-linux-gnu powerpc #build alpha-linux-gnu alpha #build s390x-linux-gnu s390 #build m68k-linux-gnu m68k #build x86_64-linux-gnu x86 #build sparc-linux-gnu sparc #build sparc64-linux-gnu sparc #build cris-linux-gnu cris #build sh4-linux-gnu shシェルスクリプト(各アーキテクチャのbinutilsをビルド)
root@af48c1a975a2:~/dev/xgcc# vi ~/dev/xgcc/xgcc_build_all_binutils.shxgcc_build_all_binutils.sh#!/bin/bash # build-binutils-all.sh set -e PREFIX=/usr/local # cpu 1 PARALLEL_MAKE= CONFIGURATION_OPTIONS="--disable-multilib --disable-nls" BINUTILS_VERSION=binutils-2.34 mkdir -p build_all_binutils cd build_all_binutils ../$BINUTILS_VERSION/configure --prefix=$PREFIX --enable-targets=all --enable-64-bit-bfd --program-prefix=all- $CONFIGURATION_OPTIONS make $PARALLEL_MAKE make install cd .. rm -rf build_all_binutilsスクリプトを実行
要:かなりの時間とディスク容量
root@af48c1a975a2:~/dev/xgcc# cd ~/dev/xgcc root@af48c1a975a2:~/dev/xgcc# bash xgcc_download.sh root@af48c1a975a2:~/dev/xgcc# nohup bash xgcc_build.sh & root@af48c1a975a2:~/dev/xgcc# nohup bash xgcc_build_all_binutils.sh &動作テスト(MIPS向けのアセンブリ出力)
root@af48c1a975a2:~/dev/xgcc# mkdir test root@af48c1a975a2:~/dev/xgcc# cd test root@af48c1a975a2:~/dev/xgcc/test# vi main.cmain.c#include <stdio.h> int main(void) { printf("Hellow"); return 0; }実行例
root@af48c1a975a2:~/dev/xgcc/test# mips-linux-gnu-gcc -S main.c root@af48c1a975a2:~/dev/xgcc/test# cat main.s .file 1 "main.c" .section .mdebug.abi32 .previous .nan legacy .module fp=32 .module nooddspreg .abicalls .text .rdata .align 2 $LC0: .ascii "Hellow\000" .text .align 2 .globl main .set nomips16 .set nomicromips .ent main .type main, @function main: .frame $fp,32,$31 # vars= 0, regs= 2/0, args= 16, gp= 8 .mask 0xc0000000,-4 .fmask 0x00000000,0 .set noreorder .set nomacro addiu $sp,$sp,-32 sw $31,28($sp) sw $fp,24($sp) move $fp,$sp lui $28,%hi(__gnu_local_gp) addiu $28,$28,%lo(__gnu_local_gp) .cprestore 16 lui $2,%hi($LC0) addiu $4,$2,%lo($LC0) lw $2,%call16(printf)($28) nop move $25,$2 .reloc 1f,R_MIPS_JALR,printf 1: jalr $25 nop lw $28,16($fp) move $2,$0 move $sp,$fp lw $31,28($sp) lw $fp,24($sp) addiu $sp,$sp,32 jr $31 nop .set macro .set reorder .end main .size main, .-main .ident "GCC: (GNU) 7.3.0" root@af48c1a975a2:~/dev/xgcc/test#Dockerコンテナを停止
exit # Ubuntuのshellから抜ける docker stop gcc-ubuntu # コンテナが停止 docker ps -a # 停止確認
- 投稿日:2020-03-31T09:27:20+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.13 - TDDでPost機能をコーディング part2 -
はじめに
こんにちは!
今回は前回のPost機能のコーディングの続きです。ユーザー詳細ページでそのユーザーが投稿したポストに絞って表示させる機能をコーディングします。前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
前回の残り
まずは前回やり残したテストシナリオをもう一度確認しておきましょう。
- ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
- ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
- ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
- サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
残り11シナリオ。今回は5シナリオをやっていきます!それでポストページが完了、あとはユーザー詳細ページにポストを表示するストーリーだけになります。
今回もコンテナを立ち上げてコンテナの中でコマンドを実行していきたいと思います。
$ docker-compose up -d $ docker-compose exec web ashポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
今回もテストコードからです。「ポストする」ボタンには
post_button
のid
を付与するとしましょう。spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", type: :system do ... + scenario "ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること" do # テスト用のユーザーを作成する + user = create_user(1) # このテストシナリオで使うポスト内容として空文字を定義する + content = "" # このテストシナリオで期待するエラーメッセージを定義する + error_message = "ポストを入力してください" # テスト開始前のDB内のPostの数を記憶しておく + post_count = Post.count # userでサインインする + sign_in(user) + # ポストページにアクセスする + visit posts_path # ポスト入力欄にcontentを入力する + fill_in :post_content, with: content # 投稿するボタン(#post_button)をクリックする + click_on :post_button + # 現在のページがポストページであることを検証する + expect(current_path).to eq posts_path # ページ内に期待するエラーメッセージが表示されていることを検証する + expect(page).to have_text error_message # DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する + expect(Post.count).to eq post_count + end end検証内容としては特に「期待するエラーメッセージが表示されていること」と「DB内のPostの数に変化がないこと」を検証することでバリデーションが効いていることを確認しています。
では、テストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること Failure/Error: click_on :post_button Capybara::ElementNotFound: Unable to find link or button :post_button Finished in 20.3 seconds (files took 7.34 seconds to load) 11 examples, 1 failure
#post_button
がないので怒られました。ので、ボタンを追加しましょう。app/views/posts/index.html.erb<div class="container my-5"> <%= form_with model: @post, url: nil, local: true do |form| %> <div class="form-group"> <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %> </div> + <div class="text-right"> + <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %> + </div> <% end %> </div>
post_button
を追加してみました。UI的には以下のような感じになっているはず!
OK。ボタンは探せるようになったはずなのでまたテスト。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること Failure/Error: expect(page).to have_text error_message expected to find text "ポストを入力してください" in "The page you were looking for doesn't exist.\nYou may have mistyped the address or the page may have moved.\nIf you are the application owner check the logs for more information." Finished in 11.97 seconds (files took 8.31 seconds to load) 11 examples, 1 failure次はエラーメッセージがみつからないみたいです。確かにエラーメッセージを表示する機能は作っていない!
まずエラーメッセージを表示するためには、ポストを送信してバリデーションに引っかかり、そのエラーメッセージをポストページ、つまり
posts/index.html.erb
で表示できるようにする必要があります。まずは、Postsコントローラーに
create
アクションを作成して、そのアクションにポストを送信できるようにルーティングとform_with
を更新しましょう。app/controllers/posts_controller.rbclass PostsController < ApplicationController def index redirect_to root_path unless signed_in? @post = Post.new end + + def create + # 未サインインの場合、トップページにリダイレクトする + redirect_to root_path unless signed_in? + # Strong parameterからリクエスト内容の通りにPostモデルオブジェクトを作成する + @post = Post.new(post_params) + # Postモデルオブジェクトのuser_idにサインイン中のuserのidを定義する + @post.user = current_user + + if @post.save + # Todo: DB保存が成功した場合の動作 + else + # DB保存が失敗した場合、ポストページをレンダリングする + render :index + end + end + + private + # PostのStrong parameter + def post_params + params.require(:post).permit(:content) + end end
@post.user
はあえてフォームからの送信ではなく、サーバーサイドでcurrent_user
、つまりサインイン中のユーザーを設定しています。フォームから送信されたユーザーのIDを設定する方法も考えられますが、フォームの内容はユーザー側で簡単に改変できてしまうので違うユーザーのポストとして登録されてしまう危険性があります。
そのため、今回はサーバー側で処理をするようにしてみました。config/routes.rbRails.application.routes.draw do ... get '/posts', to: 'posts#index', as: :posts + post '/posts', to: 'posts#create', as: :create_post end
app/views/posts/index.html.erb... - <%= form_with model: @post, url: nil, local: true do |form| %> + <%= form_with model: @post, url: create_post_path, local: true do |form| %> ...これで
create
アクションで@post.save
が失敗した場合、@post
にエラーメッセージが格納されてindex.html.erb
がレンダリングされるようになるはずです。次に、
@post.save
がcontent
が未入力の場合にエラーになるようにPost
モデルのcontent
属性にpresence
を定義しましょう。app/models/post.rbclass Post < ApplicationRecord belongs_to :user + + validates :content, + presence: true end
Postモデルには
presence
のバリデーションを定義します。最後にエラーメッセージを表示できるようにViewを更新します。
app/views/posts/index.html.erb<div class="container my-5"> + + <% if @post.errors.any? %> + <div class="alert alert-danger"> + <ul class="mb-0"> + <% @post.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> + <% end %> + <%= form_with model: @post, url: create_post_path, local: true do |form| %> <div class="form-group"> <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %> </div> <div class="text-right"> <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %> </div> <% end %> </div>
これでポストが未入力の場合にエラーメッセージが表示されるようになっているはずです。再びテストを実行してみます。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること Failure/Error: expect(page).to have_text error_message expected to find text "ポストを入力してください" in "sample app\nPosts\nProfile\nSign out\nContentを入力してください\n(c) Hoge Inc. All Rights Reserved." Finished in 19.22 seconds (files took 7.93 seconds to load) 11 examples, 1 failureまたテスト失敗理由が変わりました。
今度は「ポストを入力してください」というエラーメッセージを期待していたが、「Contentを入力してください」となってしまっていたようです。属性の日本語表記は
config/locales/ja.yml
で定義していますので、更新していきます。config/locales/ja.ymlja: activerecord: attributes: user: name: "お名前" email: "メールアドレス" password: "パスワード" password_confirmation: "確認用パスワード" + post: + content: "ポスト" errors: ...
ここまででモデルのバリデーションとエラーメッセージの表示までの実装を終えました。ではテストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb Finished in 19.99 seconds (files took 7.33 seconds to load) 11 examples, 0 failuresこれでテストがGreenの状態になりました。
ここで少しリファクタリングしておきます。今、
Posts
コントローラーのindex
アクションとcreate
アクションの最初に未サインインであればトップページにリダイレクトする、全く同じ処理を書いてしまっています。
これではDRYではないので、メソッド化してbefore_action
で実行するようにリファクタします。app/controllers/posts_controller.rbclass PostsController < ApplicationController + before_action :redirect_to_root_unless_signed_in def index - redirect_to root_path unless signed_in? @post = Post.new end def create - redirect_to root_path unless signed_in? @post = Post.new(post_params) @post.user = current_user if @post.save else render :index end end private def post_params params.require(:post).permit(:content) end + + # 未サインインの場合、トップページにリダイレクトするメソッド + def redirect_to_root_unless_signed_in + redirect_to root_path unless signed_in? + end endはい。
before_action
にひとまとめにしてみました。ではこれでもちゃんとテストがパスするか確認をしておきましょう。
# rspec spec/system/07_posts_spec.rb Finished in 18.5 seconds (files took 5.6 seconds to load) 11 examples, 0 failuresGreenな状態をキープしたままリファクタリングができましたね!
ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
まずはテストです!
spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", type: :system do ... + scenario "ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること" do + # テスト用のユーザーを作成する + user = create_user(1) + # このテストシナリオで使うポスト内容として141文字を定義する + content = "a" * 141 + # このテストシナリオで期待するエラーメッセージを定義する + error_message = "ポストは140文字以内で入力してください" + # テスト開始前のDB内のPostの数を記憶しておく + post_count = Post.count + # userでサインインする + sign_in(user) + + # ポストページにアクセスする + visit posts_path + # ポスト入力欄にcontentを入力する + fill_in :post_content, with: content + # 投稿するボタン(#post_button)をクリックする + click_on :post_button + + # 現在のページがポストページであることを検証する + expect(current_path).to eq posts_path + # ページ内に期待するエラーメッセージが表示されていることを検証する + expect(page).to have_text error_message + # ポスト入力欄に入力していたポスト内容がそのまま残っていることを検証する + expect(find("#post_content").value).to eq content + # DB内のPostの数が変わらない(=Postの登録が失敗している)ことを検証する + expect(Post.count).to eq post_count + end end
テスト実行です。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること Failure/Error: expect(page).to have_text error_message expected to find text "ポストは140文字以内で入力してください" in "sample app\nPosts\nProfile\nSign out\n(c) Hoge Inc. All Rights Reserved." Finished in 27.16 seconds (files took 5.71 seconds to load) 12 examples, 1 failureエラーメッセージを見つけられないため失敗しているようです。今、140文字の制限を
Post
モデルのcontent
属性に付与していないのでPost.save
がtrue
だったのでしょう。
バリデーションを追加してみます。app/models/post.rbclass Post < ApplicationRecord belongs_to :user validates :content, - presence: true + presence: true, + length: { maximum: 140 } endでは、またテストを実行してみます。
# rspec spec/system/07_posts_spec.rb Finished in 22.78 seconds (files took 6.21 seconds to load) 12 examples, 0 failuresGreenになりました!
ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
テストから。
spec/system/07_posts_spec.rb... feature "ユーザーとして、ポストを投稿したい", type: :system do ... + scenario "ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること" do + # テスト用のユーザーを作成する + user = create_user(1) + # このテストシナリオで使うポスト内容を4つ用意する + # "Hello, world.": 通常のポスト内容 + # "a": 0文字がNGなので境界値として1文字のポスト内容を用意 + # "a" * 140: 141文字がNGなので境界値として140文字のポスト内容を用意 + # "Hello.\nWorld.": 特殊なケースとして改行が入っているポスト内容を用意 + contents = ["Hello, world.", "a", "a" * 140, "Hello.\nWorld."] + # userでサインインする + sign_in(user) + + # contentsの中から1つずつをテストする + contents.each do |content| + # テスト開始前のDB内のPostの数を記憶しておく + post_count = Post.count + + # ポストページにアクセスする + visit posts_path + # ポスト入力欄にcontentを入力する + fill_in :post_content, with: content + # 投稿するボタン(#post_button)をクリックする + click_on :post_button + + # 現在のページがポストページであることを検証する + expect(current_path).to eq posts_path + # ポスト内容がクリアされていることを検証する + expect(find("#post_content").value).to eq "" + # DB内のPostが1つ増えている(=投稿したポストが保存された)ことを検証する + expect(Post.count).to eq post_count + 1 + # ポストページのポスト一覧の一番上に投稿したポストが表示されていることを検証する + expect(find("#posts_list").all(".post-item").first).to have_text content + expect(find("#posts_list").all(".post-item").first).to have_text user.name + end + end end
あまり
each
とかを使うとどのケースでテスト失敗したのか行数からはわかりにくくなってしまう危険性もあるのですが、長く書くのも面倒なので今回はいくつかの文字列をポストするテストをeach
で繰り返してみました。
"a"
と"a" * 140
は境界値のテストをしています。
"Hello.\nWorld."
は改行が入った時に正しく登録・表示されるかをテストするためのパターンです。
posts_list
はポストページで過去のポストが一覧表示されるエリアのid
としてます。その中で各ポストにはpost-item
のclass
を割り当ててall(".post-item")
でそのコレクションを取得できるようにしようと思います。.first
でその中でも一番最初に表示されるもの、つまり一番上に表示される要素を検証対象としています。# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること Failure/Error: expect(find("#post_content").value).to eq "" expected: "" got: "Hello, world." (compared using ==) Finished in 22.11 seconds (files took 6.73 seconds to load) 13 examples, 1 failureテストは失敗しています。
ポストが成功した場合はポストの入力エリアが空白に戻るようにしたいのですがそれができていないようです。ポスト成功時の動作を
posts#create
でまだコーディングできていないので記述していきます。
単にposts#index
アクションにリダイレクトしてあげるだけで、index
アクションの中で@post = Post.new
が実行されるのでcontent
を初期化できそうです。app/controllers/posts_controller.rbclass PostsController < ApplicationController ... def create @post = Post.new(post_params) @post.user = current_user if @post.save + redirect_to posts_path else render :index end end a... end
これで完成。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content Capybara::ElementNotFound: Unable to find css "#posts_list" Finished in 26.85 seconds (files took 7.87 seconds to load) 13 examples, 1 failure失敗理由が変わりました。
#posts_list
なんて要素ないよ、とのことなのでViewを作っていきましょう。まずは
#posts_list
で表示するデータをコントローラー側で取得しておく必要がありますので、posts#index
アクションで全てのPostを作成日降順で取得するようにしましょう。app/controllers/posts_controller.rbclass PostsController < ApplicationController ... def index @post = Post.new + # 更新日時降順で全てのポストを@postsに代入する + @posts = Post.order(created_at: :desc) end ... end
全てのポストを作成日時降順で
@posts
に入れてます。
続いてはこのインスタンス変数を使ってViewを作っていきます。app/views/posts/index.html.erb<div class="container my-5"> <% if @post.errors.any? %> <div class="alert alert-danger"> <ul class="mb-0"> <% @post.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <%= form_with model: @post, url: create_post_path, local: true do |form| %> <div class="form-group"> <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %> </div> <div class="text-right"> <%= form.submit "ポストする", class: "btn btn-primary", id: :post_button %> </div> <% end %> + + <div id="posts_list" class="my-5"> + <% @posts.each do |post| %> + <div class="card post-item my-1"> + <div class="card-body"> + <h5 class="card-title"><%= post.user.name %></h5> + <p class="card-text"><%= post.content %></p> + </div> + </div> + <% end %> + </div> </div>
form_with
よりも下にid=posts_list
のフィールドを作って@posts
をひとつずつ表示してます。
post.user.name
でpost
の外部キーuser_id
のUserモデルオブジェクトのname
を取得して表示してます。PostモデルとUserモデルを関連付けしているのでこういった使い方ができるんですね。では、テストしてみましょう。
# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること Failure/Error: expect(page).to have_text error_message expected to find text "ポストを入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information." 2) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること Failure/Error: expect(page).to have_text error_message expected to find text "ポストは140文字以内で入力してください" in "We're sorry, but something went wrong.\nIf you are the application owner check the logs for more information." 3) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content expected to find text "Hello.\nWorld." in "John Smith\nHello. World." Finished in 34 seconds (files took 5.62 seconds to load) 13 examples, 3 failuresうぉ、3つもテストが失敗している...
1)
と2)
はなにやら例外が発生してしまっているようです。試しにdevelopment
環境でポストを未入力で「ポストする」ボタンをクリックしてみましょう。
ふむふむ。<% @posts.each do |post| %>
のところでundefined method 'each'
が起きてますね。この2つのテストは
posts#create
アクションで@post.save
がfalse
の時にrender :index
でレンダリングさせているケースです。
もう一度コードをよくみると、このレンダリングまでにこのアクションでは@posts
というインスタンス変数を定義していません。
今回の例外は@posts
というモデルオブジェクトのインスタンス変数がないにもかかわらず@posts.each
を使おうとしていることから起きた例外と推測できます。
posts#create
アクションを見直してみましょう。app/controllers/posts_controller.rbclass PostsController < ApplicationController ... def create @post = Post.new(post_params) @post.user = current_user if @post.save redirect_to posts_path else + # 更新日時降順で全てのポストを@postsに代入する + @posts = Post.order(created_at: :desc) render :index end end ... end
@post.save
がfalse
の場合、render
の前に@posts
を定義するようにしてみました。
例外が解消されたかもう一度テストしてみましょう。# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること Failure/Error: expect(find("#posts_list").all(".post-item").first).to have_text content expected to find text "Hello.\nWorld." in "John Smith\nHello. World." Finished in 32.49 seconds (files took 7.4 seconds to load) 13 examples, 1 failureふー。今取り組んでいるテストシナリオだけがテスト失敗している状態に戻せましたね。
内容をみてみると"Hello.\nWorld."
のポストが成功しているはずなのにページに表示されていないようです。
スクリーンショットをみると、確かに一番上のポストの文字列が改行されていないHello. World.
という文字列になっていますね。実は
post.content
のような書き方では文字列を表示することはできるのですが、改行がうまく表現できません。
これを解決するために、今回はsafe_join
メソッドを使って改行を正しく表示できるようにします。(参考: 「simple_format」や「safe_join」を使って、正常に改行表示させる方法(Rails) - りょうたくの技術ブログ)app/views/posts/index.html.erb- <p class="card-text"><%= post.content %></p> + <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>ちょっと書き方が複雑ですが、これでテキストエリアでつけた改行の通りに改行を表現することができるようになります。
こういったやり方はいろいろあるので、どう表現したいかによって使い分けが必要です。では再度テストを実行してみましょう。
# rspec spec/system/07_posts_spec.rb Finished in 30.57 seconds (files took 7.13 seconds to load) 13 examples, 0 failures無事テストをパスすることができました!!
このように、テストで用いるデータの種類をいろいろなケース用意することで本当に期待通りの動作をしているかを正しく把握し実装することができるのです。サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
ここからはポストページの表示系ですね。
まずは投稿日時降順で表示ができているかという点です。複数ユーザーが投稿していることにします。spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", type: :system do ... + scenario "サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること" do + # テスト用のユーザーを作成する + user1 = create_user(1) + user2 = create_user(2) + # ポストを用意する + posts = [] + posts.unshift Post.create(content: "first post", user: user1) + posts.unshift Post.create(content: "初めてのポスト", user: user2) + posts.unshift Post.create(content: "second post!!", user: user1) + # userでサインインする + sign_in(user1) + + # ポストページにアクセスする + visit posts_path + + # 投稿日時降順でポストが表示されていることを検証する + posts.each_with_index do |post, i| + expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name + expect(find("#posts_list").all(".post-item")[i]).to have_text post.content + end + end end
今回は適当に3つのポストを生成しておき、作成日時降順でポストが表示されているかをチェックしています。
posts
という空のArrayを最初に作成し、unshift
メソッドを使って配列の先頭にPost.create
を挿入していきます。最終的には作成日時降順で配列にPostのモデルオブジェクトが格納されるかたちになります。(参考: Rubyで配列に要素を追加・挿入する:push, insert, unshift | UX MILK)これを
each_with_index
でモデルオブジェクトとindexを取り出して、posts_list
のi
番目にある要素が新しい方からi
番目に作成されたポストであるかどうかを検証しているというわけです。(参考: Rubyのeachでindexを取得する:each_with_index | UX MILK)では、テストを実行してみましょう。
# rpsec spec/system/07_posts_spec.rb Finished in 30.64 seconds (files took 6.38 seconds to load) 14 examples, 0 failuresこの辺りはすでに作り込みが終わっているのでテストがパスしていますね。
サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
まずはテストです。ポストしたユーザーの名前を表示している要素には
post-user-name
というclass
をつけることにします。spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", type: :system do ... + scenario "サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do + # テスト用のユーザーを作成する + user1 = create_user(1) + user2 = create_user(2) + # ポストを用意する + posts = [] + posts.unshift Post.create(content: "First Post!!", user: user1) + posts.unshift Post.create(content: "初めてのポスト", user: user2) + # user1でサインインする + sign_in(user1) + + posts.each_with_index do |post, i| + # ポストページにアクセスする + visit posts_path + # 上からi番目のポストのユーザー名をクリックする + find("#posts_list").all(".post-item")[i].find(".post-user-name").click + + # 現在のページがクリックしたポストのユーザーのユーザー詳細ページであることを検証する + expect(current_path).to eq user_path(post.user) + end + end end
いままでは
click_on
を使ってクリック操作を実装していましたが、要素.click
でも同じようにクリック操作ができます。click_on
の場合はボタンやリンクに限定されていたのですが、click
の場合はどんな要素であれクリック操作ができるので必要になるケースもあります。覚えておいてくださいね。# rspec spec/system/07_posts_spec.rb Failures: 1) ユーザーとして、ポストを投稿したい After sign in Created posts サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること Failure/Error: find("#posts_list").all(".post-item")[i].find(".post-user-name").click Capybara::ElementNotFound: Unable to find css ".post-user-name" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[1]/DIV[1]/DIV[1]"> Finished in 35.66 seconds (files took 6.04 seconds to load) 15 examples, 1 failure
post-user-name
classの要素が見つからないようですね。現在ポストしたユーザーの名前を表示している要素にpost-user-name
classのリンクを定義しましょう。app/views/posts/index.html.erb- <h5 class="card-title post-user-name"><%= post.user.name %></h5> + <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
link_to
メソッドを使ってリンクを生成してみました。# rspec spec/system/07_posts_spec.rb Finished in 34.14 seconds (files took 5.9 seconds to load) 15 examples, 0 failuresテストパス!
まとめ
はい。今日はここまでです!
今回まででポストページの機能を実装することができましたね。
あとはユーザー詳細ページ側でそのユーザーのポストを表示するテストと機能を実装していきましょう!ではまた次週!
後片付け
# exit$ docker-compose down本日のソースコード
Other Hands-on Links
- 投稿日:2020-03-31T00:45:06+09:00
swagger-nodeとDockerで簡単にモックサーバーを構築する
はじめに
APIを呼び出す機能をテストする際に、とりあえずモックサーバーを立てたい。
そんなときに便利なのが、SwaggerのNode.js 製のモジュールであるswagger-nodeです。SwaggerでAPIを定義しておくだけで、モックサーバーを起動できます。
便利なのは、自前でテストデータを用意しなくても、データ型(string, number, boolean, array, object etc)に応じた適当な値をレスポンスしてくれる点です。Swaggerの概要やswagger-nodeの使い方はこちらで紹介されているため、詳細は割愛します。
Swaggerとswagger-nodeこの記事では、Node.jsのインストールが面倒なのと、環境を汚さずに使いたいということで、
swagger-nodeとDockerを組み合わせてモックサーバーを構築する方法を説明します。前提
dockerとdocker-composeを利用できること
モックサーバーを構築する
Dockerfileの作成
まずはswagger-nodeを内包するDockerイメージを作成します。
最新のLTS版であるNode.js v12だとエラーになるようなので、v10を使用します。
https://github.com/swagger-api/swagger-node/issues/586
npm install --save swagger-router
すればOKとの情報もありましたが、未確認です。適当なディレクトリでDockerfileを作成します。
FROM node:10 # アプリケーションディレクトリを作成する WORKDIR /usr/src/app # swagger-nodeをグローバルインストールする RUN npm install swagger -g ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 10; done"]これをビルトしてDockerイメージを作成します。
$ docker build . -t swagger-nodeswaggerプロジェクトの作成
swaggerプロジェクトを作成するために、swagger-nodeをインストールしたコンテナを起動して、swaggerコマンドを実行します。
作成したswaggerプロジェクトはホストにマウントするために、-v $PWD:/usr/src/app
オプションを付けておきます。$ docker run -d -v $PWD:/usr/src/app --name first-swagger-container swagger-node $ docker exec -it first-swagger-container swagger project create sample-project ? Framework? connect ❯ express hapi restify sails作成されたsample-projectはこのような構成になります。
swagger.yamlがAPI定義ファイルです。sample-project/
├ api/
│ ├ controllers/
│ ├ helpers/
│ ├ mocks/
│ └ swagger
│ └ swagger.yaml
├ config/
├ node_modules/
├ test/
├ .gitignore
├ app.js
├ package-lock.json
├ package.json
└ README.mdなお、ここで作成したコンテナは不要なので削除します。
$ docker rm --force first-swagger-containerDockerコンテナ起動
同じディレクトリにdocker-compose.yamlを作成して、
swagger-mock
(mockサーバーコンテナ)とswagger-editor
(swagger.yamlを編集するためのコンテナ)を定義します。docker-compose.yamlversion: "3.6" services: swagger-mock: build: . environment: - CHOKIDAR_USEPOLLING=true ports: - "10010:10010" volumes: - .:/usr/src/app networks: examples-net: ipv4_address: 172.16.239.103 # -m: モックモードとして起動するオプション entrypoint: bash -c "cd sample-project && swagger project start -m" swagger-editor: build: . ports: - "8000:8000" volumes: - .:/usr/src/app networks: examples-net: ipv4_address: 172.16.239.104 # -s: 起動時にブラウザが立ち上がらないようにする # -p: 指定しない場合ポートがランダムになるため8000で固定する # --host: ipv4_addressで指定したIPを設定する entrypoint: bash -c "cd sample-project && swagger project edit -s -p 8000 --host 172.16.239.104" networks: examples-net: name: examples-net driver: bridge ipam: driver: default config: - subnet: 172.16.239.0/24
swagger-mock
のポイントとしては、CHOKIDAR_USEPOLLING=true
を設定している点です。
これは、マウントされているファイルが更新された際にコンテナを自動起動するための設定です。
これによって、swagger.yamlが更新された際に即時にモックサーバーに反映されるようになります。
- 補足
- swagger-nodeには、nodemonというモジュールが組み込まれており、ファイル更新を検知したら自動で再起動する仕組み(いわゆるホットリロード)が実装されています。 しかし、Dockerと組み合わせた場合にこれが効かなくなってしまい、Dockerコンテナとしてホットリロードする方法を採用しました。
それではDockerコンテナを起動してみます。
$ docker-compose up -dコンテナが起動すると
swagger-mock
がモックサーバーとして利用できる状態になります。
試しにhttp://<コンテナのIPアドレス>:10010
に対して、hello API(初回作成時のサンプルAPI)を呼ぶと、レスポンスを返してくれます。$ curl http://192.168.99.100:10010/hello?name=Scott {"message":"Sample text"}swagger.yamlを更新
目的とするAPIのモックサーバーを起動するには、swagger.yamlを編集する必要があります。
そこでswagger-editor
コンテナを使用します。
ブラウザでhttp://<コンテナのIPアドレス>:8000
にアクセスするとswagger.yamlの編集画面が開きます。試しに新しいAPIとして次のようにuser API(Get, POST)を定義してみます。
左側の編集パネルに次のように追記します。swagger.yamlswagger: "2.0" version: "0.0.1" title: Hello World App # during dev, should point to your local machine host: localhost:10010 # basePath prefixes all resource paths basePath: / # schemes: # tip: remove http to make production-grade - http - https # format of bodies a client can send (Content-Type) consumes: - application/json # format of the responses to the client (Accepts) produces: - application/json paths: ...(略)... /user: x-swagger-router-controller: UserController get: operationId: getUser parameters: - name: id in: query required: true type: string responses: "200": description: Success schema: properties: name: type: string age: type: number post: operationId: postUser parameters: - name: "user" in: "body" required: true schema: required: - name - age type: object properties: name: type: string age: type: number responses: "200": description: Success schema: properties: message: type: string ...(略)...swagger.yamlの書き方について、基本的にはSwaggerの構文に従えばOKです。
ここで重要なのはx-swagger-router-controller
に任意の値を設定することです。
これはswagger-node独自の項目で、ルーティングするコントローラー(.js)を設定するものです。
swagger project start
コマンドで普通に起動する場合は、ここで設定したコントローラー(.js)にルーティングされ、実装された処理に従ってレスポンスが返されます。
モックサーバーと起動する(-m
オプションを付与する)場合は、コントローラーが存在しなくても、responsesで定義している型に応じてレスポンスを返してくれるのです。動作確認
ブラウザの編集画面に書き込んだ時点で、swagger.yamlが更新され、上述したホットリロードの設定によってswagger-mockコンテナが再起動されます。
つまり、即時にモックサーバーに反映されています。
自身でコンテナを再起動する必要はありません。実際にuser APIを呼んでみます。
$ curl http://192.168.99.100:10010/user?id=xxx {"name":"Sample text","age":1} $ curl -X POST -H 'Content-Type:application/json' -d '{"id": "aaa", "name": "hoge", "age": 10}' http://192.168.99.100:10010/user {"message":"Sample text"}適当な値が設定されたレスポンスを返してくれました。
所感
APIを呼び出す機能をテストしたい場合、Swagger定義をもらって編集画面を開いてコピペして、
x-swagger-router-controller
という若干の設定を行えば、簡単にモックサーバーを起動できます。あとは、自分でAPIを定義する場合にも、「swagger.yamlを編集→API呼び出しして確認」というライフサイクルを回しやすそうです。
おわりに
Dockerを勉強中なので、無駄な手順が含まれているように思います。(特にswaggerプロジェクトを作成するあたり)
おいおい勉強して更新していきたいと思います。