Rustでwebアプリを実装して勉強 その3 - テストコードを書く

Rust初心者が勉強したことを記録する備忘録。

github.com

今日やった事

参考ページ

単体テストを書く

Rustではテストコードを同じファイル内に書く文化らしい。

まずは、 src/models/post.rs にDBに関係しないところを試しに書いてみた。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn unit_sample() {
        let post = Post {
            id: 1,
            title: "title".to_string(),
            body: "body".to_string(),
            published: false,
        };

        assert_eq!(post.id, 1);
        assert_eq!(post.title, "title");
        assert_eq!(post.body, "body");
        assert_eq!(post.published, false);
    }
}

DB周りはテスト実行後にロールバックとかする必要あるので、また別の日に挑戦したい。

結合テストを書く

tests/main.rs を作成し、HTTPリクエストを投げて、 200 OK などを確認する最低限のテストを書いた。

#[cfg(test)]
mod tests {
    extern crate rust_web;

    use actix_web::dev::Service;
    use actix_web::http::StatusCode;
    use actix_web::{test, App};
    use bytes::Bytes;
    use rust_web::routes;

    #[test]
    fn get_root() {
        let mut app =
            test::init_service(App::new().configure(routes::top).configure(routes::posts));
        let req = test::TestRequest::get().uri("/").to_request();
        let resp = test::block_on(app.call(req)).unwrap();

        assert_eq!(resp.status(), StatusCode::OK);
        assert_eq!(test::read_body(resp), Bytes::from_static(b"Hello world!"));
    }

    #[test]
    fn post_posts() {
        let mut app =
            test::init_service(App::new().configure(routes::top).configure(routes::posts));
        let req = test::TestRequest::post().uri("/posts").to_request();
        let resp = test::block_on(app.call(req)).unwrap();

        assert_eq!(resp.status(), StatusCode::CREATED);
        assert_eq!(test::read_body(resp), Bytes::from_static(b"Inserting"));
    }
}

CircleCIを設定する

テストコードを書いたのでCircleCIの設定もしておいた。

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/rust
        environment:
          DATABASE_URL: test.sqlite3
    steps:
      - checkout
      - run: cargo fmt -- --check
      - run: cargo install diesel_cli
      - run: diesel setup
      - run: cargo test

次にやりたいこと

  • SQLに関する処理をModelに移す(もしくはRepositoryを作る)
  • テストコードを書く
  • jsonを返すように直す
  • テンプレートエンジンを使ってhtmlを返す

Rustでwebアプリを実装して勉強 その2 - SQLの処理をモデルに書く

Rust初心者が勉強したことを記録する備忘録。

github.com

今日やったこと

main.rs に処理を全部書いて微妙だったので、MVCっぽい感じでディレクトリ構造を整理した。

独学なので、この構成で良いのかは全く自信がない。

ディレクトリ構成

ルーティング、モデル、コントローラーが別ファイルになるようにしている。

src
├── controllers
│   ├── mod.rs
│   ├── posts.rs
│   └── top.rs
├── lib.rs
├── main.rs
├── models
│   ├── mod.rs
│   ├── post.rs
│   └── util.rs
├── routes.rs
└── schema.rs

ルーティング

ルーティングを縦に列挙するのは辛いので、リソース単位で関数化した。

fn main() {
    HttpServer::new(|| App::new().configure(routes::top).configure(routes::posts))
        .bind("127.0.0.1:8088")
        .unwrap()
        .run()
        .unwrap();
}

routes.rs は以下のような感じで実装してある。

use crate::controllers::{posts, top};
use actix_web::web;

pub fn top(cfg: &mut web::ServiceConfig) {
    cfg.route("/", web::get().to(top::index));
}

pub fn posts(cfg: &mut web::ServiceConfig) {
    cfg.route("/posts", web::get().to(posts::index))
        .route("/posts/{id}", web::get().to(posts::show))
        .route("/posts", web::post().to(posts::create))
        .route("/posts/{id}", web::put().to(posts::update))
        .route("/posts/{id}", web::patch().to(posts::update))
        .route("/posts/{id}", web::delete().to(posts::destroy));
}

コントローラー

Rails のコントローラーのような感じでメソッドを作っている。

use crate::models::Post;
use actix_web::{HttpRequest, HttpResponse, Responder};

pub fn index() -> impl Responder {
    let results = Post::all();
    let res = format!("Displaying {} posts", results.len());

    HttpResponse::Ok().body(res)
}

pub fn show(req: HttpRequest) -> impl Responder {
    let post = find_post(req);
    let res = format!("Show {}", post.id);

    HttpResponse::Ok().body(res)
}

pub fn create() -> impl Responder {
    Post::create("title", "body");

    HttpResponse::Created().body("Inserting")
}

pub fn update(req: HttpRequest) -> impl Responder {
    let mut post = find_post(req);
    post.publish();

    HttpResponse::Ok().body("Published")
}

pub fn destroy(req: HttpRequest) -> impl Responder {
    let post = find_post(req);
    post.destroy();

    HttpResponse::NoContent()
}

fn find_post(req: HttpRequest) -> Post {
    let id: i32 = req.match_info().get("id").unwrap().parse().unwrap();
    Post::find(id)
}

モデル

main.rsにあった処理をActiveRecordっぽいAPIで使えるように実装した。

ただ、細かいところは適当。

use super::util::establish_connection;
use crate::schema::posts;
use crate::schema::posts::dsl;
use diesel::prelude::*;

#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

#[derive(Insertable)]
#[table_name = "posts"]
struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
}

impl Post {
    pub fn all() -> Vec<Post> {
        let connection = establish_connection();
        dsl::posts
            .filter(dsl::published.eq(true))
            .limit(5)
            .load::<Post>(&connection)
            .expect("Error loading posts")
    }

    pub fn find(id: i32) -> Post {
        let connection = establish_connection();
        dsl::posts
            .find(id)
            .first::<Post>(&connection)
            .expect("Error finding posts")
    }

    pub fn create(title: &str, body: &str) {
        let new_post = NewPost {
            title: title,
            body: body,
        };
        let connection = establish_connection();
        diesel::insert_into(posts::table)
            .values(&new_post)
            .execute(&connection)
            .expect("Error saving new post");
    }

    pub fn publish(&mut self) {
        let connection = establish_connection();
        let num_updated = diesel::update(dsl::posts.find(self.id))
            .set(dsl::published.eq(true))
            .execute(&connection)
            .expect(&format!("Unable to find post {}", self.id));

        if num_updated > 0 {
            self.published = true;
        }
    }

    pub fn destroy(&self) {
        let connection = establish_connection();
        diesel::delete(dsl::posts.find(self.id))
            .execute(&connection)
            .expect("Error deleting posts");
    }
}

次にやりたいこと

  • SQLに関する処理をModelに移す(もしくはRepositoryを作る)
  • テストコードを書く
  • jsonを返すように直す
  • テンプレートエンジンを使ってhtmlを返す

Rustでwebアプリを実装して勉強 その1 - sqliteを使う

Rustを勉強するためにactix-webを使ってwebアプリを書いてみた備忘録。

github.com

今日やったこと

Rust初心者で何も分からないので、actix-web と diesel のGetting Startedを読みながら、雰囲気でコードを書いてみた。

diesel_cli のインストール

DBの作成・マイグレーションなどをするため、cliをインストールする必要がある。

$ cargo install diesel_cli

で普通にインストールできた。 もし postgresql とかがローカルになければ、sqlite だけのインストールもできるらしい。

$ cargo install diesel_cli --no-default-features --features sqlite

DBの作成

DATABASE_URLを指定して diesel setup するとDBが作れる。

$ DATABASE_URL=development.sqlite3 diesel setup

毎回指定するのも面倒なので、.envrc に入れておく。

$ echo export DATABASE_URL=development.sqlite3 > .envrc

マイグレーションファイルの作成

diesel migration generate で作成できる。

$ diesel migration generate create_posts
Creating migrations/2019-10-27-152149_create_posts/up.sql
Creating migrations/2019-10-27-152149_create_posts/down.sql

ActiveRecordみたいにRubyではなくて、生SQLだった。この潔さは嫌いじゃない。

DBのマイグレーション

diesel migration runマイグレーションを実行してくれる。

$ diesel migration run
Running migration 2019-10-27-152149_create_posts

diesel migration redo で down.sql の動作確認もできる。

DBの接続

こんな感じで connection を作る。

pub fn establish_connection() -> SqliteConnection {
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    SqliteConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
}

コネクションの管理を diesel でやっているのか、自分でやる必要あるのかはよく分かっていない。

複数のPostを取得する

こんな感じでfilterやlimitが書けるらしい。たぶんorderとかも使える気がする。

let results = posts
    .filter(published.eq(true))
    .limit(5)
    .load::<Post>(&connection)
    .expect("Error loading posts");

1つのPostを取得する

findのあとにfirstを書くのが冗長感あるけど、たぶん posts.find(id) がWHERE句になるんだと思う。

let post = posts.find(id)
    .first::<Post>(&connection)
    .expect("Error finding posts");

Postを登録する

insert_intoを使う。

ちなみにdieselのGetting Started だとget_resultを使っているけど、sqliteは対応してないので代わりにexecuteを使っている。

let new_post = NewPost {
    title: "title",
    body: "body",
};
let connection = establish_connection();
diesel::insert_into(posts::table)
    .values(&new_post)
    .execute(&connection)
    .expect("Error saving new post");

Postを更新する

Getting Startedを参考にして書いたら、普通に動いた。

diesel::update(posts.find(id))
    .set(published.eq(true))
    .execute(&connection)
    .expect(&format!("Unable to find post {}", id));

Postを削除する

これもドキュメント通り。

diesel::delete(posts.find(id))
    .execute(&connection)
    .expect("Error deleting posts");

ルーティング周り

それっぽく書いたら、動いた。

HttpServer::new(|| {
    App::new()
        .route("/", web::get().to(index))
        .route("/posts", web::get().to(posts_index))
        .route("/posts/{id}", web::get().to(posts_show))
        .route("/posts", web::post().to(posts_create))
        .route("/posts/{id}", web::put().to(posts_update))
        .route("/posts/{id}", web::patch().to(posts_update))
        .route("/posts/{id}", web::delete().to(posts_destroy))
})

次にやりたいこと

  • SQLに関する処理をModelに移す(もしくはRepositoryを作る)
  • テストコードを書く
  • jsonを返すように直す
  • テンプレートエンジンを使ってhtmlを返す

Railsアプリの開発環境を速くするための調査 その1 - gemの読み込み時間

その1って書いたけど、続くかは不明。

今回は使っているgemの読み込み時間を測ってみた。

Benchmark を仕込む

config/application.rb でgemを読み込む前に Kernel.require を上書きして、計測する。

+require 'benchmark'
+$result = {}
+Kernel.singleton_class.prepend(Module.new do
+  def require(feature)
+    ret = nil
+    $result[feature] = Benchmark.realtime { ret = super }
+    ret
+  end
+end)
 Bundler.require(*Rails.groups)
+$result.sort_by { |_, t| -t }.take(20)
+  .each { |feature, time| puts "#{format("%0.3f", time)}: #{feature}" }

実際に測ってみる

tootsuite/mastodon で試してみた。

$ bin/rails runner nil
0.486: chewy
0.228: pry-byebug
0.167: charlock_holmes
0.161: fog/core
0.110: fuubar
0.105: devise-two-factor
0.101: json/ld/preloaded
0.098: omniauth-saml
0.085: goldfinger
0.077: json/ld
0.064: hamlit-rails
0.064: paperclip
0.058: health_check
0.050: fabrication
0.049: twitter-text
0.046: rqrcode
0.042: parslet
0.039: annotate
0.035: pghero
0.034: iso-639

こんな感じで読み込みに時間がかかっているgemが分かる。

Rails の issue を解決するまでの手順とOSS初心者でもできること

突然ですが、あなたはRailsのissueとプルリクがいくつあるかご存知でしょうか?

2019年10月17日現在、それぞれ issue 384 / PR 803 になります。

f:id:sinsoku:20191017000221p:plain

多いですよね...。

個人的に、最近このissueを減らすのを少しでも手伝えないものかとissueにコメントしてみたり、パッチを書いたりしてるけど、 なかなか大変なので、コントリビューターの敷居を下げるためにブログ記事を書いてみました。

コントリビュータが増えれば、きっとissueも減るはず!!

Rails への貢献について

Railsガイドに丁寧な説明が記載されているので、読んだ事がない方は一読するのをオススメします。

railsguides.jp

この記事で紹介すること

Rails への貢献方法は色々なものがあります。

  • 新機能の追加
  • バグの報告
  • バグを修正するプルリク作成
  • ドキュメントの追加や修正
  • ...etc

このなかのうち バグの修正 に絞って紹介します。

バグ修正を行うときの手順

  1. 報告されたissueを読んで、問題を手元で再現させる
  2. 原因を突き止める
  3. コードを直す
  4. 動作確認をする
  5. プルリクを投げる

こんな感じでしょうか。 まぁ、仕事でバグを直すときと同じですね。

1. 問題を再現する

Rails の issue には問題を再現させるためのバグテンプレートが存在します。

  • issue に再現手順、再現スクリプトがない場合
    • バグテンプレートを使って書けないか issue の作者に依頼する
    • 例: rails/rails#36413
  • issue に再現スクリプトがある場合
    • 自分の環境でも再現するか確認してみる

2. 原因を調べる

問題が再現できたら、原因であるコードを調べます。

3. コードを直す

問題を再現させるテストコードを書いて、テストが失敗することを確認してから、コードを直します。 (例: rails/rails#37457) 他のテストコードを参考にすれば、意外とテストは書けます。

あと、Rails では minitest が使われているので、普段 RSpec を使っている人はテストの実行方法が分からないかも。

などを参考にしてみてください。

4. 動作確認

ローカルで適当にRailsアプリを作り、修正した Rails のコードを使うように Gemfile を直します。

- gem "rails", "~> 6.0.0"
+ gem "rails", path: "~/.ghq/github.com/rails/rails"

pathディレクトリを指定すると、そのディレクトリの gem を使えます。

あとは rails consolerails server などで動作確認をします。

5. プルリクを投げる

コミットログとプルリクに変更理由をちゃんと書く必要があります。

英語で変更理由を書くのはとても大変なので、いくつか英文を書くテクニックを紹介します。

  • 変更理由を日本語で書いて Google翻訳 でざっくり翻訳する
  • 簡単な英文(=自分が読める英文)になるように調整する
  • 他のバグ修正のプルリクを読んで、似たような英語で書き直せないか考える
  • git log --no-merges --author=kamipo でkamipoさんのコミットログを読んで参考にする

Rails のコミットログやプルリクには参考になる英文がたくさんあるので、うまく探してパクってください。

変更内容に自信がなかったり、不安な場合

都内に住んでいる方であれば、Asakusa.rb や永和さんのOSSパッチ会などに参加して、プルリクの内容について相談するという方法があります。

OSS初心者向け

ここまでで issue を解決する方法を紹介しましたが、「ハードルが高い」と思った人向けにいくつか初心者向けにできることを紹介します。

  1. https://github.com/rails/rails/issues の中から、興味のあるタイトルを読んでみる
    • issue を全部読むのは大変なので、例えば ActiveRecord や ActiveStorage など絞って読むと良い
  2. 自分の環境で問題を再現させてみる
    • 例えば、5.2 で起きる issue を「6.0 でも再現しました」とコメントするのも大事
    • 再現スクリプトのないissueに「この再現スクリプトで再現できました」とコメントするのも良い
  3. Close した issue とプルリクを読んで、コードの直し方を学ぶ
    • 問題と解答例みたいなものなので、読むだけでも勉強になります

最後に Rails Contributors の紹介

Railsで1回でもプルリクがマージされると Rails Contributors に名前が載ります。

contributors.rubyonrails.org

まだランキングに名前が載っていない方は、これに載ることを目標に Rails の issue を眺めてみてはどうでしょう?

意外と簡単に直せるバグが見つかるかもしれませんよ。

*1:過去のバージョンでは動いていて、最新のバージョンでバグっているケース

勉強用にRustのスクリプトをDockerでビルドし、即実行するbashスクリプトを書いた

Rustの勉強をするため、ちょっとしたコードをDockerでビルドして実行するスクリプトを書いたのでメモ。

コード

勉強用のディレクトリに以下の bin/exec というファイルを作り、 $ chomod +x bin/exec で実行権限をつけておく。

#!/bin/sh
set -e

ROOT=$(cd $(dirname $0)/../;pwd)
IMAGE=rust
RUST_CMD="docker run -e USER=$USER -v ${ROOT}:/app -w /app ${IMAGE}"

if [ -n "$1" ]
  then
    $RUST_CMD rustc $1
    EXEC_PATH=`basename $1 .rs`
    $RUST_CMD ./$EXEC_PATH
    rm ./$EXEC_PATH
else
  echo "Usage: bin/exec <source>"
fi

Dockerでビルドし、Docker上で実行して、実行後にバイナリを消すスクリプトです。

試したいファイルを引数に渡すと、実行できる。

$ bin/exec hello_world.rs
Hello, world!

Rustの勉強

昔にもRust入門してたなーと思ってたら、2年前にRustやってたブログが出てきた。

3日坊主にならないように、定期的に Rust 触ってブログに書きたいですね。そんな気持ちはあります。たぶん。

sinsoku.hatenablog.com

持っている技術の棚卸し 2019年

一応フリーランスなので、自分のできることを整理するためにまとめた。

(文章でまとめるの難しい...)

ソフトウェア開発

RubyRails

  • 1人で仕様の調整を行い、実装できる
  • コードレビューを通して、若手エンジニアにRailsを教えられる
  • OSSにプルリクを投げられる
  • RailsのIssue/PRを読んでいる(=最新機能をある程度知っている)
  • 勉強会に参加したり、登壇したことがある

JavaScript

  • ES2015の基本的な文法を知っている
  • jQueryを使った経験がある
  • React.jsでステートレスを意識してコンポーネントを書ける
  • Puppeteerを使ったE2Eテストを書いた経験がある

CSS

  • Bootstrap v3 の基本的な知識がある
  • BEMの基本的な知識がある
  • 他サイト、周りのデザインを参考にしてcssを組める
    • サイト全体のデザイン設計は出来ません

SQL

  • SQLの基本的な知識がある
  • パフォーマンスを意識したSQLを書ける
    • RDBMSの実行計画には詳しくありません
  • リプレイス案件でDB間のデータ移行作業の経験がある

インフラ

  • TerraformとAWSを使ってインフラ環境を構築できる
    • Route53, CF, ALB, ECS(Fargate)の構成を組める
    • GCPは未経験
  • aws-samでサーバレスAPIを組める

開発環境

  • GitHub周りの環境を整えられる
    • ブランチ保護、 Issue/PRテンプレート機能など
    • GitHub Appsを作れる
  • CI環境を構築できる
    • 静的解析(Lint)の設定ができる
    • CircleCIのWorkflow、Orbsを活用できる
  • CD環境を構築できる
    • ローリングアップデートの経験のみ
    • Blue/Greenデプロイ、カナリアデプロイは未経験
  • エラー管理にSentryなどを導入・使用したことがある
  • パフォーマンス改善にDatadogなどを導入・使用したことがある

その他

  • SEO対策の作業をした経験がある
  • ユーザーテストやUX改善について少し勉強したことがある
  • Redashを使ってKPIを表示するダッシューボードを作れる
  • リリースにあわせてGitのブランチ運用を提案できる

興味のあること

  • 開発環境を改善すること
  • お金の話(売上を増やしたり、経費を減らしたり)
  • 読みやすいコードを考えたり、書いたりすること
  • 新しい言語の習得(直近だとRust, Golang
  • エンジニアの評価・採用・組織作り