技術書を読むときは、かすかなフックだけを頭に残してる

こんどこそGo言語の勉強をしてる

k8sやDocker周りでよく見かけるので、Go言語をちゃんと読めたらいいなぁとは何年も前から思ってたのだけど。チュートリアルをやってみたり本を読み始めてみたりしては、すぐに別のことに興味が移ってしまって続いてなくて。でも今回は仕事で触ることになるので、真面目に勉強を始めてしまった

やっぱり必要に迫られるとちゃんと続くので嬉しい。業務時間を使っても勉強してるし、勉強は業務時間の中だけでいいよって言われてるのだけど、欲深いので家でも本を読んだりして楽しんでる。今読んでるLearning Goって本は結構好きな感じなので、読み終わったら感想を書こうかなと思ってる

先月日本語訳が出てる

今日は今の自分の技術書の読み方をメモしておくことにした

技術書を読むとき

だいたい、こんな感じかなぁ

1ターン目

  1. まずは前半を、しっかり読む
    • 前半はわりと易しい+やる気にあふれてるから頑張れる
  2. 後半は、さらっと飛ばして、最後のほうを読む
    • 難しくなってくる+もう頭が疲れてるから集中できない
  3. 読み終わった気になる
    • 後半は全然読めてないけど達成感!

休憩

  • 公式ドキュメントを眺めたり、別の本を読んだりして「わりとわかってるかも!」っていう気持ちを楽しむ。それと、読んだ書籍では触れてなかった部分が触れられていたりして、へーってなったりする

2ターン目

  1. もっかい最初から読む
    • わりと覚えてるのでスラスラ読める
  2. 後半を、じっくり読む
    • 新たな気持ちで楽しむ
  3. それでも頭に入ってこない部分は諦める
    • しばらく手を動かしてから戻ってきたら読めるかなと思って
  4. 読み終わる

3ターン目

  • パラパラと全体を読んでおしまい

かすかなフックだけを頭に残す

僕は記憶力が全然よくないので、全部ちゃんと覚えるのは諦めてる。forの書き方とか、いまだにJavaでも調べて書いてる。毎回「ofだっけ?rangeだっけ?カッコいるっけ?」とかなって、forまで書いてエンター押してあとはIntelliJに任せてたりする

そんな記憶力なので、細かいことを覚えるんじゃなくて、かすかなフックだけを頭に残す感じで読んでる

僕がコードを書いてていちばん困るのは「認識できていない」こと

「それとは別にこういう書き方もある」とか「こういうライブラリがある」とか「こういう書き方はやめておいたほうがいい」とか「この扱いには気をつけないといけない」とか、そういうのを全く知らないと「あれ?こういう場合って気をつけたほうがいいことがあった気がする」って気づくこともできないから質問することもできない

ArrayやSliceからSliceを作ったら同じデータを参照してるとか、appendでcapacityを超えたら別の領域が確保されるとか、値レシーバーやポインタレシーバー周りで気をつけることとか。そういうのを、ちゃんと覚えてなくてもいいから、雰囲気だけ頭の片隅に置いといて「なんか気にしないといけなかった気がする」ってなるようにしておく

細かい書き方は調べれば出てくるし、IntelliJとか Linter が教えてくれるし、もういちど本を開いて読めばいいから、それくらいでいい。ペアプロとかしてると、共有してるのとは別のモニターであたふたと検索したりしないといけなくて、あんまりかっこよくはないんだけど、そういうスキルなので、しょうがない!

そんな感じで、技術書を読むときは、かすかなフックだけを頭に残す感じで読んでる。ちなみに、いまは2ターン目の1のとこ!楽しい!

これまでの仕事のやり方を忘れようとしてる

1年

CircleCI に入って来月で1年になる。あっというまだなぁ。

色々と面白いことが転がってるので全然飽きないし、スキルが高ければ高いほどその面白いものをもっと拾えるので、どんどんスキルアップしていきたい気持ち。

仕事のやり方を忘れようとしてる

その一方で、いま僕は仕事のやり方を忘れようとしてる。これまでずっとやってきた自分の仕事のやり方をいちど忘れてみようと思っているのだ。もっと肩の力を抜いて、リラックスした気持ちで仕事をしてみたい。がんばらないように。

のだけど、これが思ってたより難しい。

つい、がんばってしまう。がんばる方がラクだと思ってしまう。そっちの方がラクならがんばったらいいやん。って思うかもしれないけど。せっかくなので違う働き方に挑戦してみたい。どんな景色が見えるんだろう?ということに興味がある。

過程より結果

今のチームでは、いちばん大切なこととして「プライベートを第一に考えること」がどまんなかに置いてある。

そして、どう仕事をしているかは、マネージャーもチームメイトも誰も気にしていない。それは、個々に任されている。

「プライベートを大切にしたうえで、あなたの持っている力をいちばん発揮できるスタイルで仕事してくれたら、それがチームにとって一番良いからね」と、信頼されている。

どう働いているか、という過程ではなく、チームが目標達成に向かってどういう結果を出せているか、ということの方に注目している。

気づかされたこと

そんなチームで仕事をしていて気づかされたのが、僕は自分自身に「がんばる」という過程を求めてしまっている、ということ。

ちょっと気を抜くと、がつがつ次のチケットをとってしまう。これまで常に前へ前へと走り続けてきてて、それが自分の中の普通になってしまっていたのだなぁ。

念の為言っておくと、この働き方が悪いわけじゃないし、そういうのやめてと言われてるわけでもないし(無理しないでねとは言われてるけど)、僕自身も嫌いではない。

だけど、そうじゃない働き方が、ここにはある。

もっとゆとりを

ということで、意図的にゆとりを入れてみようとしてる。

ベランダのバジルに水をあげたり、早く目が覚めたら早く仕事を始めて早く終わったり、突然知った娘の授業参観に仕事の途中で抜けて参加したり、夏休みの昼間の塾の送り迎えをしたり、妻がお仕事おやすみのときは一緒にお昼ごはんを食べに出かけたり、ちょっと眠いときは横になったり、涼しいときは午後に散歩にでかけたり。そうそう、今日は、気分転換に梅田のコワーキングスペースで少し仕事をしたりしてみた。

タスクに関しても「これはがんばれば3時間ぐらいで終わりそうだなぁ」と思っても「じゃあ、1日かけるつもりでやろう」と思うようにしてる。結局気づいたら集中してしまって3時間ぐらいで終わらせてしまって「よし終わった!・・・あ、違う・・・」ってなってしまったりするし、リラックスして仕事をしようとするとなんか不安になってしまったりするから、まだまだなんだけど。

そういう、過程を気にせずに、じゅうぶんな結果を出すような働き方ができるようになりたいなと思っている。そうするとついでに、もうちょっと周りに優しくできるかなぁと思ったりもしている。

2年目は、ゆとりをもって過ごして、プロジェクトじゃないところに、もうちょっとふみこんで、面白いものをいっぱい拾っていきたいなと思っている。

そんな風に、これまでの仕事のやり方を忘れようとしてる。

CircleCI Config SDK を使って JS でビルド設定を書くー

はいどーも。こんばんは。CircleCI のシーバです

CircleCI Config SDK

何日か前に CircleCI Config SDK が発表されましたー!わーい!やったー!

って喜んでみたものの、どういうことかあんまりよくわかってない!(えー)

ので、とりあえず触ってみることにした。TypeScript や JavaScript で CircleCI の設定ファイルを生成できるみたい

仕組み

例えば Job はこんな感じで定義できる

const testJob = new CircleCI.Job("test", dockerNode.reuse());
testJob.addStep(new CircleCI.commands.Checkout());
testJob.addStep(new CircleCI.commands.Run({
  command: "npm install && npm run test"
}));

TypeScript で SDK が作られてるので型の情報が出てきて便利。同じ感じで Workflow とかも定義して、最終的にはその定義を YAML として書き出すことができる

ほうほうそれで?

書き出すことができるのは分かったけど、どんな風に使うんだろう?って思いながらぼーっとブログを読みながら考えてみた。こういうことかな?

  • SDK を使って設定ファイルを生成する関数を作ると、似たような YAML を何度も手で書かなくて済むようにできる
  • さらに、モジュール化して NPM レジストリにアップロードすることで、その関数やツールを共有できる

ふむふむ。面白いかもしれない

設定ファイル生成のタイミング

静的に config.yml を生成してその生成された設定ファイルをコミットすることもできるし、Dynamic Config と組み合わせて CircleCI の実行時に動的に生成することもできる

静的生成は、プロジェクトの雛形を生成するときに対話形式で config.yml を生成するようなツールを作るのに使えそうかなぁ。動的生成は、実行時の情報を使って設定ファイルを生成できて便利そう

とりあえず触ってみよう

ということで、雰囲気だけは分かったので、やってみよう。動的な生成の方で。まずは、モジュールを作って NPM レジストリにアップロードしてみるー

ブログの記事に書いてあるサンプルにちょこちょこ手を入れてこんな感じになった

const CircleCI = require("@circleci/circleci-config-sdk");
const fs = require('fs');

const nodeConfig = new CircleCI.Config();

// Node executor
const dockerNode = new CircleCI.executors
  .DockerExecutor("cimg/node:lts")
  .toReusable("docker-node");
nodeConfig.addReusableExecutor(dockerNode);

// Test Job
const testJob = new CircleCI.Job("test", dockerNode.reuse());
testJob.addStep(new CircleCI.commands.Checkout());
testJob.addStep(new CircleCI.commands.Run({
  command: "npm install && npm run test"
}));
nodeConfig.addJob(testJob);

// Deploy Job
const deployJob = new CircleCI.Job("deploy", dockerNode.reuse());
deployJob.addStep(new CircleCI.commands.Checkout());
deployJob.addStep(new CircleCI.commands.Run({ command: "npm run deploy" }));
nodeConfig.addJob(deployJob);

// Workflow
const nodeWorkflow = new CircleCI.Workflow("node-test-deploy");
nodeConfig.addWorkflow(nodeWorkflow);

const wfTestJob = new CircleCI.workflow.WorkflowJob(testJob);
nodeWorkflow.jobs.push(wfTestJob);
const wfDeployJob = new CircleCI.workflow.WorkflowJob(deployJob, {
  requires: ["test"], filters: { branches: { ignore: ["/.*/"] } }
});
nodeWorkflow.jobs.push(wfDeployJob);

/**
 * Exports a CircleCI config for a node project
 */
module.exports = function writeNodeConfig(deployTag, configPath) {
  wfTestJob.parameters = {
    ...wfTestJob.parameters, filters: { tags: { only: deployTag } }
  };
  wfDeployJob.parameters.filters.tags = { only: deployTag };
  fs.writeFile(configPath, nodeConfig.stringify(), (err) => {
    if (err) console.error(err);
  })
}

型情報があるから書きやすいのは分かるんだけど、JS で書いて読みやすいのかなぁ?って思ってたら、実際に書いて眺めてみるとわりと悪くない

んで、これを使って試しに設定ファイルを生成してみると

❯ node
Welcome to Node.js v18.2.0.
Type ".help" for more information.
> const writeNodeConfig = require('.');
undefined
> writeNodeConfig('/v.*/', './config.yml')
undefined

こういう YAML ファイルができた

❯ cat config.yml
# This configuration has been automatically generated by the CircleCI Config SDK.
# For more information, see https://github.com/CircleCI-Public/circleci-config-sdk-ts
# SDK Version: 0.0.0-development

version: 2.1
setup: false
executors:
  docker-node:
    docker:
      - image: cimg/node:lts
    resource_class: medium
jobs:
  test:
    executor:
      name: docker-node
    steps:
      - checkout
      - run:
          command: npm install && npm run test
  deploy:
    executor:
      name: docker-node
    steps:
      - checkout
      - run:
          command: npm run deploy
workflows:
  node-test-deploy:
    jobs:
      - test:
          filters:
            tags:
              only: /v.*/
      - deploy:
          requires:
            - test
          filters:
            branches:
              ignore:
                - /.*/
            tags:
              only: /v.*/

ふむふむ。んで、NPM レジストリにアップロードしておいた

それを使うプロジェクト

次は、今アップロードしたモジュールを使って CircleCI を動かすプロジェクトを作る

Dynamic Config を使うので、こういう構成にして

ちょっと分かりにくいけど、.circleci/dynamic の下に、ビルド用の JS プロジェクトが置いてある。下の方にある package.json はビルド対象のプロジェクトのやつ

ビルド用の index.js はこれだけ

const path = require("path");
const writeNodeConfig = require("@bufferings/hello-circleci-config-sdk");

writeNodeConfig("/v.*/", path.join(__dirname, "../dynamicConfig.yml"));

なるほどなぁ。これだけかー

で Dynamic Config の setup 用の config.yml は、こうなる

version: 2.1
orbs:
  continuation: circleci/continuation@0.3.1
  node: circleci/node@5.0.2
setup: true
jobs:
  generate-config:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          app-dir: .circleci/dynamic
      - run:
          name: Generate config
          command: node .circleci/dynamic/index.js
      - continuation/continue:
          configuration_path: .circleci/dynamicConfig.yml
workflows:
  dynamic-workflow:
    jobs:
      - generate-config

CircleCI でプロジェクトを登録して、プロジェクト設定 > Advanced > Dynamic Config をオンにして実行

動いたーヽ(=´▽`=)ノ

感想

  • JS で書くのは思ってたより心地よい。型情報があるのでサクサク書ける
  • NPM レジストリにアップロードして共通化するのはとても便利そう
  • アップロードしない場合でも、たくさんのサービスを持ってて同じような YAML を書きまくってるモノレポみたいな環境だと、いっこ関数を定義しておいてそれをうまく使えばとても楽そう

一方で

  • YAML だと読めば何をするかが分かる安心感があるけど、SDK で生成する場合は結局どうなるのか分かりにくいだろうなと思う
  • なので、あまり複雑なことをやりすぎると手に負えなくなりそう

でも

  • 複雑な処理をうまく関数で包んで隠すことができると、また新しい考えが生まれてきそう

ってところかな。実際に使うときは TypeScript で ES Modules なスタイルで書きたい気持ち

ぜひ使ってみてくださいー!

クソコードと思わない

なんか、あんまりいい感じじゃないなぁって思うコードに出会ったとして、それをクソコードと呼ばないようにはしてたんだけど、いつからか、そもそもクソコードだと思わなくなってる

そのときの、そのコードが書かれた環境があって、それは、その人が持っているスキル以上のことをなんとかしないといけなかったのかもしれないし、めちゃくちゃなスケジュールの中でやらないといけなかったのかもしれないし、お試しで作ったものをそのまま使われちゃったのかもしれない

あんまりいい感じじゃない構造だったとしても、そのコードによってシステムは動いて価値をもたらしていて、そのおかげで僕がそのコードに出会ってるんだから、それはとてもスゴイことだなぁって思う

コードを悪者にして文句を言っても何も変わらないし、僕はエンジニアなのだから、そのコードをより良いコードにすればそれでいい

自分がコードを書くときには少し気をつけたり、あんまりいい感じじゃないコードを書かなくていいような環境を作ったり、今あるコードをより良いコードにしていったり、そういうことができるようにスキルアップしてこうと思う

頭の中のコードを形にするまで

を書いてみる気分

今日の時点での自分のやり方なので、またしばらくすると変わってるかもしれない

僕には、最初に考えたとおりに実装できるようなスキルがないので

コードを書きながら形にしていく感じ

サイズ

だいたい、チケット一枚が、5,6時間で実装できるくらいのサイズになってる

2,3日くらいでレビューまで終わって本番にデプロイすることが多いかな

(基本的にはそれくらいってだけで、1,2週間くらいかかるような長いやつもある)

技術的なフィージビリティチェックとかはこの前に終わってる

実装を頭に思い浮かべる

こんなふうにすれば良さそうかなぁ

このあたりは何パターンか考えられるけどどっちがいいかなぁ

とか頭の中に思い浮かべる

とりあえず動くものを実装

まずは思い浮かべたものが全部つながって動くかをサクッと確認したいので雑に実装する

そして、気づくことがいくつかある

あれ?この場合どうなるんだろう?って仕様のことだったり

使ってみて初めて気づくライブラリのくせだったり

その辺りをつぶしていく

いったん見てもらう

ざっくりと方針が見える状態になってるので

ペアに見てもらったり、プルリクエストをドラフトで出して意見をもらったり

悩んでる部分があったらここで相談しておく

色々仕上げてからよりこの時点で意見をもらっておくほうが楽

これが終わるとだいたいチーム内での認識はそろった状態になる

小さく分割する

読みやすくなるように

ユニットテストしやすいように

小さい関数に分割していく

コードを捨てたりもする

この時点で、いちどコードを全部捨ててしまって、それを見ながら新しく書き直すこともある

変更が大きくなったときとかは、レビューしやすいように小さい単位に分割したりとか

分割するときは、デプロイできる単位で分割する

ログとかエラーハンドリングを入れる

このへんでも気づくことがたくさんある

「あれ?このエラーどう処理する?」って仕様レベルの相談とか

「何か問題があったときにこのログがある方がいいね」とか「メトリクスいるね」とか

そんなこんなで、けっこう、構造に影響がある

なのでこれを後からやろうとすると、ちょっと大変

部品のユニットテストをかく

ここまででも、ちょこちょこユニットテストを書いてたりはするんだけど

部品の関数に対して、ここで細かいケースを書いて仕上げていく

テストしなくても大丈夫だろうってようなやつも、やってみたら「あぁ、思ってたんとちがった!」みたいなのがあるからやめられないw

メインの関数のユニットテストをかく

部品が思ったとおりに動くことが分かってるので

あとは組み合わせて全体が思ったとおりに動くことを確認する

部品のユニットテストを見直す

メインの関数のテストの書きやすさによるけど

もし、そっちで部品のユニットテストも十分にカバーできてたら、部品のユニットテストは削除してしまう

もし、メインの関数のテストが書きにくい場合は、そっちは少なめにして、部品のユニットテストでカバーしたままにしておく

リファクタリングをする

テストが通る状態で、名前を考えたり、置き場所を考えたり

こっちがいいかなあっちがいいかなとか、置いてみて、ちょっと遠くから眺めてみたり

プロダクションコード側のリファクタリングが終わったら

テストコードもリファクタリングしてコーディングは終わり

end-to-end で動作確認

ここまでもちょこちょこ end-to-end で確認したりはしてるけど

ここでしっかり確認しておく

プルリクエストを出す

ドラフトのプルリクエストに変更をプッシュして Ready for review にしたり

新しくプルリクエストを作り直したり

で、みんなにみてもらって意見をもらってマージ

本番環境で確認

そしたら本番環境にデプロイされるので、本番環境で確認して

おしまい

完全にこの流れってわけじゃなくて、こんな感じの流れを気分でいったりきたりしながらコードを書いてる

いまはこんな感じかな

🌀CIclone で CircleCI と IntelliJ IDE を連携させるの便利!

サムライズムから CIclone (サイクロン)というプロダクトが発表された

samuraism.com

🌀CIclone?

JetBrains IDE と CI を連携させるプラグイン

有償プラグインなんだけど、リーズナブルな価格だし、サムライズムでパーソナルライセンスを契約してる場合は追加費用無しで利用できるので、とても良いー。自分はサムライズムでAll Products Packのパーソナルライセンスを契約してるのだ

JetBrains 製品の僕の紹介リンク貼っとくー。割引されるので良かったらどうぞー!そしたら僕も更新のときに割引してもらえるー

はい。ということで、CIclone が CircleCI と連携できるってことなので触ってみた!とても便利そう!

実行結果を通知してくれる

コードを push したら、それをトリガーにして CircleCI でワークフローが実行されるけど、そのワークフローの実行結果を IDE の通知で教えてくれる。push したあとにそのままコードを書いてたら通知がくるので、とても便利

ワークフローの実行結果を見ることができる

通知を受け取った後にも、そのまま IDE の中で、成功・失敗や、実行されたジョブ一覧、そのログまで確認できる。便利

失敗したテストにジャンプすることができる

テストに失敗した場合は、ソースコード上のテストケースにジャンプすることができる。便利

さらに、そこからローカルのテストを実行することもできる。便利

CircleCI の画面にジャンプできる

CircleCI の画面を確認したい場合も、できる。ワークフローやジョブを選択して、↓のボタンを押すと開く。便利

例えば CircleCI のこの画面が開く

パイプラインを実行できる

実行ボタンから、どのブランチで実行するかを指定して、パイプラインを実行できる。便利

ワークフローを再実行できる

実行済みのワークフローを選択すると、再実行ボタンからワークフローを再実行できる。便利

とても便利そう!

はい。ということで IDE の中で CircleCI の通知を受け取ったり、ログを確認したり、再実行したりと、コードを書きながらアプリを切り替えずに色々できるので、とても便利そう!

現時点だと、IDEA を再起動すると CIclone の CircleCI の設定がリセットされてしまう問題があるみたいなのでフィードバックしておこうと思います。それが修正されたらしばらく使ってみる! -> (2022-08-09 更新) 今日修正版がリリースされました!やったー!

いま、Java のプロジェクトを持ってないんだけど、Clojure とか JavaScript のプロジェクトで試してみようかな。まだサポートされていない言語の場合は、コードへのジャンプや UT の実行はできないと思うけど、CI の結果を見たり、通知を受け取ったりすることはできるんじゃないかな?と思うので、それだけでも便利そうーという予感

以下、今回のプロジェクト設定のメモ

hello-ciclone プロジェクトを準備

Spring Initializr で適当に Maven のプロジェクトを作った。ひさしぶりの Java だー

簡単な config.yml を書いて

version: 2.1

jobs:
  build-and-test:
    docker:
      - image: cimg/openjdk:17.0.3
    steps:
      - checkout
      - run:
          name: Build
          command: mvn -B -DskipTests clean package
      - run:
          name: Test
          command: mvn test

workflows:
  sample:
    jobs:
      - build-and-test

CircleCI のプロジェクト一覧から「Set Up Project」ボタンを押して

main ブランチの config.yml でポチッと

そしたら、コミットを push するたびに UT が実行されるようになる

んで、成功するテストと失敗するテストをちょこっと書いといた

これで準備は OK

IDEA の設定

次は、IDEA の設定。↓の作業の前に、サムライズムにライセンスを発行してもらってアクティベートしておいた。30日間のお試し期間があるから、ちょっと触ってみたいなって場合はライセンスなくても大丈夫だと思う

プラグインをインストール。Preferences > Plugins > Marketplace で ciclone って入力したら出てくるのでインストール

IDEA を再起動するように言われるので再起動したら次は CircleCI との連携の設定。Preferences > Tools > CircleCI を開いて

最初にトークンを取得したいので、"Get Token" のリンクから CircleCI のトークン生成画面を開いて

Create New Token で適当な名前をつけてトークンを生成してコピー。それをさっきの画面に貼り付けて "Import from existing project" ボタンをクリック。すると CircleCI のプロジェクト一覧が表示される

さっき作ったプロジェクトを選択するとこうなる。Max Pipeline Size は、取得するパイプラインの履歴の数みたい。デフォルトは5個だけ履歴に表示されるようになってる

"Test" を押して確認

つながったー!ヽ(=´▽`=)ノ

CIclone の View

設定が終わったら画面下の方のタブに CircleCI が出てくるので、左側の更新マークを押すと最新のパイプラインの情報が表示される

あとは、前半で書いたとおりの動きを確認することができる!

(息抜きコーディング)条件分岐をごにょごにょ。それとユニットテスト。

昨日は if の書き方で息抜きコーディングしたんだけど

今日もこんなツイートを見かけたので息抜きをすることにした。if が流行ってるのかな?

元のコード

(関数名は変えちゃった)

export function type1(prevX, prevY, nextX, nextY) {
  const toLeft = prevX - nextX > 0;
  const toRight = prevX - nextX < 0;
  const toTop = prevY - nextY > 0;
  const toBottom = prevY - nextY < 0;

  return {
    horizontal: toLeft ? 'left' : toRight ? 'right' : 'neutral',
    vertical: toTop ? 'top' : toBottom ? 'bottom' : 'neutral',
  };
}

export function type2(prevX, prevY, nextX, nextY) {
  return {
    horizontal: prevX - nextX > 0 ? 'left' : prevX - nextX < 0 ? 'right' : 'neutral',
    vertical: prevY - nextY > 0 ? 'top' : prevY - nextY < 0 ? 'bottom' : 'neutral',
  };
}

export function type3(prevX, prevY, nextX, nextY) {
  let horizontal = 'neutral';
  if (prevX - nextX > 0) {
    horizontal = 'left';
  }
  if (prevX - nextX < 0) {
    horizontal = 'right';
  }

  let vertical = 'neutral';
  if (prevY - nextY > 0) {
    vertical = 'top';
  }
  if (prevY - nextY < 0) {
    vertical = 'bottom';
  }

  return { horizontal, vertical };
}

おもしろいね

感想

もし、こういうコードのプルリクエストが来たら、自分の好みはあるからそれは伝えるけど、「これが自分にとってのベストなんだー!」って理由を説明してくれるなら、別にどれでもいいかなという気持ち

で、自分の好みは?

僕だったら、こうかくかなぁ。昨日の記事に影響受けてる!

export function type3_3(prevX, prevY, nextX, nextY) {
  const horizontal = (() => {
    if (prevX > nextX) return 'left';
    if (prevX < nextX) return 'right';
    return 'neutral';
  })();

  const vertical = (() => {
    if (prevY > nextY) return 'top';
    if (prevY < nextY) return 'bottom';
    return 'neutral';
  })();

  return { horizontal, vertical };
}

メインのコードの話はここでおしまい

ところでUT

今日のお昼にブロッコリーさんの記事をみて、なるほどー。ってなったので、今日は Jest を入れて UT を書くところから始めてみた。というか UT をあーだこーだするのに何時間か使って、実際のコードのリファクタリングは5分くらいで終わってしまったw

その1:最初に書いた UT

Xが左から右に、Yが上から下に増えていく座標系なのねって思いながら、とりあえずパラメタライズドテストでやってみるかーって書いたもの:

import * as targetFunctions from './detectMoveDirection';

describe.each(Object.values(targetFunctions))('%p test', (targetFunction) => {
  test.each`
    prevX | prevY | nextX | nextY | expected
    ${20} | ${20} | ${30} | ${30} | ${{ horizontal: 'right', vertical: 'bottom' }}
    ${20} | ${20} | ${10} | ${30} | ${{ horizontal: 'left', vertical: 'bottom' }}
    ${20} | ${20} | ${20} | ${30} | ${{ horizontal: 'neutral', vertical: 'bottom' }}
    ${20} | ${20} | ${30} | ${10} | ${{ horizontal: 'right', vertical: 'top' }}
    ${20} | ${20} | ${10} | ${10} | ${{ horizontal: 'left', vertical: 'top' }}
    ${20} | ${20} | ${20} | ${10} | ${{ horizontal: 'neutral', vertical: 'top' }}
    ${20} | ${20} | ${30} | ${20} | ${{ horizontal: 'right', vertical: 'neutral' }}
    ${20} | ${20} | ${10} | ${20} | ${{ horizontal: 'left', vertical: 'neutral' }}
    ${20} | ${20} | ${20} | ${20} | ${{ horizontal: 'neutral', vertical: 'neutral' }}
  `('($prevX, $prevY) -> ($nextX, $nextY)', ({ prevX, prevY, nextX, nextY, expected }) => {
    expect(targetFunction(prevX, prevY, nextX, nextY)).toStrictEqual(expected);
  });
});

targetFunctions のところは気にしなくて大丈夫。detectMoveDirection.js ファイルに新しい関数を書いたらそれも全部同じテストを通るようにしただけで、今日の記事のためだけのものなので、これ以降の例では省略する

例えば WebStorm で実行するとこんな感じになる

これくらいの関数だったら、別にこのくらいのテストでもいいかなって思う。んだけど、色々遊んでみた

そうそう、僕はあんまり期待値をテストメソッド名に書かない人です。条件だけ書くことが多い(現在のところ)

その2:ちょっと変えてみる

要らなさそうな情報があるから削ってみようかな

  • prevXprevY は変わらないので、パラメータから除外して値を直接埋め込んでみる
  • expected に Object じゃなくてそれぞれの値を書いてみる

するとこうなった

  test.each`
    nextX | nextY | expectedHorizontal | expectedVertical
    ${30} | ${30} | ${'right'}         | ${'bottom'}
    ${10} | ${30} | ${'left'}          | ${'bottom'}
    ${20} | ${30} | ${'neutral'}       | ${'bottom'}
    ${30} | ${10} | ${'right'}         | ${'top'}
    ${10} | ${10} | ${'left'}          | ${'top'}
    ${20} | ${10} | ${'neutral'}       | ${'top'}
    ${30} | ${20} | ${'right'}         | ${'neutral'}
    ${10} | ${20} | ${'left'}          | ${'neutral'}
    ${20} | ${20} | ${'neutral'}       | ${'neutral'}
  `('(20, 20) -> ($nextX, $nextY)', ({ nextX, nextY, expectedHorizontal, expectedVertical }) => {
    expect(targetFunction(20, 20, nextX, nextY)).toStrictEqual({
      horizontal: expectedHorizontal,
      vertical: expectedVertical,
    });
  });

んー。まぁ悪くはない・・・prevXprevY をなくしたからスッキリはしたものの、どんな値を使ってるの?って一瞬探してしまうから、そんなに変わんないか。どっちかっていうと最初の方が好きかも

その4:意図を書いてみる

今度は意図を書いてみよう:

  describe('Xが増加', () => {
    test('Yが増加', () => {
      expect(targetFunction(20, 20, 30, 30)).toStrictEqual({
        horizontal: 'right',
        vertical: 'bottom',
      });
    });
    test('Yが減少', () => {
      expect(targetFunction(20, 20, 30, 10)).toStrictEqual({
        horizontal: 'right',
        vertical: 'top',
      });
    });
    test('Yが変わらない', () => {
      expect(targetFunction(20, 20, 30, 20)).toStrictEqual({
        horizontal: 'right',
        vertical: 'neutral',
      });
    });
  });
  describe('Xが減少', () => {
    test('Yが増加', () => {
      expect(targetFunction(20, 20, 10, 30)).toStrictEqual({
        horizontal: 'left',
        vertical: 'bottom',
      });
    });
    test('Yが減少', () => {
      expect(targetFunction(20, 20, 10, 10)).toStrictEqual({
        horizontal: 'left',
        vertical: 'top',
      });
    });
    test('Yが変わらない', () => {
      expect(targetFunction(20, 20, 10, 20)).toStrictEqual({
        horizontal: 'left',
        vertical: 'neutral',
      });
    });
  });
  describe('Xが変わらない', () => {
    test('Yが増加', () => {
      expect(targetFunction(20, 20, 20, 30)).toStrictEqual({
        horizontal: 'neutral',
        vertical: 'bottom',
      });
    });
    test('Yが減少', () => {
      expect(targetFunction(20, 20, 20, 10)).toStrictEqual({
        horizontal: 'neutral',
        vertical: 'top',
      });
    });
    test('Yが変わらない', () => {
      expect(targetFunction(20, 20, 20, 20)).toStrictEqual({
        horizontal: 'neutral',
        vertical: 'neutral',
      });
    });
  });

んー。長くなったなぁwでも意図は分かるのはとても良い。実行結果はこうなる

実は、↑の前に、こんなパラメーター化をしてみた↓んだけど、あんまり好きじゃなかったので却下した。これが「その3」

  describe.each`
    caseName           | nextX | expectedHorizontal
    ${'Xが増加'}       | ${30} | ${'right'}
    ${'Xが減少'}       | ${10} | ${'left'}
    ${'Xが変わらない'} | ${20} | ${'neutral'}
  `('$caseName', ({ nextX, expectedHorizontal }) => {
    test.each`
      caseNameY          | nextY | expectedVertical
      ${'Yが増加'}       | ${30} | ${'bottom'}
      ${'Yが減少'}       | ${10} | ${'top'}
      ${'Yが変わらない'} | ${20} | ${'neutral'}
    `('$caseNameY', ({ nextY, expectedVertical }) => {
      expect(targetFunction(20, 20, nextX, nextY)).toStrictEqual({
        horizontal: expectedHorizontal,
        vertical: expectedVertical,
      });
    });
  });

その5:それぞれのケースでいくつかの値をチェック

最初のテストだとあんまり気にならなかったけど、意図を書くと「あぁ、それぞれの意図の中にいくつかのケースを書きたいなぁ」って気持ちになった。ゼロ周りとか、マイナスとか。数値の最大最小値周りは…今日はいいや。みたいなことを考えてこう書いてみた:

  describe.each`
    prevX  | nextX
    ${10}  | ${15}
    ${0}   | ${1}
    ${-1}  | ${0}
    ${-15} | ${-10}
  `('Xが増加($prevX -> $nextX)', ({ prevX, nextX }) => {
    test.each`
      prevY  | nextY
      ${10}  | ${15}
      ${0}   | ${1}
      ${-1}  | ${0}
      ${-15} | ${-10}
    `('Yが増加($prevY -> $nextY)', ({ prevY, nextY }) => {
      expect(targetFunction(prevX, prevY, nextX, nextY)).toStrictEqual({
        horizontal: 'right',
        vertical: 'bottom',
      });
    });
    test.each`
      prevY  | nextY
      ${15}  | ${10}
      ${1}   | ${0}
      ${0}   | ${-1}
      ${-10} | ${-15}
    `('Yが減少($prevY -> $nextY)', ({ prevY, nextY }) => {

...(省略)

んー。色んなパターンがテストできていいけど、読みやすくはないかぁ

その6:もうちょっと読みやすくしてみた

X と Y のパターンを掛け合わせなくてもいいかなぁって思ったので、 組み合わせごとに describe を書いてみた。そして、これでいいかーって気持ちになった

  describe('Xが増加・Yが増加', () => {
    test.each`
      prevX  | prevY  | nextX  | nextY  | expectedHorizontal | expectedVertical
      ${20}  | ${20}  | ${30}  | ${30}  | ${'right'}         | ${'bottom'}
      ${0}   | ${0}   | ${1}   | ${1}   | ${'right'}         | ${'bottom'}
      ${-1}  | ${-1}  | ${0}   | ${0}   | ${'right'}         | ${'bottom'}
      ${-20} | ${-20} | ${-10} | ${-10} | ${'right'}         | ${'bottom'}
    `(
      '($prevX, $prevY) -> ($nextX, $nextY)',
      ({ prevX, prevY, nextX, nextY, expectedHorizontal, expectedVertical }) => {
        expect(targetFunction(prevX, prevY, nextX, nextY)).toStrictEqual({
          horizontal: expectedHorizontal,
          vertical: expectedVertical,
        });
      }
    );
  });

  describe('Xが増加・Yが減少', () => {
    test.each`
      prevX  | prevY  | nextX  | nextY  | expectedHorizontal | expectedVertical
      ${20}  | ${30}  | ${30}  | ${20}  | ${'right'}         | ${'top'}
      ${0}   | ${1}   | ${1}   | ${0}   | ${'right'}         | ${'top'}
      ${-1}  | ${0}   | ${0}   | ${-1}  | ${'right'}         | ${'top'}
      ${-20} | ${-10} | ${-10} | ${-20} | ${'right'}         | ${'top'}
    `(
      '($prevX, $prevY) -> ($nextX, $nextY)',
      ({ prevX, prevY, nextX, nextY, expectedHorizontal, expectedVertical }) => {
        expect(targetFunction(prevX, prevY, nextX, nextY)).toStrictEqual({
          horizontal: expectedHorizontal,
          vertical: expectedVertical,
        });
      }
    );
  });

  describe('Xが増加・Yが変わらない', () => {
    test.each`

...(省略)

実行結果

これでいっかー!

テストを色々書いて楽しんだので

あとは jest で watch オプションをつけてプロダクションコードをリファクタリングするだけだった。watch オプションをつけておくと、ファイルを更新するたびに関係するテストが再実行されるので便利。いっかいコードを書き間違えてテストが落ちたので、おぉ!って思いながら修正したのだ

おもしろかったー!

今回みたいなシンプルな関数に対するテストでは、「その1」で十分だと思う。もうちょっと条件とかが複雑で細かく挙動をチェックしておきたい場合は、「その6」にしたい気持ち

こういうときに、QAエンジニアがいてくれると「どんなケースを残しておくといいのかな?」って相談できて良いんだろうなぁと思うのだった

コードはここに置いといた: https://github.com/bufferings/detect-move-direction-js

おわりー