- 投稿日:2021-10-31T21:06:51+09:00
【個人用rails tutorial -3
静的ページの生成 ①コントローラー,モデル,アクションの削除方法 $rails destroy 〜 ②マイグレーションの変更を1つ前に戻す方法 $ rails db:rollback ③最初に戻したい時 $ rails db:migrate VERSION=0 ※マイグレーションは逐次的に実行され、マイグレーションに対してバージョン番号が付与される。上記の0を別の数字に置き換えることによって、指定したバージョンの状態に戻せる。 テストスイート(Test Suite) ①なぜテストを行うのか 1.機能停止に陥るような回帰バグ(Regression Bug: 以前のバグが再発したり機能の追加/変更に副作用が生じたりすること)を防止 2.コードを安全にリファクタリング(機能を変更せずにコードを改善)できる。 3.アプリケーションコードから見ればクライアントとして動作するので、アプリケーションの設計やシステムの他の部分とのインターフェイスを決めるときにも役に立つ。 ②テスト方法 1.test/controllers/controllr_namecontroller_test.rbを開く (コントローラ作成時に自動的に生成されている) 2.テストコードを書く 3.ターミナルで以下を入力 $rails test 4.結果がREDならエラー分を見て修正 ③タイトルをテストしてみる 1.assert_selectメソッドを使う(レイアウト内で頻繁に変更されるHTML要素 (リンクなど) をテストする) 2.titleタグ内に「Home | Ruby on Rails Tutorial Sample App」という文字列があるかどうかをチェック assert_select "title", "Home | Ruby on Rails Tutorial Sample App" 埋め込みRuby(コードの重複を取り除く) ①なぜ行うのか 重複しているコードがあると余計に長くなってしまい読みづらい ②重複を取り除く方法 1.provideメソッドを使う(タイトルをページごとに変更するときに使うメソッド) 2.各viewページの先頭に以下コードを記入 <% provide(:title, "タイトル名") %> 3.titleタグを以下に変更 <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title> <% ... %>:中に書かれたコードを単に実行するだけで何も出力しない <%= ... %>:中のコードの実行結果がテンプレートのその部分に挿入される 5.application.html.erbファイルのデフォルトタイトル部分を以下に編集 6.<%= yield %> の説明 各ページの内容をレイアウトに挿入するためのもの ※homeで説明すると、レイアウトを使う際に、/static_pages/homeにアクセスするとhome.html.erbの内容がHTMLに変換され、<%= yield %>の位置に挿入される
- 投稿日:2021-10-31T21:02:12+09:00
Rails+React+MySQL+nginx+puma on Docker
はじめまして。 初投稿です。 よろしくお願いします。 概要 Docker 自己紹介 初投稿なので軽く自己紹介します。 現在横浜の大学に通うB3で絶賛就活中です。 Qiitaの方々の記事に大変お世話になっているのでぜひ自分も書いて誰かの役に立てればと思い書いてみました。 誰かのtipsになれば幸いです。 承認欲求強めです。いいね、ストック、スター、フォローしてくれると飛び跳ねるくらい嬉しいです。 よろしくお願いします。 すぐはじめたい方へ 完成版 github clone後setup.shを実行すればコンテナが立ち上がります。 構成 バック version Ruby 2.6.2 Rails(API モード) 6.1.4 nginx 1.20.1 MySQL 5.7 フロント version Node.js 12.6.1 React 17.0.2 nginx 1.20.1 投稿時テーブルを上から持ってきたままになっておりバージョンの編集が行われておりませんでしたので編集を行いました。 ディレクトリ . ├── api │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf │ └── server │ ├── Dockerfile │ ├── Gemfile │ ├── Gemfile.lock │ ├── entrypoint.sh │ └── run.sh ├── frontend │ ├── Dockerfile │ └── run.sh │ ├── front │ │ ├ │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf ├── mysql │ ├── Dockerfile │ └── my.cnf ├── docker-compose.yml └── setup.sh ポートは以下のようです。 rails 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp api-nginx 0.0.0.0:8080->80/tcp, :::8080->80/tcp react 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp front-nginx 0.0.0.0:80->80/tcp, :::80->80/tcp db 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp ReactとRailsのデフォルトポートが3000でかぶるので、今回はReactのポートを8000にしました。 このポートって慣習とかルールってあるんですかね? qiitaの記事とか見ると色んな設定ですけど、分かる方がいらっしゃればぜひ教えてください?♂️ バック まずはバックを構築していきます。はじめにRailsのコンテナ作って、MySQLを作ってからnginx作ります。 Rails Gemfile Gemfile source 'https://rubygems.org' gem 'rails', '~> 6.1.4' Dockerfile Dockerfile FROM ruby:2.6.2 RUN apt-get update -qq && \ apt-get install -y build-essential libpq-dev nodejs vim RUN mkdir /code WORKDIR /code COPY Gemfile /code/Gemfile COPY Gemfile.lock /code/Gemfile.lock RUN bundle install COPY . /code COPY entrypoint.sh /usr/bin/ ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["bundle", "exec", "puma", "-C", "/code/config/puma.rb"] よく見る構成のDockerfileだと思います。 Dockerfileの最後のコマンドは、今回pumaを使用するのでこちらのような形式となっています。 -Cオプションの後ろで実行するファイルを指定します。 今回はルートディレクトリ下の/codeに諸々のアプリケーションファイルを置くので、このような指定としています。 Dockerfile内apt-get installの最後のvimはお好みで入っているだけなのでなくても動きます。 entrypoint.sh entrypoint.sh #!/bin/bash set -e rm -f /code/tmp/pids/server.pid exec "$@" 実行 run.sh #!/bin/bash cd /code rm -f /code/tmp/pids.server.pid bundle exec puma -C /code/config/puma.rb MySQL .env MYSQL_DATABASE=server_dev MYSQL_USER=docker MYSQL_ROOT_PASSWORD=DockerMysql1.0! MYSQL_PASSWORD=DockerMysql1.0! MySQLの環境変数設定です。 今回はユーザー名 docker、パスワード DockerMysql1.0!としてMySQLに接続します。 Dockerfile Dockerfile FROM mysql:5.7 EXPOSE 3306 COPY ./my.cnf /etc/mysql/conf.d/my.cnf CMD ["mysqld"] 特に注意する点はありません。 MySQLイメージを持ってきて、3306ポートを開け、confファイルをコンテナ内にコピーし、mysqldで起動します。 my.cnf [mysqld] explicit_defaults_for_timestamp = 1 character-set-server = utf8mb4 collation-server = utf8mb4_bin [mysql] default-character-set = utf8mb4 [client] default-character-set = utf8mb4 データベースの文字設定を行っています。 nginx(Rails側) Dockerfile Dockerfile FROM nginx:1.20.1 RUN rm /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d CMD ["/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/conf.d/default.conf"] Rails側のnginxコンテナです。 nginxイメージを持ってきて、/etc/nginx/conf.d下にあるデフォルトのconfファイルを削除し、これから作成するconfファイルを置きます。 その後nginxを起動します。 default.cnf default.cnf events { worker_connections 1024; } http { upstream rails { server qiita-rails:3000; } server { listen 80; server_name localhost; root /code/public; location / { try_files $uri @app; } location @app { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://rails; } } } nginxのconfファイルです。 今回はバックエンドでnginxコンテナに来たリクエストをRailsコンテナに流すので、こちらの設定を行います。 upstreem内でrailsコンテナ(これからdocker-compose.yml内で設定するサービス名qiita-railsの3000番ポートサーバー)を設定し、locationのproxy_passで流します。 docker-compose.yml docker-compose.yml version: "3.4" services: qiita-db: build: ./mysql/ container_name: qiita_db environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - db-data:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping --host=127.0.0.1 --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} --port=3306"] ports: - "3306:3306" qiita-rails: build: ./api/server/ container_name: wt4u_api_rails command: ["./run.sh"] container_name: qiita_rails env_file: - ./api/server/.env volumes: - ./api/server/:/code/ - public:/code/public/ environment: TZ: Asia/Tokyo RAILS_ENV: development depends_on: qiita-db: condition: service_healthy ports: - "3000:3000" healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] qiita-api-nginx: build: ./api/nginx/ container_name: wt4u_api_nginx volumes: - public:/code/public/ ports: - "8080:80" depends_on: qiita-rails: condition: service_healthy volumes: db-data: public: バック+DBまでのdocker-composeです。 特に変わった点はないですが、注意点としてqiita-db/qiita-rails両サービスにヘルスチェックを設定します。 depends_onで設定できるのはコンテナの起動順のみですので、コンテナ起動後アプリケーションが起動してから新たなコンテナを起動するように設定します。 起動順はdb→rails→nginxです。 DBの方はなくてもいけましたが、Railsのサービスの方はその後のnginxのほうが早く立ち上がり、リクエストを流す先がないエラーとなってしまうことを防ぐために設定しています。 内容としては、DBの方はmysqladminコマンドで接続できるかどうか、Railsの方はcurlコマンドでレスポンスが帰ってくるかどうかです。 詳細はDockerのドキュメントに書かれているのでこちらを参照してください。 コマンドだけでなく試行回数やインターバルなども設定できます。 またDBのデータを永続化するためvolumeを設定します。 そしてローカルの編集をコンテナへ反映させるためapi/serverをマウントします。これでコンテナ内でbundle installしたものなどが永続化できます。 さらにRailsコンテナとnginxコンテナでpublicディレクトリを共有するため同様にvolumeを作成します。 フロント 続いてフロント側のコンテナ達を作成します。 React Dockerfile FROM node:12.16.1 RUN apt-get update && \ apt-get install -y vim COPY run.sh / RUN mkdir /code WORKDIR /code EXPOSE 8000 CMD ["PORT=8000", "yarn", "start"] これもよくあるReactのDockerfileです。 注意点として、今回React側のポートを8000にするので、yarn startコマンドでポートを指定します。 run.sh #!/bin/bash cd /code PORT=8000 yarn start 上と同じくです。 nginx(React側) Dockerfile FROM nginx:1.20.1 RUN rm /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d CMD ["/usr/sbin/nginx", "-g", "daemon off;", "-c", "/etc/nginx/conf.d/default.conf"] React側のnginxのDockerfileはRails側と同じです。 default.cnf events { worker_connections 1024; } http { upstream react { server qiita-react:8000; } server { listen 80; location / { proxy_pass http://react; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; } location /sockjs-node { proxy_pass http://react; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } } confもRails側と同じです。今回Reactのコンテナ名をqiita-reactとするのでupstreamで設定します。 docker-compose.yml docker-compose.yml version: "3.4" services: qiita-db: build: ./mysql/ container_name: qiita_db environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - db-data:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping --host=127.0.0.1 --user=${MYSQL_USER} --password=${MYSQL_PASSWORD} --port=3306"] ports: - "3306:3306" qiita-rails: build: ./api/server/ container_name: wt4u_api_rails command: ["./run.sh"] container_name: qiita_rails env_file: - ./api/server/.env volumes: - ./api/server/:/code/ - public:/code/public/ environment: TZ: Asia/Tokyo RAILS_ENV: development depends_on: qiita-db: condition: service_healthy ports: - "3000:3000" healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] qiita-api-nginx: build: ./api/nginx/ container_name: wt4u_api_nginx volumes: - public:/code/public/ ports: - "8080:80" depends_on: qiita-rails: condition: service_healthy qiita-react: build: ./frontend/ container_name: qiita_react volumes: - ./frontend/front/:/code/ ports: - "8000:8000" command: ["/run.sh"] healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8000 || exit 1"] qiita-front-nginx: build: ./frontend/nginx/ container_name: qiita_front_nginx ports: - "80:80" depends_on: qiita-react: condition: service_healthy volumes: db-data: public: docker-composeの最終的な構成です。 フロント側もnginxを立てるので同様にヘルスチェックを行い起動を確認後nginxのコンテナを起動します。 RailsもReactもコンテナ内の/codeディレクトリにコードを一式置きます。 volumesの設定でマウントしているので、コンテナ内のbundle(yarn) installやローカルでの編集が同期されます。 立ち上げ Build $ docker-compose build Rails $ docker-compose run qiita-rails rails new . --api -d mysql --force --path vendor/bundle Railsのアプリケーションを新規作成します。 今回はRailsをAPIモードで使用し、データベースにMySQLを使用するのでオプションで指定します。 またbundle installしたgem達を永続化したいので、/code/vendor/bundleにインストールしvolumeの対象となるようにするため--pathオプションで指定します。 Rails DB接続情報 database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("MYSQL_USER") %> password: <%= ENV.fetch("MYSQL_PASSWORD") %> host: qiita-db development: <<: *default database: <%= ENV.fetch("MYSQL_DATABASE") %> RailsのDB接続情報を設定します。 hostにDBのコンテナ名を指定します。 React $ docker-compose run qiita-react npx create-react-app . npx create-react-appでReactアプリケーションを作成します。 起動 $ sudo chmod -R a=rx,u+wx . $ docker-compose up --build dockerがsudo権限で実行されている場合、コンテナ内で作成されvolumeでローカルにマウントされたRails、Reactアプリケーションファイルには実行権限がありません(でした)。 ユーザーに実行権限を設定するため1つ目のコマンドを実行します。 最後にdocker-compose up --buildを実行することですべてのコンテナたちが立ち上がります。 localhostの3000、8080ポートでRailsのトップが、8000、80ポートでReactのトップページが表示されます。 最後に 拙い文章でしたが最後までお読みいただきありがとうございます。 修正/加筆依頼/アドバイスは大歓迎です! qiitaでもissueでもぜひお願いします。 ちょこちょこ記事書きたいと思っているのでぜひよろしくお願いします。
- 投稿日:2021-10-31T20:56:21+09:00
【個人開発】チーム開発にしてみたらメリットいっぱいな話
結論 個人開発の目的が 楽しみたい or 勉強のためにならチーム開発するべき ターゲット 1人で個人開発をしてる人 駆け出しエンジニア マネジメント力をつけたいと思っている人 筆者経験 2ヶ月かけて個人開発アプリを作った 現在、計6人体制でチーム開発を行なって2ヶ月目! 作ってるアプリ アイデアとエンジニアのマッチングアプリ これまでの背景 駆け出しエンジニアは増えている 一番の鬼門である就職の難易度UP → お金を払ってでも実務経験を得るサービスなんてのが出てきた。。? 正直な感想? うわ〜、きち〜です ツイートからの募集 【#駆け出しエンジニア 募集】✅コードレビューします✅チーム開発経験できます変な勧誘は一切しませんwすべて無料最大2名まで私と一緒にチーム開発してみませんか?詳細はDMください?♂️#駆け出しエンジニアと繋がりたい pic.twitter.com/3gv5dkoQys— なる(ハチドリの一滴)個人開発の人 (@1026NT) September 9, 2021 スモールで始めたメンター 友達に教えたり、仕事でならあるけども、 公募の方に対しては初めてだったので 最初は1人だけのメンターを開始 すぐに感じるメリット 早速1人チーム開発に加わって頂きました!?本日からのスタートですが既にメリット感じる✅チケット管理の質が上がった✅自分のリソース管理も必要性&質も上がる✅分報などを始めて個人開発の寂しさは無くなった人の目があると綺麗なコードを書こうと良いプレッシャーがかかる感じが良い?♂️ https://t.co/bGR9rS4DSv— なる?個人開発の人 (@1026NT) September 10, 2021 1人でやってた頃の自分? チケット管理が雑で計画的に動けてなかった 読みやすいコードかどうかは自分が書いてるから判断できない 今動けば良いコードを書いていた → 技術負債 変化 Slackでのチーム体制基盤を整理 → 通知などの自動化 チケットを細かく → 全体を管理しやすくなり計画性向上 教えるタスクが入り、時間管理&メンターのスキルが強制向上 2回目の募集 【#駆け出しエンジニア 募集】「めちゃくちゃ需要あると思いますよ」ありがたいことをメンティーに言われ、再募集します!?現在すべて無料やってるメンターon個人開発✅コードレビュー✅実務ベースな開発方法でのチーム開発最大2名まで詳細はDMください?♂️#駆け出しエンジニアと繋がりたい pic.twitter.com/lDMfaRl2ZB— なる?個人開発の人 (@1026NT) September 30, 2021 計5名をメンター 【満員御礼】#駆け出しエンジニア を2名だけ募集していたメンターon個人開発250%応募の結果参加意欲をお聞きするとうれしくなって全員OKしちゃった?フリーランスやフロントエンジニア、転職活動中のメンバー含め現在6名体制で #個人開発..いや、チーム開発してます!! pic.twitter.com/oVr5KxRhOE— なる?個人開発の人 (@1026NT) October 5, 2021 3つの最大のメリット 自己スキルのアップ アプリの質アップ 楽しい 自己スキルのアップ メンタースキルのアップ 技術レベルのアップ 時間管理の質アップ メンタースキル 「どう伝えれば駆け出しの方に伝わるか」という難しさが学びに 人に教えることが最も質の良いインプット 技術レベル 疑似CTOの状態 全てのコードレビューをして、1行1行説明できるようにする過程が学び 時間管理 強制的にタイムマネジメントを強いられる botでの自動化を進めたり、ドキュメント化を進めた アプリの質アップ シンプルなコミット 当たり前だけどアプリの改修が進む それぞれの得意に合わせた改善提案 UIに特化した人もいれば、インフラに特化したい人もいる 彼らが自由に動くことによって知見を共有できる 自動化を進める やってること Slackでの自動化 notionでのドキュメント化 Slack事例 (base)Github連携でPRやissue、デプロイまでが通知される analyticsの情報でユーザーの状況を通知 アイデアがtwitterでシェアされると通知(IFTTT) アイデア、ユーザーの新規登録で通知(Webhook) 楽しい Weekly meetingを設定 メンバー内LT会を実施 Discordで一緒に作業 みんなで成長? 競プロなども定期で行っていく 就職チャンネルもありアドバイスし合ってる 就職するメンバーのお祝いや、それぞれの成長を応援し合う Win ✖️ Win やる意義 自分が駆け出しの頃にやりたかった 駆け出しのレベルは上がっているのにもったいない エンジニアを1人でも増やすことが社会のためになる マッチングが広まって欲しい 個人開発者リソースが足りてない 教えるのは無料で良いから手伝って欲しい 駆け出しエンジニアはチーム開発経験が少ない 現エンジニアとの開発は貴重な体験になる 結論 個人開発をチーム開発にするとより楽しく、自分のスキルアップに繋がる ぜひ駆け出しエンジニアも一緒になってチーム開発を盛り上げましょう!?♂️ おまけ メンティーアンケート 参画前に期待していたこと? 具体的なチーム開発体験 開発の進め方・チーム開発の進め方を学ぶ アプリの共同開発を通しての達成感や喜び、ここはこうするべきとか技術のアドバイスなど チームで開発することを具体的に経験できる。 Railsとチーム開発の知見。 走りはじめのサービスという部分で、学ぶ事は多そうだと思った。 参画して以外だったこと? プロジェクト立ち上げ段階で非常に裁量権が多いこと 開発環境を整える難しさ メンバーがめっちゃ増えた(良い意味) Rails以外にもGitHubやLTの勉強も含まれていた事。 GitHubはなぁなぁにやっていたところもあるので、非常に勉強になった。 LTもエンジニアとしてかなり効果的な自己学習だと思う。 何が得られているか? サービスの裏側がどの様な導線で動いているか、サービスとして機能するためにユーザー数をどう増やすかなどの施策が面白いと思った。 コードレビュー、裁量権の多い開発(issue立てて自分で解決) gitの使い方を把握出来た、メンバーからの刺激 開発に必要な知識やツールの使い方 開発についてだけでなくITスキル、知識全般の向上。協働でやる難しさと楽しさ。 自己紹介 個人開発について発信しています! Twitterもフォローください? @1026NT なる
- 投稿日:2021-10-31T19:47:25+09:00
【RSpec】Railsチュートリアル第6版 第8章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章 第7章 第8章(本記事) 事前準備 Gemfileにテストデータ作成用のFactory Botを追加 Gemfile group :development, :test do gem "factory_bot_rails" end Minitest Sessionsコントローラのテスト test/controllers/sessions_controller_test.rb require 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end ユーザーログイン処理に対するテスト test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "login with valid email/invalid password" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: @user.email, password: "invalid" } } assert_not is_logged_in? assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end end ユーザーログインのテストで使うfixture test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> テスト中のログインステータスを論理値で返すメソッド test/test_helper.rb ENV['RAILS_ENV'] ||= 'test' . . . class ActiveSupport::TestCase fixtures :all def is_logged_in? !session[:user_id].nil? end end RSpec Sessionsコントローラのテスト spec/requests/sessions_spec.rb require 'rails_helper' RSpec.describe "Sessions", type: :request do describe "GET /new" do it "returns http success" do get login_path expect(response).to have_http_status(:success) end end end ユーザーログイン処理に対するテスト spec/system/users_login_spec.rb require 'rails_helper' RSpec.describe "UsersLogins", type: :system do before do @user = FactoryBot.create(:user) end it "login with valid email/invalid password" do visit login_path expect(page).to have_current_path "/login" fill_in "Email", with: @user.email fill_in "Password", with: "invalid" click_button "Log in" expect(page).to have_current_path "/login" expect(page).to have_content("Invalid email/password combination") visit root_path expect(page).to_not have_content("Invalid email/password combination") end it "login with valid information followed by logout" do visit login_path expect(page).to have_current_path "/login" fill_in "Email", with: "michael@example.com" fill_in "Password", with: "password" click_button "Log in" expect(page).to have_current_path "/users/#{@user.id}" expect(page).to_not have_css("a", text: "Log in") expect(page).to have_css("a", text: "Log out") expect(page).to have_css("a", text: "Profile") click_on "Log out" expect(page).to have_current_path "/" expect(page).to have_css("a", text: "Log in") expect(page).to_not have_css("a", text: "Log out") expect(page).to_not have_css("a", text: "Profile") end end systemスペックにおいて、sesssion機能は使用できないらしいので、is_logged_in?メソッドはrequest specにて別途記述する。 expect(page).to have_current_path "/"でエラーが発生する。 ユーザーログインのテストで使うfactory spec/factories/users.rb FactoryBot.define do factory :user do name {"Michael Example"} email {"michael@example.com"} password {"password"} password_confirmation {"password"} end end テスト中のログインステータスを論理値で返すメソッド spec/support/support.rb module Support def is_logged_in? !session[:user_id].nil? end end RSpec.configure do |config| config.include Support end spec/supportディレクトリにサポートモジュールを作成し、is_logged_in?を追加。RSpec.configureでサポートモジュールをincludeする。 is_logged_in? spec/requests/session_spec.rb require 'rails_helper' RSpec.describe "session test", type: :request do before do @user = FactoryBot.create(:user) end it "log in and log out" do post login_path, params: { email: "michael@example.com", password: "password", } expect(response).to redirect_to user_path(@user) expect(is_logged_in?).to be_truthy delete logout_path expect(response).to redirect_to root_path expect(is_logged_in?).to be_falsey end end expect(response).to redirect_to user_path(@user)でエラーが発生する。 発生するエラーについて ユーザーログイン処理に対するテストの中のexpect(page).to have_current_path "/"のテストにおいて、 Failure/Error: expect(page).to have_current_path "/" expected "/logout" to equal "/"というエラーが発生する。 click_button "Log in"の時と違い、ルーティンの時点あるいはSessionsコントローラーがうまく動作していないようである。 click_buttonとclick_onとの動作の違いや、Bootstrapのドロップダウン機能の影響ではないことは確認済み。 is_logged_in?の中のexpect(response).to redirect_to user_path(@user)のテストにおいて、 Failure/Error: expect(response).to redirect_to user_path(@user) Expected response to be a <3XX: redirect>, but was a <422: Unprocessable Entity> というエラーが発生する。 どなたか解決策があれば是非教えていただきたい。
- 投稿日:2021-10-31T19:22:07+09:00
【Ruby silver】範囲式 フリップフロップ演算子
次のコードを実行するとどうなりますか。 10.times{|d| print d == 3..d == 5 ? "T" : "F" } 解説文 d == 3..d == 5の部分は条件式に範囲式を記述しています。 この式は、フリップフロップ回路のように一時的に真偽を保持するような挙動をとります。 詳細は、Rubyリファレンスに詳しい説明がありますのでそちらを参照してください。 初めて見た時に混乱しました。 どうゆう順番で評価するか整理して考える必要があったのでまとめます。 ⑴Rangeの復習 # ..の場合 a = (1..5).to_a => [1, 2, 3, 4, 5] # ...の場合 a = (1...5).to_a => [1, 2, 3, 4] ? ..の場合は5を含む1〜5の範囲になる。 ...の場合は4.9999...までは対象になるが5は範囲に含まない。整数で考えると1〜4が範囲になる。 ひとつずつ崩して考えて理解する 10.times {|d| print d == 3..d == 5 ? "T" : "F" } 10.times 0..9の範囲を考える => 0123456789 (d == 3)..(d == 5) dが3〜5の時に(わかりやすく括弧で囲いました。) ? "T" : "F" 条件式:trueの場合"T"を出力、falseの場合は"F"を出力。 【補足】 三項演算子: 条件式 A ? 式 B : 式 C 条件式 A が真の時は式 B が実行され、偽の時は式 C が実行される。 0123456789 345がTrueのためTが出力される 結果としてFFFTTTFFFFが出力される ソニックガーデンjnchitoさんの記事: その他参考解説:
- 投稿日:2021-10-31T18:04:02+09:00
細かいつまずいたことをメモしておく(10月編)
はじめに 2021年10月も仕事や勉強でつまづいたことをまとめて自分用のメモとしてあげておきます。 調べればすぐわかる程度で私が記事にする必要はないなと思ったもです。 今月は仕事の繁忙期を乗り切り(来月再来予定)、比較的落ち着いた1か月となりました。 しかし、Windows11が始まるなどのトラブルが起きており、それに関連した問題に対処しました。 問題 1. GoogleColabratoryでMeCab+Neologdを使いたい こちらの記事を参考にした。 インストールは以下のコマンド !apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null !git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null !echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1 !pip install mecab-python3 > /dev/null !ln -s /etc/mecabrc /usr/local/etc/mecabrc 作りながら学ぶ!PyTorchによる発展ディープラーニングで以下のNeologdのパスを指定していたがColabだと若干違うので修正。 7-1_Tokenizer.ipynb import MeCab m_t = MeCab.Tagger('-Ochasen -d /usr/lib/mecab/dic/mecab-ipadic-neologd') text = '機械学習が好きです。' print(m_t.parse(text)) ↓ import MeCab m_t = MeCab.Tagger('-Ochasen -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd') text = '機械学習が好きです。' print(m_t.parse(text)) パスは以下のコマンドで分かりました。 !echo `mecab-config --dicdir`"/mecab-ipadic-neologd" 2. AttributeError: module 'torchtext.data' has no attribute 'Field' torchtextを利用したところ AttributeError: module 'torchtext.data' has no attribute 'Field' というエラーが発生した。このエラーは新しいバージョンで起こるらしい。 解決方法としてネットにあったのは from torchtext import data ↓ from torchtext.legacy import data に変更するのだが、今のバージョンではエラーが発生する。 バージョンを下げてインストールすることで解決(?)した pip install torchtext==0.8.1 3. Railsでモデルで名前付きルーティングを使う Rails.application.routes.url_helpersを使う 4. PytorchでGPUを使えるようにしたかった 自分で持っているGPUがGTX 1650と1080Tiで、書籍を読んでいるとRTXの2000系と3000系の設定の仕方はあるが、GTXは載っていなかった。 調べたところGTXの1000番台はそもそも対応していないっぽい ただし、GTX 1650なんかは2000番台。それだけは使えた。 【GPGPU】くだすれCUDAスレ part8【NVIDIA】 [無断転載禁止]©2ch.net 5. Railsでredirect先を変数の値によって変更したい A(a_path)、B(b_path)、C(c_path)という画面があり、そこには会員一覧があるとします。 どのページかを開いて、会員を1人削除すると、destroyのアクションが実行されて、会員が削除されたあとに、削除したときに開いていたページにリダイレクトしたかったとします。 そこで、destoryにパラメータでどのページかの情報を与え、params[:page](Aならparams[:page]=a_path)で取得できるようにして、 redirect_to params[:page] のようにしましたが、うまくいきませんでした。このように変数を利用したい場合は、redirect_to action: "アクション名"をすることでできます。 params[:page]にアクション名をいれて渡します。そのあと以下のようにします。 redirect_to action: params[:page], .... action: をしていすれば文字列が使えることがわかりました。 6. jupyter_notebook_config.pyを読み込まない jupyter_notebook_config.pyをJupyterが読み込まなくなっていた。 原因はコピーの場所だった。 COPY ./config/jupyter_notebook_config.py /root/.jupyter/jupyter_notebook_config.py ↓ COPY ./settings/jupyter_notebook_config.py .jupyter/ 7. Windows11による影響 ブルースクリーン多発 Windows 10 Insider Preview 10.0.22478.1012 (rs_prerelease) をインストールしたこと起動不可に。とりあえずインストールを戻しました。 VSCodeでターミナルがおかしい。 いつもPowershellでcmdコマンドでコマンドプロンプトを立ち上げるが、なぜかコマンドが効かない。 普通にPowershellを開くと問題なくできる。 コマンドプロンプトもなぜかcondaコマンドが効かない。普通に立ち上げるとできる。 設定を同期したうえでVSCodeをインストールしなおしたところできるようになった。 ビルドの期限が切れます ビルドの期限が切れるとのことでアップデートしてもブルースクリーンになってしまった。 そこで、devチャンネルからbetaチャンネルに乗り移ったところ解決。 なぜかdevしか選択できなかったので以下のサイトを参考に変更した。 Windows 11 Insider、チャネル変更が出来ない場合の対処方法。 検索窓で「レジストリ エディタ」を開いて設定が可能。 8. Rspecでリダイレクトしてしまうからエラーが調べられない そうだ、flash[:alert]をみればいいんだった。 おわりに 今月は比較的忙しくなく、与えられたタスクをこなしながら忘年会の準備をしておりました。 このメモ書きですが、案外確認することがありまとめておくべきだなと思いました。 最近わからないことができたらTwitterで検索することが多く、たまに解決することもあるのでツイートでまとめておくのも手段だなと思いました。忙しくてもアウトプットすることは忘れずにやっていきたいです。 ついに来月から案件が再度盛り上がる予定なので、がんばっていきたいところです。それと1年半続いた在宅も終了で出社となりました。。 Windows11のビルドが10月いっぱいで対応したつもりですが、明日パソコンが起動できなくなっていませんようにと一応お祈りして終わりにしたいと思います。 参考 Google ColabにMeCabとipadic-NEologdをインストールする 第8章 p.433 AttributeError: module 'torchtext.data' has no attribute 'Field' ModuleNotFoundError: No module named 'torchtext.data.field' 【GPGPU】くだすれCUDAスレ part8【NVIDIA】 [無断転載禁止]©2ch.net Windows 11 Insider、チャネル変更が出来ない場合の対処方法。
- 投稿日:2021-10-31T14:17:49+09:00
Progate Ruby on Rails5 VI ~ VII 個人的ざっくりまとめ
emailの重複がないかチェック バリデーションでuniqueness: trueを指定 models/user.rb class User < ApplicationRecord validates :email, {uniqueness: true} end マイグレーションファイルのみ作成 データベースに変更を加えるが、モデルは必要ないとき rails g migration ファイル名をターミナルに入力 ターミナル $ rails g migration add_image_name_to_users マイグレーションファイルの仕組み テーブルの変更を加えるには、マイグレーションファイルのchangeメソッドの中に処理をかく 20170427053118_add_image_name_to_users.rb class AddImageNameToUsers < ActiveRecord::Migration[5.0] def change # 変更内容 end end カラムを追加するマイグレーションファイル add_column テーブル名, カラム名, データ型をchangeメソッドの中にかく 20170427053118_add_image_name_to_users.rb class AddImageNameToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :image_name, :string end end ※rails db:migrateはこのchangeメソッドの中身を実行するためのコマンド ターミナル $ rails db:migrate # changeメソッドの中身を実行 画像の送信 画像の送信の場合はform_tag("URL", {multipart: true})と指定する必要がある users/edit.html.erb <%= form_tag("/users/#{@user.id}/update"), {multipart: true}) do %> <p>画像</p> <input name="image" type="file"> <% end %> Rubyのコードでファイルを作成 File.write(ファイルの場所, ファイルの中身)と指定する ターミナル $ rails console > File.write("public/sample.txt", "Hello World") 画像の保存 画像を保存するためには、画像データを元に画像ファイルを作成する必要がある ↓ ファイルを作成するにはFileクラスを使う ↓ File.binwrite(ファイルの場所, ファイルの中身(画像データ))と指定する ※readメソッドを用いることで、その画像データを取得することができる users_controller.rb def update : : @user.image_name = "#{@user.id}.jpg" image = params[:image] File.binwrite("public/user_images/#{@user.image_name}", image.read) end
- 投稿日:2021-10-31T13:21:03+09:00
rails6 bootstrap5導入方法
はじめに railsチュートリアルを進めるに当たってbootstrap3を導入することがあるのですが、2周目でローカル環境で進めていることもあり、bootstrap5を使用してみようと思い、導入できるようになるまでの備忘録です。 詳しいことは分かりませんが色々な記事を参照して自分にできそうな方法で導入しました。 railsチュートリアル同様、gemを入れてsassにインポートするやり方です。 筆者の環境 Macbook Air(M1, 2020) macOS Big Aur バージョン11.4 ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [arm64-darwin20] Rails 6.1.4.1 ①gemを入れる bootstrap3では’gem bootstrap-sass’を入れていましたが、bootstrap5では2つ入れるそうです。 Gemfile gem 'bootstrap', '~> 5.1' gem 'jquery-rails' ②cssファイルをscssに変更する こちらはrailsチュートリアル同様に行いました。 ③importする railsチュートリアル同様custom.scssを新しく作り,一番上に次のように記述します。 custom.scss @import "bootstrap"; @import "bootstrap-sprockets"; @import "bootstrap";” このようにrailsチュートリアルでは書かれていますが、 @import "bootstrap-sprockets"; こちらの部分でSyntaxErrorが出てしまい動きませんでした。 他にも ・bootstrapファイルをダウンロードしてそれを参照する方法 ・htmlファイルにbootstrapのHP情報等を記載して読み込む方法 ・yarn,npmなどを使う方法 などがあるようでした。
- 投稿日:2021-10-31T09:48:58+09:00
【コードリーディング】Rails 1.0 ActiveRecordのソースコードを読んでみた
はじめに Railsポートフォリオ作成でActiveRecordを使っており、作成当時は正直内部処理がどうなっているのか分かっていませんでし、気にもしていませんでした笑 そういった中、実務案件で直接SQLを書く機会があり、ActiveRecordって実際どういう風にSQLに置換して実行しているのか気になりはじめました。 そこで、ActiveRecordのソースコードでSQLに置き換えている仕組みの部分のコードを読んで理解した部分を書き留めたいと思います。 今回読んだソースコード 今回はコードの流れをシンプルに見たかったので最新のソースコードではなくRails v1.0.0のソースコードを読みました。とは言ってもコードは約1800行もあり、読むのになかなか骨が折れました笑 その中でもController部分でよく使用するfindメソッドに絞って読んだものを記載します。 ソースコード元はこちら そもそもActiveRecordって? ActiveRecordはRailsにおいてデータベースからデータを取り出したり、データベースにデータを保存したりする役割(MVCでいうMのModelの部分)を担っています。 ActiveRecordのメリットは主に以下の2点かなと感じております。 ・簡易的な記述でSQL文を発行できる ・MySQL、PostgreSQLなど様々なデータベースに対し共通の記述で対応することが可能 O/Rマッパーと呼ばれるデータベースとプログラムを橋渡しする役目を担うものがActiveRecordに含まれているため、多様なデータベースからデータを取り出したり、データベースにデータを保存したりといった処理がスムーズに行うことができます。 早速コードを読んでいく active_record/base.rb/find def find(*args) options = extract_options_from_args!(args) #省略 case args.first when :first find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first when :all records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options)) records.each { |record| record.readonly! } if options[:readonly] records else return args.first if args.first.kind_of?(Array) && args.first.empty? expects_array = args.first.kind_of?(Array) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] ids = args.flatten.compact.uniq case ids.size when 0 raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}" when 1 if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" })) return expects_array ? [ result ] : result else raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}" end else ids_list = ids.map { |id| sanitize(id) }.join(',') result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"})) if result.size == ids.size return result else raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}" end end end end 最初の部分はallやfirstなど、find内に記載した引数に対し条件分岐するコードが記載されていました。 はじめにextract_options!で可変長引数(柔軟な引数指定)を受け取れるメソッドを使い引数を受け取りoptionsへ代入 :all、:firstを引数としており、:allの中でさらに分岐が行われている模様。 ・:firstの場合 inclued(モジュールを呼び出す)されなければ、optionsに{ :limit => 1 }を代入し、:allへ id指定の場合はconditionsに"#{table_name}.#{primary_key} = #{id}"を追加し:allへ ・:allでやっていること :includeオプションありの場合find_with_associations(options)へ :includeオプションなしの場合find_by_sql(construct_finder_sql(options))へ 今回はfind_by_sql(construct_finder_sql(options))を読み進めていきたいと思います。 find_by_sqlメソッド まずはfind_by_sql(construct_finder_sql(options))のfind_by_sqlメソッドから active_record/base.rb/find_by_sql(sql) def find_by_sql(sql) connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } end find_byは今でも残る重要なメソッドですよね!なんだか知ってるメソッドがあるだけで嬉しい気持ちになりました笑 find_byメソッドはid以外のカラムを検索条件としたい場合に使用しますが、まさにカラムをとってきて返している処理が書かれています。 ここで気になったのはsanitize_sqlとinstantiate sanitize_sqlメソッド 公式ドキュメントはこちら これによるとサニタイズした値を入れることによって意図しないSQLの挙動を防ぐことができるみたいですね。このメソッドはSQLインジェクション攻撃対策のために使用されています。 サニタイズってなに? 特別な意味を持つ文字の特別さを無効化する処理のこと SQLインジェクションとは? SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。 Rails セキュリティガイド - Railsガイド 不正な「SQL」の命令を注入し、意図しないSQL文を発行し攻撃すること。 例えばDELETE文を発行され、データを全て消されてしまうなど対策をしないとセキュリティ的に非常に危険。 instantiate instantiateは読み進めているとメソッドとして定義している箇所がありました。 active_record/base.rb/instantiate def instantiate(record) object = if subclass_name = record[inheritance_column] if subclass_name.empty? allocate else require_association_class(subclass_name) begin compute_type(subclass_name).allocate rescue NameError raise SubclassNotFound, "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + "or overwrite #{self.to_s}.inheritance_column to use another column for that information." end end else allocate end object.instance_variable_set("@attributes", record) object end ほぼ読む必要はないがallocateによって取得したものが新しいレコードかを判断するメソッドと理解しました。レコードに保存されていないカラムや値の場合はエラーを吐き出す仕様になっている模様。 find_by_sqlメソッド総括 総括してfind_by_sqlメソッドはサニタイズした値を入れ込む事によりSQLインジェクション攻撃を防ぎつつ、取ってきたレコードを返す。レコードはinstantiateにより既存のレコードかNewレコード化を判断し、場合によりエラーを吐き出す仕様になっていると言う感じですかね。 construct_finder_sqlメソッド 続いてfind_by_sql(construct_finder_sql(options))のconstruct_finder_sqlメソッドについて読み進めました。 active_record/base.rb/construct_finder_sql def construct_finder_sql(options) sql = "SELECT #{options[:select] || '*'} FROM #{table_name} " add_joins!(sql, options) add_conditions!(sql, options[:conditions]) sql << " GROUP BY #{options[:group]} " if options[:group] sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options) sql end ここは結構SQL文そのままって印象でした。用途によっていろんなSQL文を発行しテーブルからデータを参照している印象です。 保存、更新 最後にfindとは関係なくなってしまいますがActiveRecordによる保存、更新処理部分を読み進めて終わりたいと思います。 saveメソッド active_record/base.rb/save def save raise ActiveRecord::ReadOnlyRecord if readonly? create_or_update end saveメソッドによりcreateやupdateを呼び出しています。 createメソッド active_record/base.rb/save def create if self.id.nil? and connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end self.id = connection.insert( "INSERT INTO #{self.class.table_name} " + "(#{quoted_column_names.join(', ')}) " + "VALUES(#{attributes_with_quotes.values.join(', ')})", "#{self.class.name} Create", self.class.primary_key, self.id, self.class.sequence_name ) @new_record = false end 愚直にテーブル名、カラム名、プライマリーキー、idなどをSQLで挿入していますね。 updateメソッド active_record/base.rb/update def update(id, attributes) if id.is_a?(Array) idx = -1 id.collect { |id| idx += 1; update(id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) object end end ここでidから検索しidとattributes(Modelの属性全て)を渡して更新しています。 検証中に失敗した場合はオブジェクトを返しています。 attributesについてはこちらが参考になりました。 あとがき 今回は初めてOSS(オープンソースソフトウェア)を読んでみました。 ソースコードを辿ってみた結果愚直にSQLを発行していたと言うことがわかりました。 自分で作成したポートフォリオの比じゃないコード量を読み進めていくのは大変でしたが一つ一つメソッドを辿ってく内に理解していく感覚がすごく楽しかったです。 企業の自社サービスのコードはこれの比じゃないくらい膨大な記述量になっているとは思いますが、早くコードを読んでみたいと言う願望が高まっています。 これからも挙動の根幹をコードを読み進めて理解していきたいと思います。 最後までご覧いただきありがとうございました!!
- 投稿日:2021-10-31T04:07:32+09:00
【RSpec】Railsチュートリアル第6版 第7章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章 第7章(本記事) Minitest ユーザー登録に対するテスト test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' end test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' end end RSpec ユーザー登録に対するテスト spec/system/users_signup_spec.rb require 'rails_helper' RSpec.describe "UsersSignups", type: :system do it "invalid signup information" do visit root_path click_on "Sign up" expect { fill_in "Name", with: "" fill_in "Email", with: "user@invalid" fill_in "Password", with: "foo" fill_in "Confirmation", with: "bar" click_button "Create my account" }.to_not change(User, :count) expect(page).to have_current_path "/users" expect(page).to have_content("Name can't be blank") expect(page).to have_content("Email is invalid") expect(page).to have_content("Password confirmation doesn't match Password") expect(page).to have_content("Password is too short (minimum is 6 characters)") end it "valid signup information" do visit root_path click_on "Sign up" expect { fill_in "Name", with: "Example User" fill_in "Email", with: "user@example.com" fill_in "Password", with: "password" fill_in "Confirmation", with: "password" click_button "Create my account" }.to change(User, :count).by(1) expect(page).to have_current_path "/users/#{User.last.id}" expect(page).to have_content("Welcome to the Sample App!") end end click_onでユーザー登録ページに移動し、fill_inでフォームに情報を入力した後、click_buttonで情報を送信する。 expect(page).to have_current_pathで正しいページに移動しているかチェックし、expect(page).to have_contentでエラーメッセージとフラッシュがページに存在しているかチェック。
- 投稿日:2021-10-31T03:39:51+09:00
【RSpec】Railsチュートリアル第6版 第6章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章(本記事) 第7章 Minitest Userテスト test/models/user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end test "should be valid" do assert @user.valid? end test "name should be present" do @user.name = "" assert_not @user.valid? end test "email should be present" do @user.email = " " assert_not @user.valid? end test "name should not be too long" do @user.name = "a" * 51 assert_not @user.valid? end test "email should not be too long" do @user.email = "a" * 244 + "@example.com" assert_not @user.valid? end test "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] valid_addresses.each do |valid_address| @user.email = valid_address assert @user.valid?, "#{valid_address.inspect} should be valid" end end test "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end test "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save assert_not duplicate_user.valid? end test "password should be present (nonblank)" do @user.password = @user.password_confirmation = " " * 6 assert_not @user.valid? end test "password should have a minimum length" do @user.password = @user.password_confirmation = "a" * 5 assert_not @user.valid? end end RSpec Userテスト spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do before do @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end it "should be valid" do expect(@user).to be_valid end it "name should be present" do @user.name = "" @user.valid? expect(@user.errors[:name]).to include("can't be blank") end it "email should be present" do @user.email = "" @user.valid? expect(@user.errors[:email]).to include("can't be blank") end it "name should not be too long" do @user.name = "a"*51 @user.valid? expect(@user.errors[:name]).to include("is too long (maximum is 50 characters)") end it "email should not be too long" do @user.email = "a" * 244 + "@example.com" @user.valid? expect(@user.errors[:email]).to include("is too long (maximum is 255 characters)") end it "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] valid_addresses.each do |valid_address| @user.email = valid_address expect(@user).to be_valid end end it "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address @user.valid? expect(@user.errors[:email]).to include("is invalid") end end it "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save duplicate_user.valid? expect(duplicate_user.errors[:email]).to include("has already been taken") end it "password should be present (nonblank)" do @user.password = @user.password_confirmation = " " * 6 @user.valid? expect(@user.errors[:password]).to include("can't be blank") end it "password should have a minimum length" do @user.password = @user.password_confirmation = "a" * 5 @user.valid? expect(@user.errors[:password]).to include("is too short (minimum is 6 characters)") end end model specが自動で作成されていなければrails g rspec:model userで作成。 expect(@user.errors[:?]).to include("?")でエラーメッセージをテスト。
- 投稿日:2021-10-31T02:20:44+09:00
【RSpec】Railsチュートリアル第6版 第5章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章(本記事) 第6章 第7章 事前準備 Gemfileにsystem spec用のcapybaraを追加 Gemfile group :test do gem 'capybara' end ファイル内容の変更 spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end config.before(:each, type: :system, js: true) do driven_by :selenium_chrome_headless end end Minitest StaticPagesコントローラーのテスト test/controllers/static_pages_controller_test.rb require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest def setup @base_title = "Ruby on Rails Tutorial Sample App" end test "should get home" do get root_path assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do get help_path assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do get about_path assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end test "should get contact" do get contact_path assert_response :success assert_select "title", "Contact | Ruby on Rails Tutorial Sample App" end end レイアウトのリンクに対するテスト test/integration/site_layout_test.rb require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest test "layout links" do get root_path assert_template 'static_pages/home' assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path end end RSpec StaticPagesコントローラーのテスト spec/requests/static_pages.rb require 'rails_helper' RSpec.describe "StaticPages", type: :request do let(:base_title) { 'Ruby on Rails Tutorial Sample App' } describe "GET root" do it "returns http success" do get root_path expect(response).to have_http_status(:success) assert_select "title", "#{base_title}" end end describe "GET /help" do it "returns http success" do get help_path expect(response).to have_http_status(:success) assert_select "title", "Help | #{base_title}" end end describe "GET /about" do it "returns http success" do get about_path expect(response).to have_http_status(:success) assert_select "title", "About | #{base_title}" end end describe "GET /contact" do it "returns http success" do get contact_path expect(response).to have_http_status(:success) assert_select "title", "Contact | #{base_title}" end end end Contactページのテストを追加 名前付きルートを使用するよう変更 レイアウトのリンクに対するテスト spec/system/site_layout_spec.rb require 'rails_helper' RSpec.describe "SiteLayouts", type: :system do it "layout links" do visit root_path expect(page).to have_current_path "/" expect(page).to have_link nil, href: root_path, count: 2 expect(page).to have_link 'Help', href: help_path expect(page).to have_link 'About', href: about_path end end system specを作成し、レイアウトの各リンクをテスト。
- 投稿日:2021-10-31T02:10:34+09:00
【RSpec】Railsチュートリアル第6版 第4章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章(本記事) 第5章 第6章 第7章 Minitest StaticPagesコントローラーのテスト test/controllers/static_pages_controller_test.rb require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest def setup @base_title = "Ruby on Rails Tutorial Sample App" end test "should get home" do static_pages_home_url assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do static_pages_help_url assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do static_pages_about_url assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end end RSpec StaticPagesコントローラーのテスト spec/requests/static_pages.rb require 'rails_helper' RSpec.describe "StaticPages", type: :request do let(:base_title) { 'Ruby on Rails Tutorial Sample App' } describe "GET /home" do it "returns http success" do get "/home" expect(response).to have_http_status(:success) assert_select "title", "#{base_title}" end end describe "GET /help" do it "returns http success" do get "/help" expect(response).to have_http_status(:success) assert_select "title", "Help | #{base_title}" end end describe "GET /about" do it "returns http success" do get "/about" expect(response).to have_http_status(:success) assert_select "title", "About | #{base_title}" end end end /homeで基本タイトルのみを表示するよう変更。