RSpec では context 間の違いを表現するときにのみ let を使う

Test which reminded me why I don't really like RSpec | Arkency Blog (日本語訳:Rails: RSpecが好きでないことを思い出したテスト(翻訳)|TechRacho by BPS株式会社) を見ての感想。

元のコードのイマイチなところは 4 つあって、

  • paramsbefore で書き換えている *1
  • it "will succeed" の文言
  • it { is_expected.to be_success }expect(result.success?).to eq(true) が混ざっている
  • let が不思議な順序で連発されていて事前条件を読み解けない

すべて、これによって何をテストしているのかが分かりづらくなっているという問題を引き起こす。

paramsbefore で書き換えている

let(:params) { { last_name: "something" } }
subject(:result) { described_class.call(current_user: user, params: params) }
...
describe "checking Address update" do
  let(:new_zip_code) { Faker::Address.zip_code }
  before { params[:zip_code] = new_zip_code }

  context "when user has address and want to change something in their address" do
    let!(:address) { create(:address, owner: user) }

    it "will succeed" do
      expect(result.success?).to eq(true)
      ...

この before は上の方で定義してある let との合わせ技で、かなり厳しい。

このクラスでは、異なる結果が得られる主な入力は params である。

この入力がどう変わると出力がどう変わるかは明確にわかるはずだが、これを call に明示的に渡さない理由がわからない。

にまったく同意で、そのために describe で切り分けているのだから、params に何が渡るのかは describe 内で明示する。何をテストしているのかを明確にするというのを意識したい。今は params の違いをテストしているのだから、params が何なのかをそのまま書く。let で定義して subject に渡しているので、let で上書けば良い。

 let(:params) { { last_name: "something" } }
 subject(:result) { described_class.call(current_user: user, params: params) }
 ...
 describe "checking Address update" do
   let(:new_zip_code) { Faker::Address.zip_code }
-  before { params[:zip_code] = new_zip_code }
+  let(:params) { {
+    last_name: "something",
+    zip_code: new_zip_code,
+  } }

   context "when user has address and want to change something in their address" do
     let!(:address) { create(:address, owner: user) }

     it "will succeed" do
       expect(result.success?).to eq(true)
       ...

「他のところで作ったものを before でちょこっと書き換えよう」はだいたい悪手。あと多分ココは last_name: "something", を残す必要なくて、zip_code のテストをしたいので zip_code のみになっている方が望ましいと思う。

it "will succeed" の文言

describe "checking Address update" do
  ...
  context "when user has address and want to change something in their address" do
    ...
    it "will succeed" do
      ...
    end
  end

  context "when user has not have any address and want to change something in their address" do
    ...
    it "create address with given params" do
      ...
    end
  end
end

これは何のテストをしているのか読み解くのが難しいと思うけど、よくよく読むと、やりたいことはただの upsert である。おそらく実装側はこうなっている。

if params[:zip_code]
  address = current_user.address || current_user.build_address
  address.zip_code = params[:zip_code]
  address.save!
end

upsert のテストをしているというのが分かるように説明を書く。なお、テスト対象や事前条件と context の説明は一致していて、context 直下に before もしくは let が来るのが良い分割。テストの違いを contextbefore, let で表現するのだ。

ついでに何をテストしているのか分かりやすいように「params[:zip_code] を渡したとき」というのも説明文として表現するとより親切だろう。

context "with params[:zip_code]" do
  let(:params) { {
    zip_code: new_zip_code,
  } }
  ...
  context "when address exists" do
    before { create(:address, owner: user) }
    ...
    it "will be updated" do
      ...
    end
  end

  context "when address does not exists" do
    ...
    it "will be created" do
      ...
    end
  end
end

describe, context, it の説明文を駆使して何をテストしているのかを明らかにするのは、可読性の高いテストを書くための基本です。

it { is_expected.to be_success }expect(result.success?).to eq(true) が混ざっている

context "when address exists" do
  ...
  it "will be updated" do
    expect(result.success?).to eq(true)
    expect(address.reload.zip_code).to eq(new_zip_code)
  end
end

context "when address does not exists" do
  ...
  it { is_expected.to be_success }
  it "will be created" do
    expect { result }.to change(Address.all, :count).from(0).to(1)
    expect(Address.last.zip_code).to eq(new_zip_code)
    expect(user.address).to eq(Address.last)
  end
end

update 側では result.success? と、更新されたことの 2 個の expect があるが、 create 側では result.success? は単独のテストとして切り出されているし、書き方も違う。

書き方が違うと「この違いに何か意味があるのか?」という疑問が湧いてしまい、テスト対象がぼやける。テストの粒度は揃える。

また、success? であることはあまり主眼ではないので、目立たないように定型文っぽくなっているとより良い。つまり eq(true) とハッキリと対象をテストするよりも、読み下して読み飛ばせる方が望ましい。

 context "when address exists" do
   ...
+  it { is_expected.to be_success }
   it "will be updated" do
-    expect(result.success?).to eq(true)
     expect(address.reload.zip_code).to eq(new_zip_code)
   end
 end
 
 context "when address does not exists" do
   ...
   it { is_expected.to be_success }
   it "will be created" do
     ...
   end
 end

こうすると、全 context で「result.success?」と「詳細な副作用」をテストしている、と整理できる。

let が不思議な順序で連発されていて事前条件を読み解けない

context "when user has parent and want to change something" do
  let(:parent) { create(:parent) }
  let(:user) { create(:user, profession: student) }
  let(:student) { create(:student, parent: parent) }

  it { is_expected.to be_success }
  ...

ここでは parentupsert するときの update 側をテストしたい。そのため、 テスト対象 (subject) である Api::Students::Update.call(current_user: user, params: params)user 側に「parent を持っている User である」という事前条件がある。

最終的に欲しいのは user なので、せめてこの順に並んでいて欲しい。

  let(:parent) { create(:parent) }
  let(:student) { create(:student, parent: parent) }
  let(:user) { create(:user, profession: student) }

また、事前条件なんだから before で整える、というのも自然な考え方だろう。let と違い遅延評価されず、スコープが絞られるため、怖れずに上から順に読めるようになり、処理を追うのが簡単になる。

context "when user has parent and want to change something" do
  before {
    parent = create(:parent)
    student = create(:student, parent: parent)
    @user = create(:user, profession: student)
  }
  let(:user) { @user }

  it { is_expected.to be_success }
  ...

let(:user) { create(:user, profession: @student) } でも良いんだけど、私は before で作ったインスタンスlet にパスするだけが好きです。この方が事前条件を整えた後に context に受け渡している感がある。

僕なら最初の 5 分でこうリファクタリングする

RSpec.describe Api::Students::Update do
  let(:user) { create(:user) }
  let(:params) { { last_name: "something" } }
  subject(:result) { Api::Students::Update.call(current_user: user, params: params) }

  context "when user is a student" do
    it { should be_success }
  end

  context "when user is teacher" do
    let(:user) { create(:user, :teacher) }
    it { should be_failure }
  end

  context "with params[:zip_code]" do
    before { @new_zip_code = Faker::Address.zip_code }
    let(:params) {
      {
        last_name: "something",
        zip_code: @new_zip_code,
      }
    }

    context "when address exists" do
      before { @address = create(:address, owner: user) }

      it { should be_success }
      it "will be updated" do
        result
        expect(@address.reload.zip_code).to eq(@new_zip_code)
      end
    end

    context "when address does not exists" do
      it { should be_success }
      it "will be created" do
        expect { result }.to change(Address.all, :count).from(0).to(1)
        expect(Address.last.zip_code).to eq(@new_zip_code)
        expect(user.address).to eq(Address.last)
      end
    end
  end

  context "with params[:parent]" do
    before { @parent_first_name = Faker::Name.female_first_name }
    let(:params) {
      {
        parent: { first_name: @parent_first_name },
      }
    }

    context "when parent exists" do
      before {
        @parent = create(:parent)
        student = create(:student, parent: @parent)
        @user = create(:user, profession: student)
      }
      let(:user) { @user }

      it { should be_success }
      it "will be updated" do
        result
        expect(@parent.reload.first_name).to eq(@parent_first_name)
      end
    end

    context "when parent does not exists" do
      it { should be_success }
      it "will be created" do
        expect { result }.to change(Parent.all, :count).from(0).to(1)
        expect(Parent.last.reload.first_name).to eq(@parent_first_name)
        expect(user.profession.parent).to eq(Parent.last)
      end
    end
  end
end

こうしておくと -f d で実行したときにも読み解きやすい。

Api::Students::Update
  when user is a student
    is expected to be success
  when user is teacher
    is expected to be failure
  with params[:zip_code]
    when address exists
      is expected to be success
      will be updated
    when address does not exists
      is expected to be success
      will be created
  with params[:parent]
    when parent exists
      is expected to be success
      will be updated
    when parent does not exists
      is expected to be success
      will be created

change はあんまり使いたくないなー (expect { result }.to はちょっと読みづらくない?) とか、多分 create の方のテストを先に書くだろうな (条件が複雑な方をより後でテストしたいので、レコードが存在している場合は後ろに書きたい) とか、一つのテストの中で複数の expect を書くなら aggregate_failures を使えとかの思いはあるけど、シュッと直すならこうかなー

元記事のリファクタリング後と何が違うか

  • describe (context) のネストは維持している
    • テスト対象が明確に違うので、ネストしておく方が自然
    • params[:zip_code]params[:parent] があるときの、それぞれの upsert を確認している
  • context の説明はより実装寄りに
    • ユニットテストなので実装寄りの言葉で問題無いし、その方が実装を書き換えるときにテストの対応を取りやすい
  • subject を引き続き使う
    • テストしたいのは params の違いなので、params に着目したい。
    • 元記事のリファクタリング後だと params は 70 文字目辺りから始まっていて、どこに違いがあるのかを読み解くのが困難
    •     result = Api::Students::Update.call(current_user: user, params: { parent: { first_name: parent_first_name } })
      
    • subject に執着しているわけではなく、テスト間の違いをもっと目立たせろと思っています
  • letcontext 間の差を表現するために使う
    • よりテスト間の違いに目を向けさせるためです
    • let は遅延評価&後勝ちなところが大きなメリットで、まさに context 内で上書きしたいときに使う語彙です
    • そのため、私は積極的に before やローカル変数、インスタンス変数を使い、let を温存します
      • 今回の書き換えでも user, params のみが let になっていて、何をテストしているのか読み解きやすいんじゃないか?
      • テーブルテストと同じことを contextlet でやっているのです

subjectインスタンス変数に反対の派閥があるのは知っていて、異論はあると思うけど、私は「テスト対象が分かりやすくあった上で Ruby/RSpec の表現力にあやかりたい」と考えているので、こう書いています。

合わせて読みたい:Rails Developers Meetup 2017 で RSpec しぐさについて話した - onk.ninja

*1:公開当初は「params を merge している」と書いていたのでこういう反応があった https://twitter.com/qsona/status/1629840312389742593

YAPC::Kyoto 2023で話します! そしてチケットを今すぐに購入しましょう!!

YAPC::Kyoto 2023の採択トークが決まったようですね。面白そうなトークが沢山あってすごいですね。

blog.yapcjapan.org

私のトークも採択されました。ありがてぇ!

こういう話をします。

ORM - Object-relational mapping

はてなPerl プロダクトは薄いフレームワークを志向して、データベースとのやり取りに DBIx::Sunny や DBIx::Handler::Sunny を用い、主に SQL を書いて暮らしていました。最近、私はこの世界に ORM を持ち込みました。 PofEAA によるデータソースのアーキテクチャの 4 分類、我々が何を考えてどのパターンを選んだか、必要になって書いたプラグイン等、ORM の無い世界に ORM を入れていくに当たって考えたことと、その実践。 Perl Monger なら一生に一度は書くといわれる ORM を書いていく様子を、RailsActiveRecord に慣れ親しんだ現代の目線も交えつつお送りします。

プロポーザルは 40 分枠のつもりで出していたのですが、備考に「20分でも構いません」と書いておいたら連絡が来たので快諾しました。 話したい人が沢山いてめでたい!ちょっと駆け足で話します!

裏話

タイトルは Kyoto.pm Tech Talks #01 リスペクトです。

このときの「ORM - Object-relational mapping」というトーク@nekokak さんによるものですが、今回もゲストスピーカーとしていらっしゃるので、目の前で同じタイトルのトークをするのはなんだかすごくアレですね……!頑張ります。

blog.yapcjapan.org

チケットを買ってくれ

それはそうとして、そんなYAPC::Kyoto 2023ですがチケット販売が今月1月の31日までとなっています。

passmarket.yahoo.co.jp

今月中にチケットを買わないと参加ができないのです! 今、まさにこの瞬間、すぐに買いましょう!!!!!

買いましたか? 買いましたね。それでは会場でお会いいたしましょう!

私は京都は住んでいるので、地元で YAPC を開催することができて感動しています。来る方を精一杯歓迎いたします!

オマージュ元

moznion.hatenadiary.com

ストーリー性のあるプレゼン

このツイートの「文字を組み合わせる」のところについて、もうちょっと掘り下げてみる。*1

この記事は はてなエンジニア Advent Calendar 2022 の1月2日の記事です。昨日は id:stefafafan『UNIXという考え方―その設計思想と哲学』を読んだ - stefafafan の fa は3つです でした。

3 つのポイント

  • 知っていること 7 割、聞いたことがあること 2 割、知らないこと 1 割
  • 引用しやすいワーディング
  • ストーリー作り

この 3 つはとても意識しているポイント。

知っていること 7 割、聞いたことがあること 2 割、知らないこと 1 割

大半は頷きながら聞けるし、やった方が良いって聞いたことはあるけど本当なんだと感じることもできる。そして知らないことを知って興奮する部分もある、というバランスを気にしている。

知っていることから始めることで、従来と違うところや、一番注目させたいポイントにちゃんと目を向けさせることができる、がこの割合の重要な点。これが逆転していると聞き手が途中で振り落とされてしまう。せっかくなら全員を最後まで連れて行きたい。

引用しやすいワーディング

Twitter で実況しやすいとか、ブクマコメントにしやすいとか、一部を切り出して「そうそうコレ!!」と流通させてくれるような聞き手との対話を考えながら作っている。これがあると無いとでは後から思い返される率がガラッと変わる。「引っかかり」を感じさせるキーワードを考え抜いて、色んな軸で引っかけに行くイメージ。

2-3 分おきに繰り出せたら飽きないトークになるので、緩急を考えながら散りばめていく。

いいワードが出たら懇親会とかでも話題にして貰いやすい。自分のトークをきっかけにコミュニケーションを生みたいので、話題になるのは目指したい姿。

ストーリー作り

上 2 つはテクニックっぽい *2 けど、ストーリーはもっと根幹なので、しっかり掘り下げてみる。

自分のプレゼンをふりかえる

Hatena Engineer Seminar #22 「会社説明資料に載らないはてな」

www.slideshare.net

20 分トーク。会のテーマは「カジュアル面談」。僕は主にアウトプットをテーマにしたカジュアル面談を行ってきたので、その話をしたい。

話してきた内容をザッと書き出した後に、書き手側と読み手側という 2 つの軸があるんじゃないかというのが見えてきた。

  • 技術記事を書く文化
  • 技術記事を読むのを楽しむ文化

「片方に注力するのではなく両輪である」は普遍的に使える構造。

書く側にフォーカスした記事はよく見るが、読み手として楽しむ文化作りの方はあまり見ないので、これをセットにして両輪構造であると伝えるのは新規性がありそう。この構造を組み立てられたので、このトークは成り立つと考えた。

両輪のそれぞれが単独のトークとしても成り立つ自信がある内容だったので割と安産。

id:onk 技術力向上にアウトプットが効くと聞いているが始め方が分からない人から、アウトプット文化の無いところに文化を創りたい人まで、色んな カジュアル面談 をしてきました。その中で明らかになった 2 つの階段についてお届けします。

Hatena Engineer Seminar #19 「カクヨム編」

www.slideshare.net

20 分トーク。会のテーマは「カクヨム」。その中でも、技術の話は他の人がやってくれるので、僕は開発チームの暮らしぶりにフォーカスしたい。

一番話したいのは昼会が 1h あること。これを短くできないか悩みながら 1h を維持することを選択しているので、この悩んだ過程を伝えたい。

リモートワーク下での雑談の必要性を「毛づくろい」に例えた話を以前どこかで見たことがあったので、タイトルと開始直後に「社会的グルーミング」というワードを打ち出して、必要性に権威があるっぽく見せかけることにした。*3

毛づくろいと雑談の対比構造だけだと LT で消化してしまう程度の話題なので、20 分トークにするにはもう 1-2 個盛り上がりを入れたい。

そこで

  • 時間軸の違うそれぞれの雑談
    • デイリー、スプリントごと、四半期ごとで、それぞれ喋る時間軸の違い
  • 雑談とはつまり何なのか
    • 相手の反応を想像できるようになる行為
    • ボケ、ツッコミ、客

という 2 つの話題を追加した。

時間軸を変えると視座が変わり、視点を変えられる、というのは以前ブログに書いたが、この視座の変化を日常の中で回しているのは、安定して技術ロードマップを敷けるチームの特徴なので語っておきたい。

tech.drecom.co.jp

雑談から生まれる関係性というのは、記憶にあったこのツイートをイメージしていた。

我々はハイコンテキストな会話をよく行う。つまりコンテキストを理解していないと盛り上がらない、話す側も安心して話題を放り込めない。ボケ、ツッコミ、客の役割が成立している安心感が必要。

「雑談」というワードから、そもそも雑談は社会的グルーミング行為なので必要なのだと断言し、技術ロードマップ、メンバー間の関係性と、色んな種類の雑談の必要性を畳みかける、というストーリー。膝を打つシーンが何度かあるんじゃないかと思う。*4

id:onk カクヨムの開発チームではスクラムベースの開発プロセスを回していますが、昼会、スプリント会 (プランニングとレトロスペクティブを同時に行っています) に特徴的な工夫があります。現在のアジェンダに落ち着くまでの変遷と、このチームだから成立している「グルーミング」についてお話しします。

YAPC::Japan::Online 2022

www.slideshare.net

20 分トーク

かなり難産だった。作ったアプリケーションはある *5 んだけど、単発の施策であって、ストーリーが無い。

2 週間ぐらいこねくり回し続けていたら「フロー情報をまとめて放流し直すことで定着させる」が、このアプリケーションでのみ行われているわけではなく、他の施策にも適用されていて、繰り返し構造があることに気づけた。

社内勉強会によるアウトプットも、ブログによるアウトプットも、オープンソース活動も、すべて複数回触れる機会があるようにしている、というのは面白いテーマだろう。

これで

  • アプリの説明
  • 繰り返し構造の説明

で 2 テーマ、もう 1 テーマ欲しいので、最近の技術ブログの動向の一つでもある「ハブ」をくっつけて、全体を「個人か会社かは対立構造ではない」を軸としながら整えた。

僕らはインターネット上で開発の知見を得ることによってサービスを開発・運営できているので、インターネットに還元したい。そんな気持ちから、会社でもアウトプット (登壇や技術ブログ、執筆、OSS 活動等) が推奨されています。 社としての技術ブログも存在しますが、スタッフ個人のブログを通じて発信するのも同じように推奨していきたい。エンジニアの個人ブランディングも大事だと考えているし、自分の場所の方が書きやすいというのも感じているからです。 その上で、社内外の色んなサービスに散らばった技術 Tips を上手くまとめて再放流することで、手軽に情報を摂取し、技術的好奇心を満たし、成長し続けられる環境を用意したい。 そんな、個人の集合体としての技術コミュニティを運営する方法と、そのために開発したアプリケーションについて紹介します。

発表時間とテーマ数

以上のように、僕は 20 分トークでは 3 テーマを盛り込んでいることが多い。

LT だと 1 テーマで駆け抜けられる。20 分トークは LT 4 本分の時間があるので、素朴には 4 つの LT をすれば完成する。が、テーマ間の関連性を示すためにも時間を使いたいので、3 テーマが良いんじゃないかと思っている。

テーマ間の関連が薄いただの羅列だと聞き手がトークの現在位置を見失うので、上手い関連付け (これを特にストーリーと僕は呼んでいそう) が必要だとも考えている。

僕の好きな構成

5 分トークだと

  1. 問題提起
  2. 考えられる解決法
  3. 実際にやったこと
  4. 途中で出会った問題
  5. ふりかえりとまとめ

いわゆる STAR フレームワークに、やった人しか語れない話題をひとつまみ。

20 分トークだと

  1. 問題提起
  2. 問題を 3 つに分解する *6
  3. 問題 A について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  4. 問題 B について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  5. 問題 C について
    1. 考えられる解決法
    2. 実際にやったこと
    3. 途中で出会った問題
  6. それぞれの問題に共通していた特性
  7. ふりかえりとまとめ

特に最後の「それぞれの問題に共通していた特性」がアハ体験になるような、事実を積み上げていったら新たな発見があった!という構成が書いていて/聞いていてテンションが上がる。問題や解決法の再定義とか、つまりどういうことか、とかが決まるとカッコイイ。*7

この流れが好きなのは、聞き手の脳内モデルと一致しやすいと思っているから。聞き手が知っている、想定できる解決策から開始して、思ってなかった着地点に連れて行かれる。着地点はいいワーディングになっていてインパクトが残る。

クライマックスをしっかり盛り上げるような構成が好きです。*8

まとめ

  • 聞き手を置いてきぼりにしない
  • 聞き手をダレさせない
  • 長いトークでも、5 分語れるテーマの組み合わせで考える
  • 羅列ではなく、テーマ間に関連性を見出せるように選ぶ
    • 僕は最終稿の 3 倍の量を文字にしてから、テーマに合うものだけ絞り出している *9
  • 聞き手の感情グラフを考えながら、一本通ったストーリーにする
    • クライマックスに計算した感動を用意する

*1:以前 登壇資料の作り方 - id:onk のはてなブログ にも書いたが、このときはあまり「考えていること」にフォーカスできていなかったので

*2:よく言われる「聞き手に持ち帰って貰いたいものを明確にする」を更にテクニックっぽくするとこの 2 つになりそう

*3:「毛づくろい」をカッコ良く言いたかったんだけど、「グルーミング」によくない意味が乗っかっちゃったので「毛づくろい」とそのまま言う方が望ましそう https://twitter.com/__gfx__/status/1598644538306097155

*4:id:seiunsky が輸入してくれたのは最高の出来事でした チーム内にテックな話題を話す場を作っておよそ半年が経ちました - SmartHR Tech Blog

*5:ブログから技術記事を抽出・集約してワイワイする - id:onk のはてなブログ

*6:やった順にフェーズを分けるのが一番簡単。納得感のある分け方になるように頭を捻る

*7: は綺麗に決まったプレゼンたち

*8:似た話題を id:Pasta-K がしていた。この感情グラフのイメージを僕も意識している Nota Tech Conf 2022 Springの"全員登壇"を支えた技術 - Helpfeel Developers' Blog

*9:懇親会でトークの関連として語れる話題が 2 倍あるので、引き出しの多い人っぽい振る舞いができてお得

今年買ったもの2022

毎年書いていて 4 回目なのでカテゴリ作った。今年買ったもの

さすがにリモートワーク始めて 3 年経つとだいたい揃っちゃったな。今年はあんまり買ってない (と思う)。買った順です。

git push -f が更に安全になる --force-if-includes

歴史改変、してますか?

私は歴史改変が大好きで、毎日 rebase しています。なので割と毎日 git push -f することになっています。

口で -f と言っても、実際には --force-with-lease --force-if-includes をしているので、これらのオプションのご紹介。

この記事は はてなエンジニア Advent Calendar 2022 の 18 日目です。昨日は id:rokoucha さんで 壊れたデータベースとの向きあいかた - rokoucha でした。

qiita.com

-f の危険性

...--F--G--H   <-- main

という状態で push した後、H をコミットし直したとしよう。

...--F--G--H'  <-- main
         \
          H   <-- origin/main

このまま H' (main) を origin/main に push したいが、改変しているので「fast-forward ではない」と怒られます。

$ git push origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (non-fast-forward)
error: failed to push some refs to 'github.com:onk/foo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

ここで push -f をするんですが、もし origin 側に他人のコミット I が更に積まれていたら。

...--F--G--H'  <-- main
         \
          H--I   <-- origin/main

この状態で push -f すると I は消えて無くなります。H を書き換えるつもりはあったが、I を闇に葬るつもりは無かった。

...--F--G--H'  <-- main, origin/main
         \
          H--I   <-- ???

このように、-f はローカルを強制的にリモートに同期するので、安易に使うと共用リポジトリを頻繁に破壊できます。

少し安全になる --force-with-lease

自分の知らないコミットがあると失敗するようになるオプションです。

先ほどと同じように、origin/main 側に気づかないうちに I が積まれている場合。

...--F--G--H'  <-- main
         \
          H--I   <-- origin/main

この状態で push --force-with-lease すると、以下のように怒ってくれます。

$ git push --force-with-lease origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (stale info)
error: failed to push some refs to 'github.com:onk/foo.git'

stale info、古くて使えないと言われています。何を比べて古いのかというと、実はローカルブランチ main、リモートブランチ origin/main だけではなく、ローカルの .git の中に remotes/origin/main というブランチがあり、この remotes/origin/mainorigin/main より古いと怒られているのです。

...--F--G--H'  <-- main
         \
          H    <-- remotes/origin/main
           \
            I  <-- origin/main
  • ローカルブランチ: H'
  • ローカルのリポジトリが知っているリモートブランチ: H
  • リモートブランチ: I

この「ローカルのリポジトリが知っているリモートブランチ」は .git/refs/remotes/origin/ に並んでいるので、眺めてみてください。オススメは .git の中で git init して、 git fetchgit push したときに何が変わるのかを diff の形で眺めてみることです。コミットハッシュへの参照なんだな、というのがよく分かると思います。*1

ローカルのリポジトリが知っているリモートブランチは git fetch で origin と同期されます。

$ git fetch
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 283 bytes | 283.00 KiB/s, done.
From github.com:onk/foo
   8f51241..f953a86  main       -> origin/main

fetch したことで、remotes/origin/mainorigin/main と同じく I を指すようになりました。

...--F--G--H'  <-- main
         \
          H--I  <-- remotes/origin/main, origin/main

ローカルのリポジトリが知っているリモートブランチと実際のリモートブランチとが揃っていると、push --force-with-lease は成功し、 I は闇に消えていきます。

...--F--G--H'  <-- main, remotes/origin/main, origin/main
         \
          H--I  <-- ???

このように、手癖で push -f 前に git fetch してしまった場合等では、--force-with-lease は他人のコミットを守ってくれません。

更に安全になる --force-if-includes

Git v2.30.0 で増えたオプションです。更に安全になります。

...--F--G--H'  <-- main
         \
          H--I  <-- remotes/origin/main, origin/main

--force-if-includes の場合、--force-with-lease では push できてしまっていた remotes/origin/mainorigin/main が揃っている場合でも、reflog に remotes/origin/main (I) が含まれていること、をチェックして撥ねてくれます。面白い条件だと思う。

$ git push --force-with-lease --force-if-includes origin HEAD
To github.com:onk/foo.git
 ! [rejected]        HEAD -> main (remote ref updated since checkout)
error: failed to push some refs to 'github.com:onk/foo.git'
hint: Updates were rejected because the tip of the remote-tracking
hint: branch has been updated since the last checkout. You may want
hint: to integrate those changes locally (e.g., 'git pull ...')
hint: before forcing an update.

ここまでやっておくと、日常的に rebase && push -f していても闇に飲まれる他人のコミットは無くなり、安全になります。

というわけで、口では push -f と言っているけど、安全なチーム開発を意識している人は --force-with-lease --force-if-includes のことを指していて、本当に push -f していることは無いんだよ、という説明でした。

はてなエンジニア Advent Calendar 2022、明日は id:tokizuoh さんです。

参考 URL

*1:この技はドリコムの社内勉強会で id:sue445 に教えて貰いました https://sue445.hatenablog.com/entry/2013/06/26/012409

積極的フィルターバブルで価値観を破壊する

と成長への近道なんじゃないかという仮説。

積極的フィルターバブルとは

resize.fm #100id:nagayama が語っていた概念。44:10 辺りから。

anchor.fm

  • 新しいことを始めるときに、Instagram でサブ垢を作って、その関係の人しかフォローしない
    • タイムラインは全部スケボーになる
  • 自分の価値観が、いかに板に乗って高く飛ぶか、に変わっていく
  • 業界用語や技術のトレンドとかが自然に入ってくる

以前僕も似たようなことを言っていた。

このときは情報を浴びてインデックスを自分の中に持つことを目的としていたが、いわゆる近接性バイアス、接触頻度が価値観に与える影響というものも大きいんだろうな、と思う。

これを聞いたので、僕も Instagram でサブ垢を作って、#kendama タグで検索して、100 人ぐらいフォローして、毎晩眺めるようにした。

けん玉を始めた

ある日 Amazon のオススメ商品に出てきて、オッと思って買っちゃった。

ここからずっとやっている。

日付 つぶやき/できごと
2022-10-16 大皿すらムズいなコレ……。明日絶対足が筋肉痛。
2022-10-18
2022-10-19 大空 REShape3 秋元モデル を購入。3日前に買った大空はオフィスに持っていって、ネスプレッソ&電子レンジの横に置いた。ちょっとした待ち時間に手に取らせる狙い
2022-10-21 同僚から喜びの声「コーヒーけん玉施策いいなと思ったんですけど、できないと一生離れなれなくて困っています」
2022-10-29 稀に日本一周できるようになってきた
2022-11-02 世界一周できないんじゃないか なおこの数日後にできる。
2022-11-05 寝起きになんとなく試したら飛行機初メイクした
2022-11-06 日本一周は安定してきたし世界一周も稀に成功するようになってきた
丸一日やってたら世界一周も確度高くなってきたな
2022-11-06 軽く目標に置いていた世界一周ができるようになったので振り返り。
2022-11-07 Sweets Kendamas けん玉 BOOST RADAR 購入。
2022-11-07 %w(大皿 小皿 中皿 けん).to_a.permutation(3).to_a.shuffle.take(10) を毎日練習するようにしてみようかな
2022-11-08 飛行機分かってきて、お昼だけで 5 回成功してるので、あとは回数こなせば出来るようになるはず
2022-11-11 昨日つばめ返しができるようになったので嬉しくて無限にやっていたが、足を踏ん張っているらしくスネがめちゃくちゃ筋肉痛
2022-11-12 飛行機成功するたびにはねけんの練習してたら稀に入ることはあるんだな。運じゃなく実力で入れたい
2022-11-13 地球回しも運で入ることがある
2022-11-14
2022-11-19 灯台できた
2022-11-20 地球回しを運じゃなく入れられるようになってきたかもしれない
2022-11-21 うぐいすが時々できるようになった
中皿→けんが安定しない(ので県一周も世界一周も安定しない
2022-11-22
2022-11-24 次はすくいけんをやりたいんだけど、今のところ一ミリぐらいしかできる気がしない
2022-11-28 すくいけんは引き続き出来る気がしないが裏ふりけんは5回できた
2022-11-29 灯台→逆落とし成功
2022-12-01 風車の練習してるけど回ったり回らなかったりだ。ペン回しと同じでそのうち無意識にできるようになるんだろうけど……。
2022-12-02 裏ふりけん、4連続までは行くんだけど5回目が何故か入らない。成功率上げていきたい
裏ふりけんができるなら裏飛行機もできるよな。(試したらできた
2022-12-04 一回転飛行機できた
3回できたし、これはできるようになるんじゃないか

今どんな感じ

級位認定の技はできるようになった。灯台のメイク率だけが課題。段の技も徐々にできつつあるので、これ全部やれるようになるところまでは続けるんじゃないか。

あとは全般に 90-95% ぐらいのメイク率と、ストリートけん玉というか、映える感じを手に入れたいなー。せっかくなら一発芸としてやってオーッと言われるようになりたい。レジェンド(飛行機→はやて中皿→天中殺)が確度高くできればこの段階だと思う。

けん玉のオススメ度合い

  • 室内でできる
  • 子どもの頃にできなかった技が今だとできる
    • 身体の使い方が上手くなってるのだと思う
  • 技が無限にあるので「常に何かに挑戦している」というのを維持できる
    • 1ヶ月やったが、まだ山の 2-3% ぐらいの裾野だと思う
  • 2日おきぐらいで新しい技ができるようになるので、成長実感がすごい
  • 無限にスクワットをしているので、下半身の筋力を維持できる
  • 20-30 分でほどよく汗をかく有酸素運動なので健康にも良さそう

と良いことずくめなので、皆さんも始めてみてはいかがでしょうか。

吉祥寺.pm 31 で LT した

kichijojipm.connpass.com

www.slideshare.net

SlideShare 広告がエグくなったんだけど、embed では入らないので一旦アップロード先はコレで)

IPA組織における内部不正防止ガイドライン では

  • 犯行を難しくする(やりにくくする)
  • 捕まるリスクを高める(やると見つかる)
  • 犯行の見返りを減らす(割に合わない)
  • 犯行の誘因を減らす(その気にさせない)
  • 犯罪の弁明をさせない(言い訳させない)

としても語られていたんだけど、機会・動機・正当化の三要素を削減していくことで発生しないようにしていく。これは CI でコードベースを守ってることと近いなぁと思ったので、その気づきを LT として話してきた。

  • 動機
    • 過度の開発速度への期待をしない
  • 機会
    • CI することで検査頻度を高める
  • 正当化
    • CI を定常的に無視する状態を作らない

内部不正防止の研究の歴史に乗っかって、我々は CI で何を守りたかったのかを、体系立てて見つめ直してみるのはいかがでしょうか。

吉祥寺.pm はここ数年よく顔を出しているコミュニティだけど、今回もまたバラエティに富んだ発表があって良かったです。この幅広さがとても魅力だと思う。