テスト初心者がRSpecで必要十分なテストを書く

今年の4月から新卒エンジニアとしてvivit株式会社で働いている氏家です。 私は現在、主にアウトドアWebメディアhinataの開発を行うmediaチームに所属しており、Railsを中心にコードを書いております。

最近、ある機能の実装のためコントローラにメソッドを追加しそのテストをRSpecで書いていたのですが、テスト(この場合は単体テスト)についての知識が乏しく、どのように書けばいいか分からず苦戦しました。 しかしこのタスクに着手しているとき、ちょうどソフトウェアテストをテーマにエンジニア研修が実施されており、自分の書いたテストのどこが不適切なのかを知ることができました。

今回は僕のようなテスト初心者がどのようなマインドでテストを書いていたのかと、必要十分なテストを書くために学んだことを話していけたらと思います。

メソッドの仕様

コントローラに追加するメソッドは「データベースのcategoriesテーブルから値を取得し、変数に格納する」という単純なものです。 このメソッドはあらゆる場面で使用されるため、ApplicationControllerに記述しています。

# frozen_string_literal: true

class ApplicationController < ActionController::Base

~~~

before_action :set_categories

~~~

def set_categories
  @categories = Category.where.not(priority: 0).order(priority: :desc).limit(6)
end

取得する条件は「priorityカラムが0ではないレコードをpriorityの降順に6つまで取得すること」です。

必要十分なテストとは

テストはただ書けばいいというものではなく、テストケースが メソッドが仕様通りの動作をするかどうか(正常時、異常時の双方) を網羅している必要があります。 さらに、1つの動作を確認するのに同じようなテストケースが複数あったり回りくどい書き方をしていても意味がなく、テストの実行時間が遅くなる原因になってしまいます。

メソッドが取りうる動作を、最低限のテストケースで網羅することが必要十分なテストといえます。

最初に書いたテスト

まず僕がテストについて理解せず書いたset_categoriesメソッドのテストがこちらです。

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ApplicationController, type: :controller do

~~~

  describe '#set_categories' do
    let!(:category1) { FactoryBot.create(:category, name: 'カテゴリ1', priority: 6) }
    let!(:category2) { FactoryBot.create(:category, name: 'カテゴリ2', priority: 5) }
    let!(:category3) { FactoryBot.create(:category, name: 'カテゴリ3', priority: 4) }
    let!(:category4) { FactoryBot.create(:category, name: 'カテゴリ4', priority: 3) }
    let!(:category5) { FactoryBot.create(:category, name: 'カテゴリ5', priority: 2) }
    let!(:category6) { FactoryBot.create(:category, name: 'カテゴリ6', priority: 1) }
    subject { controller.send(:set_categories) }

    context 'カテゴリが6つ以下の場合' do
      it 'priorityの順にすべて取得する' do
        is_expected.to match([category6, category5, category4, category3, category2, category1])
      end
    end

    context 'カテゴリが7つ以上場の場合' do
      let!(:category1) { FactoryBot.create(:category, name: 'カテゴリ1', priority: 8) }
      let!(:category7) { FactoryBot.create(:category, name: 'カテゴリ7', priority: 7) }
      it 'priorityの順で6つだけ取得する' do
        is_expected.to match([category6, category5, category4, category3, category2, category7])
      end
    end
  end
end

僕が必要だと感じたテストケースは、降順に取得できるかどうか7つ以上取得していないかどうかの2つでした。

コードを順番に見ていきます。

前提

まずFactoryBot.createでカテゴリを6つ作成します。テストケース毎に作成させたかったためlet!としています。

テストケース1

1つ目のテストケースではカテゴリがpriorityの降順で取得されているか確認したかったので、matchマッチャで順番通りに格納されているか確認します。

テストケース2

2つ目のテストケースではメソッドの.limit(6)が正常に動作しているか確認するため、追加でcategory7を作成しました。その際、category1priorityの値を変えて新しく作成することで、降順で取得されているかどうかを確実なものにしようと考えました。

エンジニア研修で学んだ内容と先輩エンジニアのフィードバックから、このテストはテストケースが不十分であり、かつ書き方にもいくつか問題があることがわかりました。 特にlet!で余計なデータを作ることは、テストの実行速度が遅くなる原因になるうえに、テストケース前に全て実行されてしまうのであまり使うべきではないことを学びました。

必要十分なテスト

まず必要十分なテストケースを考えるために、改めてメソッドの仕様を確認してみます。

def set_categories
  @categories = Category.where.not(priority: 0).order(priority: :desc).limit(6)
end

このメソッドはcategoriesテーブルからpriorityが0ではないレコードをpriorityの降順に6つまで取得し、@categoriesに格納します。つまり以下のような仕様になります。

  • priorityが0ではないレコードを取得すること
  • priorityの降順で取得すること
  • 最大6つまで取得すること
  • @categoriesに格納されること

それぞれのテストケースを作成することを考えると、例えば「priorityの降順に取得すること」をテストするのにテスト用のカテゴリが6つもいらなかったり、「priorityが0ではないレコードを取得すること」をテストするテストケースが不足していることが分かります。

以上の点を踏まえ、必要十分なテストを目指し修正したset_categoriesメソッドのテストは以下のようになりました。

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ApplicationController, type: :controller do

~~~

  describe '#set_categories' do
    subject { controller.send(:set_categories) }

    context do
      before do
        FactoryBot.create(:category, priority: 1)
        FactoryBot.create(:category, priority: 2)
        FactoryBot.create(:category, priority: 0)
      end

      it 'priorityが0でないカテゴリをpriorityの降順ですべて取得する' do
        res = subject
        expect(res.size).to eq 2
        expect(res.first.priority).to eq 2
        expect(res.last.priority).to eq 1
      end

      before { subject }

      it '@categoriesに格納される' do
        expect(assigns(:categories)).not_to be_nil
      end
    end

    context 'カテゴリが7つ以上ある場合' do
      before do
        FactoryBot.create(:category, priority: 1)
        FactoryBot.create(:category, priority: 2)
        FactoryBot.create(:category, priority: 3)
        FactoryBot.create(:category, priority: 4)
        FactoryBot.create(:category, priority: 5)
        FactoryBot.create(:category, priority: 6)
        FactoryBot.create(:category, priority: 7)
      end

      it 'priorityの降順で6つ取得する' do
        res = subject
        expect(res.size).to eq 6
        expect(res.first.priority).to eq 7
        expect(res.last.priority).to eq 2
      end
    end

    context '該当するカテゴリが無い場合' do
      before do
        FactoryBot.create(:category, priority: 0)
        subject
      end

      it '@categoriesに空の配列が格納される' do
        expect(assigns(:categories)).to eq []
      end
    end
  end
end

順番に見ていきます。

テストケース1

まず1つ目のテストケースでは、以下の仕様をテストしています。

  • priorityが0ではないレコードを取得すること
  • priorityの降順で取得すること
  • @categoriesに格納されること

1つ目の仕様のテストのためにpriorityが0のカテゴリが1つと、priorityの値が違うカテゴリを2つ作成しています。先ほどのテストと違い、let!インスタンス変数を定義しておらず、テストするのに最低限のレコードしか作成していません。

テストの内容も、わざわざマッチャを使用して順番まで完全に一致しているか見る必要がなく、priority: 1priority: 2として作られたカテゴリを取得した際に、最初のカテゴリのpriorityが2、最後のカテゴリのpriorityが1であれば降順で取得できていることが分かります。 配列の大きさを確認することで、priorityが0のカテゴリは含まれていないことも確認できます。

テストケース2

2つ目のテストケースでは以下の仕様をテストしています。

  • 最大6つまで取得すること
  • priorityの降順で取得すること

6つまでしか取得しないことをテストするのにカテゴリを7つ作成する必要がありますが、先ほど同様にインスタンス変数を定義する必要はありません。 配列の大きさと最初と最後のpriorityの値を見て、降順で6つ取得されているかをテストします。

テストケース3

3つ目のテストケースは、「何も取得できなかった場合(異常時)」をテストしています。

priorityの値が0のカテゴリを1つだけ作成し、set_categoriesが実行されたとき、@categoriesには空の配列が格納されることを確認します。

まとめ

これからはテストを書く際、以下のような部分に気を付けていきたいと思いました。

  • 異常時のことも考える
  • 不要なコードを書かない(テストが遅くなる)
  • そもそも機能の実装時に仕様を正しく理解しておく

特に不要なコードを書かないようにするためには、それが不要かどうか知らなくてはいけないし、考えれなくてはいけません。 テストを書く回数を重ね、何が最適であるかを模索するのも必要十分なテストを書くうえで必要です。

おわりに

メソッドの仕様や異常時の振る舞いを最低限の処理、テストケースで確認することができたと思っていますが、まだテストケースが不十分だったり余計な処理が含まれているかもしれません。 今までテストを作成するという文化を知らずに生きてきたのでまだ苦手意識がありますが、テストを作成する際は必要十分なテストになるよう心がけていきたいです。

新卒でベンチャーに入社した話

技術開発部の氏家です。 私は今年の4月に新卒でvivit株式会社に入社し、現在エンジニアとして働いています。 今回は私がvivitへの入社を決めた理由や入社前のインターンでしたこと、入社前と入社後のギャップについて話していけたらと思います。

vivitへの入社を決めた理由

vivitと出会ったのは私がエンジニアの就活イベントに参加したときでした。 マッチングした企業と就活生が1on1形式の面談をしてすり合わせするというもので、事前プレゼンで社風や事業に興味を持ちvivitとの面談を希望しました。

無事vivitとの面談が叶い、話していくうちに今何をやっていてこれから何をやろうとしているのか、もし自分が入社したらどのような業務をするのか知り、エンジニアとして成長していくのに理想的な環境だと感じました。 たくさんの企業を見て回り、将来のビジョンを考えたときにvivitが1番合っていると考え入社を決めました。

余談ですが、私が就活していたときの企業選びの軸は1つで、事業を好きになれそうかどうかだけでした。 携わる業務にやりがいや面白さを見出せれば、その業務を好きになり、もっと頑張ろうと思えるはずです。 「好きこそものの上手なれ」という言葉の通り、本当に好きなものは自然と上達していくと考えています。

入社前の長期インターン

大学4年生の夏季休暇中、約1ヶ月ほどvivitにインターンとしてお邪魔していました。 内容はざっくり言うと、実際の業務をやりながら学ぶというもので、MAUが350万を超えるアウトドアwebメディアのhinataというサービスの業務に携わることになりました。

hinataのメインはRuby on Railsで作られており、RailsどころかRubyすら触ったことが無かった私は初歩的なところで躓いていましたが、調べたりエンジニアの方々に教わりながら業務をこなしていました。 自分の好きなことを業務として取り組めること困ったときすぐにサポートしてくれる職場の環境に感謝しながらインターンに取り組み、4月から働けることを楽しみにしていました。

入社前と入社後のギャップ

エンジニアとして仕事ができるという期待で胸を膨らませていましたが、新卒1年目の自分がすぐに業務でコードをかけるとは考えていませんでした。 最初は研修を受けたり、初歩的な業務を任されると思っていたからです。 企業によっては3ヶ月~半年かけて研修を行うところもあり、インターンのときとは立場が違うことも理解していたのでしばらくは辛抱だと自分に言い聞かせていました。

しかし、4月1日の入社式で自分がインターンのときと同じmediaチーム(主にhinataの業務)への配属が決まった次の日にタスクが振られ、コードを書くことになりました。

ああ、ベンチャーって本当に良いなって思いました。

ちなみに研修自体は長期間を確保して実施されているわけではないですが普通にあります。 外部のビジネス研修を受けさせていただいたり、毎週エンジニア研修と称してチーム開発のノウハウや昨今の技術的な話をしていただいています。 今年の新卒エンジニアは僕1人だけなのですが、多くの時間とコストを割いて実施してくださっているので、それに見合うだけのインプットをし、これからの業務に活かしていきたいです。

おわりに

まだ入社したばかりで基本的な仕事もこなせていない私ですが、業務を通して少しずつ成長を感じています。 頼られるエンジニアになれるよう日々精進していきたいと思います。

突撃!隣のキーボード 2021

はじめに

フロントエンドエンジニアの関(@kur0buchi)です。

新型コロナウイルス感染症(COVID-19)により世の中の様々なコトモノが変わっていった2020年も明けたものの、未だ家にいる方が長い日を過ごす方も多いのではないかと思います。在宅での仕事を快適にしようとデスク周りの整備に務める方も多いのではないでしょうか。

今回はパソコン仕事には欠かせない「キーボード」にフォーカスして弊社の技術開発部にアンケートを取りました!
長引くテレワークで気分転換が欲しいという方も、一番多く触れるであろうキーボードにちょっとこだわってみてはいかがでしょうか。

本記事は同名タイトルで様々な企業様が書かれている記事に便乗したものとなっております。以下にその一部を紹介しますので、興味が湧いたという方は以下の記事もご覧になってください。

www.m3tech.blog

tech.fusic.co.jp

blog.yushakobo.jp

メンバーのキーボード

村山

f:id:KeytacK:20210210143105j:plain

ひとこと

キーボードの打鍵が気持ち悪くなければあと何でも大丈夫です!

筆者コメント

我らが技術開発部マネージャー、村山のキーボードは「孤高の遺伝子を受け継ぐ真の最高峰」でおなじみ Happy Hacking Keyboard (通称 HHKB) です!
実は弊社開発部は HHKB ユーザが結構多く、一時期並んだ机が一列 HHKB といった状況があったりしました。
「打鍵が気持ち悪くなければあと何でも」と言って HHKB を選ぶあたりなんとなくこだわりも感じますね!(?)

ijima

f:id:KeytacK:20210210191655j:plain

ひとこと

静電容量無接点方式を試したかったのと、テンキーがほしかった

筆者コメント

こちらはエンジニアのみならず弁護士や小説家など、幅広い分野で人気(公式サイトより)の静電容量無接点方式キーボード、東プレ REALFORCE for Mac です!
待望の Mac 向けキーボードとして話題になったキーボードですね、テンキーレスモデルも人気ですが、インフラエンジニアとしてパラメータ操作等数字を扱うことが多いとフルサイズも欲しくなりますね!

shimar

f:id:KeytacK:20210210143927p:plain

ひとこと

カラーキートップを着けて使っています

筆者コメント

こちらは HHKB にオプションのキートップを着けてますね!
HHKB はメカニカルキーボードと違ってキーキャップが特殊でアレンジがしにくいイメージがありますが、公式でカラーキートップを出していたりするので結構遊べますね!

小石

f:id:KeytacK:20210210144140j:plain

ひとこと

中古のHHKBです。会社とキーボード揃えたかったので買いました。満足してます。
写真のキッチンタイマーはポモドーロタイマーとして活用してます。

筆者コメント

リモートワークに合わせて HHKB を買い足すという人も……!
一般的なキーボードと比べるとキー数も少なく Professional モデルになると方向キーまでなくなってしまったりと「使いづらそう」というイメージがあったりもしますが、慣れてしまうとコードを書くのに凄く合理的な作りだったりもするので手放せなくなるキーボードですね!

@taroodr

f:id:KeytacK:20210210144242j:plain

ひとこと

つい一週間ほど前に購入しました。
手首が痛むので体への負担がなるべく少ないものを選んで使っています。

筆者コメント

キーボードは静電容量タイプだけではありません! Mistel BAROCCO はメカニカルスイッチ(写真は CHERRY MX 静音赤軸モデル)採用の分割キーボードです!
巷では「肩が開いて人間工学的に良い」なんて言われたりする分割キーボードですが、実際自分の思う自然な姿勢でタイプができるためかなりオススメです。

これがやりたかっただけだろのコーナーです

f:id:KeytacK:20210210144700j:plain

Lily58 + Domikey SA Dolch Orange + DUROCK T1
いわゆる「自作キーボード」と呼ばれる、キットを購入して自分で組み立てるタイプのキーボードを使っています。
私が使っているのは Lily58 という分割式のキーボードで、会社用と自宅用で2台作るほど愛用しています。
少ないキー数と特徴的な配列で限りなく最小限の手の動きで全てのキーをタイプできるよう設計されているので、慣れたときの快適さは言葉では言い表せられません。
細かく紹介していくとキリがないのでいずれ機会があれば。

f:id:KeytacK:20210210144825j:plain

FILCO Majestouch 2SS TKL
今回のアンケートでは静電容量無接点方式のキーボードが多かったので、どうしても紹介したいメカニカルキーボードをもう一つ紹介します。
王道一直線のメカニカルキーボードといえばこの Majestouch シリーズかと思います。軸も多種多様でサイズもフルから60%まで用意されているので、買ったことないけど外付けキーボードが気になるという方にお勧めしやすいシリーズです。
(写真は2021年1月に発売されたばかりのスピード銀軸モデル)

他にも……

実は以前にもアンケートを実施し、筆者都合で記事化には至らなかったキーボードの写真をご紹介します!
ご協力頂いた方にはこの場をお借りしてお詫びと感謝を申し上げます
m(_ _)m

f:id:KeytacK:20210210145042j:plain

f:id:KeytacK:20210210145118j:plain

f:id:KeytacK:20210210145204j:plain

f:id:KeytacK:20210210145245j:plain

おわりに

今回紹介したキーボードは高額なキーボードばかりでしたが、もっと安価で使い勝手のいい高コスパなキーボードもたくさんあります!
在宅ワークが捗らないなぁと感じる方も是非外付けキーボードをご検討してみてはいかがでしょうか?

求人

vivitではコダワリ派のあなたの応募もお待ちしております

www.wantedly.com

Goでsitemap.xmlを生成する

こんにちは、spotチームの名嘉眞です。spotチームはキャンプ場検索サービス(hinata spot)を開発しております。私はspotチームのバックエンド担当として日々Goを書いてます。

hinata spot

今回はsitemap.xmlをGoで生成する方法についてまとめてみました。標準パッケージで割と簡単に出来るかなと思います。

始めに

hinata spotでは、sitemap.xmlの生成もGoで書いています。ちなみにsitemap.xmlとは、ウェブサイト内の各ページのURLや優先度、最終更新日、更新頻度などを記述したXML形式のファイルのことです。

railsだとsitemap_generatorというgemを使って生成したりするかもしれません。Goにもそのようなライブラリがあるかもしれないですが、標準packageのencoding/xmlで十分実装できると考えました。

処理の流れ

私の担当するサービスの場合、sitemap.xmlに記載されるurlを構成する要素はDBから取得する必要があります。そのため下記のような処理の流れで生成します。

  • DBから対象のデータ取得
  • URLの組み立て
  • sitemap.xmlの生成
  • sitemap.xmlgzip
  • GCSへアップロード

この記事では主に、sitemap.xmlの生成部分について紹介していきます。

実装部分

sitemap.xmlは以下のようなファイルです。sitemap固有のタグなど生成ルールが決まっています。

<?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <url>
          <loc>https://hinata-spot.me/spots/taito-beach</loc>
          <changefreq>2020-10-20T14:56:46+09:00</changefreq>
      </url>
      <url>
          <loc>https://hinata-spot.me/spots/tanpopomura</loc>
          <changefreq>2020-10-20T14:56:46+09:00</changefreq>
      </url>
  </urlset>

まず、sitemapxmlをGoのstructで表現するために、structを以下のように定義しています。 encoding/xmlの場合、structの各フィールドにxmlタグと付与したいメタ情報を適用すると、適用したメタ情報をもとにxml生成時にタグで値を囲います。 attr と記載すると上位のタグの中にattrで指定した値を埋め込みます。

実際に生成したいsitemap.xml(上の例)と以下のstructでマッチする部分としては、 XMLName、Version、Xhtmlフィールドが実際に生成したいsitemap.xmlのurlsetタグの部分になります。 SiteListフィールドがsitemap.xmlに設定したいサイトのページのURLになります。

package models

import "encoding/xml"

type SiteMapXML struct {
    XMLName  xml.Name `xml:"urlset"`
    Version  string   `xml:"xmlns,attr"`
    Xhtml    string   `xml:"xmlns:xhtml,attr"`
    SiteList []*Site  `xml:"url"`
}

type Site struct {
    URL       string `xml:"loc"`
    UpdatedAt string `xml:"changefreq"`
}

サイトマップ生成の関数が実行されると、DBから対象のデータを取得し上記のSite structを生成するようにします。 以下のような関数を使ってsitemapに設定したいURLを生成しています。 最終的に、[]*Site型 を生成します。

func CreateSite(path, updatedAt string) *models.Site {
    u := url.URL{Scheme: "https", Host: spotHost, Path: path}
    site := &models.Site{
        URL:       u.String(),
        UpdatedAt: updatedAt,
    }

    return site
}

// dbResultはDBから取得したデータとする
siteList := make([]*models.Site, len(dbResult))
var index int

for i := range dbResult {
  path := "spots/" +  dbResult[i]
  site := CreateSite(path, dbResult[i].UpdatedAt)

  siteList[index] = site
  index++
}

sitemap.xml生成するパッケージは以下のように定義しています。このパッケージに定義したCreateXMLgzip 関数の引数に、上の例で生成した[]*models.Siteを渡すことで、sitemap.xmlの生成とgzip化を行います。 sitemap.xml固有の値は定数にて定義しています。

package sitemap

import (
  // 省略
)

const (
    version         = "http://www.sitemaps.org/schemas/sitemap/0.9"
    xhtml           = "http://www.w3.org/1999/xhtml"
    spotHost        = "hinata-spot.me"
)

// CreateXMLgzip creates site map xml from struct.
func CreateXMLgzip(siteList []*models.Site) (io.ReadWriter, error) {
    ss := &models.SiteMapXML{
        Version:  version,
        Xhtml:    xhtml,
        SiteList: siteList,
    }

    data, err := xml.MarshalIndent(ss, "  ", "    ")
    if err != nil {
        return nil, err
    }

    // xml.Header は、encoding/xmlパッケージで以下のようにconstで宣言されています。
    // <?xml version="1.0" encoding="UTF-8"?>
    bss := [][]byte{[]byte(xml.Header), data}
    bs := (bytes.Join(bss, []byte("")))

    var result bytes.Buffer
    zw := gzip.NewWriter(&result)

    _, err = zw.Write(bs)
    if err != nil {
        return nil, err
    }

    if err := zw.Close(); err != nil {
        return nil, err
    }

    return &result, nil
}

CreateXMLgzip 関数は、戻り値をインターフェース io.ReadWriter型にしています。理由はGCSにアップロードするオブジェクトをインターフェースのio.Reader型で受け取るようにしているからです。 io.Reader で受け取れるようにすることで、ストレージへのアップロードなど共通で使うような関数を使いやすくしています。

結構簡単に実装できたと感じるのではないでしょうか。サービスの内容によるかもしれないですが、SEOを考えるとXMLサイトマップを生成し管理することがあると思いますので、その際に役に立つことができたら嬉しいです。

コーディング時のデザイナーとのコミュニケーションについてガイドラインを作成してみました

vivit株式会社でアウトドアメディア:hinataの開発をしています河村です。 今回はコーディング時のデザイナーとのコミュニケーションについてガイドラインを作りましたのでそのお話を書こうと思います。

きっかけ

以前からデザイン改修やLP実装などの開発案件が入る際、フロントエンドエンジニア・デザイナー間で細かい部分の仕様のやりとりが増えていたという問題が発生していました。 簡単な例ですが、以下のような一覧の要素があったとします。

f:id:Kawam:20201001093052p:plain

この内容が3行を超えた場合はどうなるでしょうか?

(4行に高さが延びる場合) f:id:Kawam:20201001093054p:plain

(3行で収めたい場合) f:id:Kawam:20201001093057p:plain

エンジニア側としては4行目に達することも想定して実装をしなければならないため、その場合にどちらのデザインで実装すべきなのかは把握しておく必要があります。 こういった一つ一つについて確認のやりとりが発生すると、結構なコミュニケーションコストになります。

作成したガイドライン

そこで、デザインが納品される際にどのようなことが明確であれば開発がスムーズに行えるのかという点を洗い出してチェックシートを作成しました。 内容は以下の通りです。

  • モニターサイズによる可変

    • PCのウインドウサイズが変わった場合やSPでデバイスの画面サイズが違う場合の表示
  • カラーコードの指定

    • カラーコードは既存通りのものなのか、別に指定しているのか
  • アニメーションの挙動

    • モーダル・サイドメニュー・スクロール固定ヘッダーなどのアニメーションについてはデザインに落とし込むのが難しいため、参考サイトのURLを載せておくなどして対応しています。
  • クリックon,off、マウスオーバー時、disabledのデザインパーツは用意されているか

  • 文字数が表示領域を超える場合、要素が増える場合、要素が一つもない場合の想定をされているか

  • フォントサイズの指定(h1,2,3,本文)ごとにルールが決まっているか

  • フォントの指定はあるか

  • マージンのルールが決まっているか

  • 文字の視認性が保たれているか

  • 機能デザインがあれば仕様が固まっているか

    • 一画面の中にフォームのエラー表示や決済・画像アップロードなどの完了の文言を表示する場合に、デザインが用意されているか
  • pxは2で割り切れるか?

    • retina対応で倍のサイズに書き出す必要がある場合、実際の表示サイズ指定は半分になるのでその指定ができるようになっているか。
  • テキストに誤字・脱字・スペルミス・機種依存文字macの絵文字とか)がないか

  • 意図的ではない表記のゆれがないか

    • 例:WEB・Web・ウェブなど
  • 文字の表記が統一されているか

    • 半角・全角、日付の表現など

項目としては少々多いかもしれませんが、項目追加などでは既存のデザインに合わせる場合などもあるので毎回全てが当てはまるわけではありません。

やってみた結果

こちらのガイドラインを導入してみた結果、体感的にではありますが確認のやりとりは少なくなりました。 また、デザイナー側からもエンジニアが実装する上でどんな情報が必要なのかが理解できて、やりとりがスムーズなったというフィードバックもいただきました。

まとめ

毎回同じようなやりとりをしているのであれば、確認事項は先にわかっていた方がお互いに健全に仕事ができます。 特にエンジニアは仕様が不明確だと実装時に不安になってしまうものなので、こういったガイドラインがあるとよいアウトプットができるようになると思います。

GKEに対し、GitOpsやってます

インフラエンジニアをやっている井島です。

hinata の各プロダクト(Rental, Spot, Media)はGKEで稼働しており、GitOpsの考え方でデプロイを行っています。 今回はvivit で実際に使っている具体的なデプロイ構成についてご紹介したいと思います。

vivit では、開発環境は各人のローカルPC上とし、ステージング環境、本番環境はGKE上にあります。 開発環境で開発したものは、各々がステージング環境にデプロイして、最終的に本番環境にデプロイして、リリース完了、という開発の大きな流れになっています。

vivit では、1つのチームが1つのマイクロサービスの開発を行っているのではなく、基本的にプロダクト毎に開発チームが別れています。1つのチームが複数のマイクロサービスを扱っており、開発内容によってリリース範囲が変わってきます。

構成

f:id:ijimakenta:20200928201145p:plain

GitHubレポジトリは基本的にモノレポ構成で、プログラムコードとk8sマニフェストも同じレポジトリに入っています。k8sへのデプロイにはArgoCDを使用しており、CIには主にCircleCIを使用しています。

開発者がコードをレポジトリにPushするとCircleCIでDocker build が実行され、コンテナがGCSにPushされます。

そして、開発者がGKE上の環境にデプロイしたい時は、git tag をレポジトリにPushして、デプロイをトリガーします。この時、CircleCIではデプロイ用の命名規則のgit tag 名だった場合にデプロイ処理を発火するように設定してあります。

このデプロイ処理は、k8sマニフェストでコンテナイメージのタグを更新し、レポジトリのmaster ブランチに対してPull Request を作成する処理です。

そして、開発者はCIによって作成されたPull Request を自分でApproveし、master にMergeすることで、一連の操作が完了です。

このPull RequestのMergeが、実際に環境へのデプロイ(Podの更新)処理開始のトリガーとなります。

ArgoCDはいずれの環境へもGitHubのmaster ブランチを参照するよう設定しており、ステージング環境であっても、master ブランチにあるマニフェストが正とすることを大切にしています。

詳細

この構成の欠点は、デプロイをトリガーする前に、GCR上にコンテナが出来上がっていることを事前に開発者が確認する必要があることです。

しかし、現在のvivit の開発チーム構成からデプロイの柔軟性を優先して、この構成にしています。

このデプロイ手順については以下の要求に対応できるよう設計しています。

  • 1つのマイクロサービスだけリリースしたい

  • 複数のマイクロサービスを同時にリリースしたい

  • チーム内で管理しているすべてのマイクロサービス(≒プロダクト)を同時にリリースしたい

  • 特定のブランチ(で作成されたコンテナ)を特定の環境にデプロイしたい

  • ステージングや本番への各環境へのデプロイ手順に差異がないようにする

  • いま各環境にデプロイされているアプリケーションのバージョンなど(どのコンテナを使っているか)は、k8sマニフェストから追えるようにしたい

上記条件でデプロイを行うためにgit tag 作成でデプロイのトリガー、git tag の内容によって、どのブランチの、どのマイクロサービスを、どの環境にデプロイするかを制御しています。 具体的なgit tag の内容は以下です。

git tag 命名規則
<プロダクト名>/<環境名>/<年>/<月>/<日>/<時分>-<氏名>-<マイクロサービス(コンマ区切りで複数指定可)>
「ブランチ」の指定は、目的のブランチに移動してからgit tag を行う。
product1 の service1 とservice2 を production 環境にデプロイする時
product1/production/2020/09/29/1234-ijima-service1,service2
product1 のすべてのマイクロサービスをstaging 環境にデプロイする時
product1/staging/2020/09/29/1234-ijima

CircleCI では git tag 付与をトリガーとしてCIを実行できるので、その構成を行い、git tag のパース、指示されたデプロイ内容に基いたマニフェストの更新(コンテナイメージタグ部分)、Pull Request 作成などの処理を実装しています。

最後に

CircleCIなどの具体的なコードは割愛していますが、実際に使っている GitOpsなデプロイの具体的な構成の紹介でした。
デプロイ手順は、会社やチームによって、いろいろな要件が出てくるかと思います。なにかの参考になれば幸いです。

Google Analytics API を使ってディレクトリごとにドリルダウンする

フロントエンドエンジニアの関です。

リモート環境整備として椅子の検討を数ヶ月間予算や材質など検討した結果、現在バランスボールに座って仕事をしています。 ちなみにvivit社内でも希望者はバランスボールに座って仕事ができます。

弊社が運営しているキャンプ場検索サービス hinata spot で、よく見られているキャンプ場の一覧を作りたいということになり、PVランキングのような仕組みが必要になりました。

幸い当サービスでは特定のディレクトリをドリルダウンしていけば特別に解析タグを用意したりせずにキャンプ場ページのPVランキングを取得できる構造になっているので、今回は Google AnalyticsCore Reporting API を使ってコンテンツのドリルダウンを行います。

developers.google.com

API クエリは Query Explorer で実際にレスポンスを確認することができます。

ドリルダウンでPVを取得する

example.com/hoge/ 配下のPV順を見たいと仮定し、まずはパラメータから出します。

  • start-date: 30daysAgo*
  • end-date: yesterday*
  • metrics: ga:pageviews
  • dimensions: ga:pagePathLevel2
  • filters: ga:pagePathLevel1==/hoge/

* 期間は任意です

filters: ga:pagePathLevel1==/hoge/ で配下を見たいディレクトリを絞り込み、 dimensions: ga:pagePathLevel2 でその直下のディレクトリを見るといった具合です。

ドリルダウンということで更に example.com/hoge/fuga/ 配下を見たい場合は dimensionsfilters を以下のように書き換えます

  • dimensions: ga:pagePathLevel3
  • filters: ga:pagePathLevel1==/hoge/;ga:pagePathLevel2==/fuga/

Google Analytics Webコンソール上でディレクトリを下っていくように、ディレクトリを指定してその直下を見るというイメージで下っていくことができます。

最後に

hinataはWebメディアの枠を超え、実店舗や各種リアルイベントでもアウトドアの良さを発信しはじめました。

vivitでは領域を問わずサービスを成長させていく仲間を募集しています。 www.wantedly.com

参考

jamesdoc.com