のこのこかずのこ

10年エンジニアやってるけどいまだになんもわからん

SourceTreeからコミットしたらhuskyのエラーが出た

現象

huskyを導入したプロジェクトで、SourceTreeからgit commitしようと思ったら、エラーが出た。

Unexpected token {
husky - pre-commit script failed (code 1)
Completed with errors, see above

原因

ローカルのNode.jsのバージョンが古いことが直接の原因。(開発は、Docker上で最新のNode.jsを使っていた。)

やったこと

nodenvでNode.jsのバージョンを切り替える

nodenv install 22.14.0
nodenv local 22.14.0
nodenv init

これだけだと、SourceTree上のエラーは解消しない。nodenvのパス($HOME/.nodenv/shims)を通す必要がある。

PATHを .bash_profile に直接記述したり .huskyrcを作ろうとしたり色々試したけど、けっきょく以下が一番簡単そう。

echo $0
bash           # bashを使っていることを確認
echo 'eval "$(nodenv init -)"' >> ~/.bash_profile   # nodenvの初期化設定を追加。zshを使ってる場合は ~/.zshrc に出力する。

これによって、端末ログイン時(SourceTree起動時)に自動的にnodenvのPATHが通る。

SourceTree を再起動すると、huskyがちゃんと動くようになりました。

Vuex storeの非同期な変更を反映する

やりたいこと

非同期(axios)で取得した値をstoreに保存して、リアクティブに反映させたい。

ポイント

  1. storeのstateには初期値をちゃんと設定する。初期値がないと変更が検知されない。
  2. Vuexのstateは自動的にリアクティブになるので、watchとかする必要なし。

実装

storeはVuexのモジュール構造で分割している想定。

① モジュールを作成する。

store/data/moduleData.js というスクリプトを用意して、storeを作る。

export default {
  namespaced: true, // 名前空間
  state: {
    dataStore: {},  // stateの初期値が大事
  },
  getters: {
    get: state => key => {
      return state.dataStore[key];
    },
  },
  mutations: {
    updateAll(state, payload) {
      state.dataStore = { ...payload };
    },
  },
  actions: {
    update({ commit }, payload) {  // データの更新時はこれを呼ぶ
      commit('updateAll', payload);
    },
  },
};

② store.jsにモジュール登録

import moduleData from './data/moduleData';
export default new Vuex.Store({
  getters,
  mutations,
...
  modules: {
    data: moduleData,
  },
});

③ storeの値を更新

どこかしらの.vueで非同期APIからデータを取得し、storeを更新。

<script>
export default {
...
  methods: {
    getData() {
      this.$http
        .get('api/dataList')
        .then(res => {
          this.$store.dispatch('data/update', res.data);   // store更新
        })
        .catch(function () {
          return false;
        });
    },
  },
</script>

④ storeの値をリアクティブに反映

どこかしらの.vueで mapState を使って変更を反映する。

<template>
  <div>
    <p>{{ dataStore }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: {
    ...mapState('data', ['dataStore']) // 自動的にリアクティブになる
  }
};
</script>

思ったより簡単にリアクティブになるのはうれしい☺️

ある日突然 zsh: killed されてDockerが使えなくなった(Mac)

現象

久しぶりにDockerを立ち上げようとしたところ、なぜかkillされてしまい、dockerコマンドがまったく使えなくなっていた😱

zsh: killed  docker compose up -d

Docker Desktopのダッシュボードを確認すると、「Dockerは危険だからゴミ箱に捨てるで!」みたいなポップアップが表示され(スクショ撮り忘れた)、なんやこれと思ったらこちらの現象に遭遇していた。たくさんの方が記事を書いていてくれてありがたかった。

Docker Desktop for Macがエラーで起動できない件(#7527)についての情報 #DockerDesktop - Qiita

結論から言うと、この公式ドキュメントの Upgrade to Docker Desktop version 4.37.2 (recommended) を忠実に実行すれば解決したのだが、そこまでの道のりが意外と苦労したので書いておく。

docs.docker.com

やったこと

新しいDocker Desktop をインストールすれば直るやろ、と思ったが、「"Docker"は壊れているため開けません。」と表示されて何度もインストールに失敗してしまう。 homebrewからのインストールを試してみても同じ。

どうやら徹底的にDockerの影を消す必要があるらしい。ので以下のことをやった。

  • 公式ドキュメントに書いてあるコマンドを実行
 sudo launchctl bootout system/com.docker.vmnetd 2>/dev/null || true
 sudo launchctl bootout system/com.docker.socket 2>/dev/null || true

 sudo rm /Library/PrivilegedHelperTools/com.docker.vmnetd || true
 sudo rm /Library/PrivilegedHelperTools/com.docker.socket || true

 ps aux | grep -i docker | awk '{print $2}' | sudo xargs kill -9 2>/dev/null
  • アクティビティモニタでDocker関連のプロセスが動いていないか検索

  • ps aux | grep -i docker で怪しいプロセスがないか確認 -> VSCodeでDocker関連のプロセスがありそうだったので終了した

  • インストール時のDocker.dmgを削除&ゴミ箱を空にする。マウントも解除する。(これけっこう意味あった気がする)

    これ!

  • PC再起動

ここまでやってやっと、Dockerをインストールすることに成功しました!が、なぜかインストールはできてるのに、起動できない😱

docker version
Client:
 Version:           27.5.1
...(略)
Cannot connect to the Docker daemon at unix:///Users/***/.docker/run/docker.sock. Is the docker daemon running?

で、これの原因がドキュメントに 「Download and install version 4.37.2.」と書いてあるのに、その時点での最新版をインストールしようとしていたからだった……。

結果として、上記のDockerの気配抹消をすべてやった上で、指示通りに 4.37.2. をインストールしたことで解消されました! 4.37.2.をインストール後に、最新版にアップデートは問題なくできました。

教訓:ドキュメントには忠実に従おう👼

半年ぶりに仕事復帰したらVue Devtoolsが使えなくなってた

約半年ほど仕事を休職していまして、最近元の職場に復帰しました!✨

すごく久しぶりに仕事のプロジェクトを立ち上げていつも通りChromeからのVue Devtoolsを使おうとしたら以下のメッセージが…

Migration Tips
Vue DevTools v7 detected in your Vue2 project. v7 only supports Vue3 and will not work.
The legacy version that supports both Vue 2 and Vue 3 has been moved to here, please install and enable only the legacy version for your Vue2 app. If you're still using v5 version, you can install it here.

使えなくなってる〜!

どうやら知らぬ間に、Vue Devtoolsの最新版(v7)ではVue2のプロジェクトでは動かせなくなっていた…。2024年10月からかな。 (仕事のプロジェクトはリプレイスの機会も与えられないままいまだにVue2なのです…。)

大人しく、案内通りにhereをクリックしてレガシーバージョンをインストールしました。

Chrome拡張機能から新しい方を無効にして、レガシーを有効にして、ついでに「詳細」からツールバーに固定して、無事使えるようになりました!

Vue2はもう終わりだ…早くリプレイスさせて……

技術書読んだメモ : 良いコード/悪いコードで学ぶ設計入門

こちらの本を読みました!

もう何年も仕事で超絶レガシーコードを触っていて嫌になることが多々あるので、そして自分もレガシーコードを書いている気がして不安になるので、気になって読みました。

JavaC#のような静的型付けオブジェクト指向言語のクラス設計の話が中心だったので、普段自分が触っているJavaScriptPHPにドンピシャな本ではなかったですが、それでも学ぶことが多かったです。サンプルコードは馴染みのないJavaでしたが、特定の言語に限る話ではないので理解できないことはなく、NG例とOK例がセットで載っているので何が良くて何がダメなのかすらわからない初心者にはとてもわかりやすかったです。普段の仕事ではクラスを書くこともあまりないのですが(ロジックベタ書き)(絶対ダメ)、自分がクラス設計とかオブジェクト指向について完全に誤認していた部分があることが分かって、あ、読んどいてよかった……となりました。言葉だけ知ってて概念をイマイチ理解できていなかったものたちもだいぶクリアになりました。

以下、特に自分が特に誤解してた〜とか知らなかった〜とかやらかしそう〜と思った箇所の備忘録。

不変(イミュータブル)をちゃんと設定する

❌ NGコード これは副作用があるからだめ!

class AttackPower {
  static final int MIN = 0;
  int value;

  AttackPower(int value) {
    if(value < MIN) {
      throw new IllegalArgumentException();
    }
    this.value = value;
  }
  
  void reinForce(int increment) {
    value += increment;
  }
  void disable() {
    value = MIN;
  }
}

主作用というのは、関数(メソッド)が引数を受け取り、値を返すこと。副作用とは主作用以外に状態変更すること。参照透過性(同じ条件(引数)を与えたとき、実行結果が常に等しくなる)を意識すると副作用に気付きやすそう。

インスタンス変数にfinal演算子を付与して不変にする。でもそれだと変更できなくなるので、変更時には変更値を持った新しいインスタンスを生成する。 さらに、引数がintやstringのプリミティブ型ではミスや不正状態を防げないので、型を指定すると良い。

⭕️ Goodコード

class AttackPower {
  static final int MIN = 0;
  final int value;
  // コンストラクタは省略
  AttackPower reinForce(final AttackPower increment) {
    return new AttackPower(this.value + increment.value);
  }
  void disable() {
    return new AttackPower(MIN);
  }
}

出力引数は危険

❌ NGコード

class ActorManager {
  // ゲームキャラの位置を移動する
  void shift(Location location, int shiftX, int shiftY) {
    location.x += shiftX;
    location.y += shiftY;
  }
}

引数のLocationが変更されることが外部からわからない、低凝集になりやすい! データとデータを操作するロジックは同じクラス内に凝集する。

⭕️ Goodコード

class Location {
  final int x;
  final int y;
  Location(final int x, final int y) {
    this.x = x;
    this.y = y;
  }
  Location shift(final int shiftX, final int shiftY) {
    final int nextX = x + shiftX;
    final int nextY = y + shiftY;
    return new Location(nextX, nextY);
  }
}

継承より委譲

継承はよっぽど注意して扱わないと危険!スーパークラス依存を避けるため、下手に継承するより委譲したほうがいい。 委譲とはコンポジション構造にすること。継承するのではなく、privateなインスタンス変数として持ち、呼び出す。

❌ 闇雲な継承

class FighterPhysicalAttack extends PhysicalAttack {
    @Override
    int singleAttackDamage() {
        return super.singleAttackDamage() + 20;
    }
    @Override
    int doubleAttackDamage() {
        return super.doubleAttackDamage() + 10;
    }
}

⭕️ 委譲

class FighterPhysicalAttack {
    private final PhysicalAttack physicalAttack;
    // コンストラクタ省略
    int singleAttackDamage() {
        return physicalAttack.singleAttackDamage() + 20;
    }
    int doubleAttackDamage() {
        return physicalAttack.doubleAttackDamage() + 10;
    }
}

その他もろもろ

  • クラス設計とは、インスタンス変数を不正状態に陥らせないための仕組みづくり。 ≒ コンストラクタにバリデーションを定義すること。
  • ifやswitchの分岐処理を描きそうになったら、まずinterfaceを使うことを検討する!
  • DRY原則において、DRYにすべきはそれぞれの概念単位。同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではない。 ex. 通常割引価格と夏季割引価格はロジックは似ていても流用すべきでない。
  • getter/setter メソッドは開発生産性が良くないコードでよく見られる!よそのクラスを気にしたりいじったりするメソッド構造に陥りやすいので闇雲に実装すべきではない。
  • メソッドはコマンド(=状態の変更)とクエリ(=問い合わせ、状態を返す)のどちらか一方だけを行うよう設計する。
// コマンド
void gainPoint() {
    point += 10;
}
// クエリ
int getPoint() {
    return point;
}

以上のようなポイントを、普段使っているJavaScriptに応用しようと思うと、機能自体がなくて難しいものもある…。けど、調べた感じ、TypeScriptではかなりカバーできそう。(TypeScriptには不変にするためのreadonly プロパティがあったり、interfaceがあったり。) TypeScriptもちゃんと勉強したい!

技術書読んだメモ : フロントエンド開発のためのテスト入門

こちらの本を読みました。とっても勉強になりました!

読もうと思ったわけと感想

テストを導入の必要性はとても感じているけれど、仕事でなかなかテストを書く機会に恵まれず…。 そもそも自分がフロントエンドのテストって何すればいいのかよくわかっていなかったので、提案もままならず…なので、まずは知識をつけたいと思ってこちらの本を読みました。フロントエンドのテストに関して体系的にまとめられている本はほとんどお見かけしないので、本当にありがたいです。

結果、読む前と後ではテストに対する解像度がガラッと変わりました。世の中にはこんなに便利なツールが溢れているのかと!(笑) ツールの情報が与えられて、使い所が理解できれば、テストを書くこと自体はそれほど難しくなさそう、と感じました。(導入のハードルはあるけど)

普段の仕事ではVueを使っているのですが、こちらの本のサンプルコードはReactとNext.jsで書かれています。慣れないReactのコードは読むのに時間がかかりましたが、要所で説明が入るので理解できないことはなかったです。React + Next.jsのプロジェクトってこんな感じなのか〜、と触れるいい機会にもなりました。(やっぱりReactは勉強しなきゃな…。) サンプルコードはGitHubからcloneできるので手元で色々試しながら読めたのも楽しかったです。

以下は、復習も兼ねた備忘録。

Jest

Jest · 🃏 Delightful JavaScript Testing

こちらの本の体感7割くらいが Jestによるテストの解説だったし、後々出てくるStorybookやPlaywrightでもJestを使ったりJestライクなコードだったりするので、まずはJestでコンポーネント単位の単体テストを書けるようになるといいっぽい。

単体テストはロジックのテストしかイメージできていなかったが、jsdom と Testing Library をインストールすることでUIコンポーネントのテストもできることを知った。カバレッジレポートの読み方も章を使って説明が書いてあって、ありがたかった。

テストしやすい単位にコンポーネントを設計することが大事だな〜と感じた。

UIテストで使ったライブラリ
  • jest-environment-jsdom : JestでDOM APIを使用するための環境。jest.config.ts に設定する。
  • @testing-library/jest-dom : UIコンポーネント用カスタムマッチャーの拡張。
  • @testing-library/react : renderでコンポーネントレンダリングしたり、screenで要素を取得したり、fireEventでDOMイベントを発火させたり。
  • @testing-library/user-event : キーボード入力など、実際のユーザー操作により近いシミュレートができる。
モック化

Jestのモックはいろんな手法があってちょっと混乱しがち。適切に使えるようになりたい。

  • jest.fn() : 無からモック関数を生み出せる。適当なコールバック関数を指定したい時とかに便利。
  • jest.spyOn() : 既存の関数(メソッド)を監視して実行情報を記録する。モックとして動作を上書きすることも可能。
  • jest.mock() : importした外部モジュールをまるっとモック化する。
  • MSW (Mock Service Worker) : ネットワークレベルのモックを実現するライブラリ。特定のWeb APIリクエストをインターセプトし、レスポンスを任意の値に書き換えることができる。これ使いこなせたらローカルのテストが捗りそう。
  • next-router-mock : Next.jsのRouterに関連するテストを書くためのライブラリ。 setCurrentUrl で現在のURLを仮定できたりする。

Storybook

Storybook: Frontend workshop for UI development

これめちゃくちゃ便利! コンポーネントの状態毎に登録できるのでせっかくコンポーネントを作ったのに誰にも使われない…という事態が減りそうだし、開発途中にデザイナーさんとかと認識合わせるのにもとても便利そう。コンポーネントの一覧画面かと思いきや、テストもできる。

ディレクトリで npx storybook init コマンド一発打つだけで始められて、サンプルコードもあるので、後述の GitHub Actionsとか試してみる時のプレイグラウンドとしても使いやすかった。

使ってみたアドオン
  • @storybook/addon-essentials : 標準インストール時に追加される便利アドオン。Controls、Actions、Viewportなどのデバッガーが使える。
  • msw-storybook-addon : StorybookでMSWを使う時のアドオン。
  • storybook-addon-next-router : StorybookでNext.jsのRouterの設定をしたい時のアドオン。
  • @storybook/addon-a11y : アクセシビリティの検証をしてくれる。
  • @storybook/test-runner : CLIとかCIでStoryのテストができる。Storybookでついでにテストしたい時に便利。
  • @storybook/testing-react : StoryをJestのテストに読み込んで、コンポーネントを再利用できる。

GitHub ActionsでVRT(ビジュアルリグレッションテスト)を自動化

GitHubリポジトリにpushした時に、Storybookのキャプチャ画像を比較して差分のレポートを受け取るVRTを自動化した。比較するのは、AWS S3に保存した過去のキャプチャ画像。 ハンズオンでできて楽しかった!おお、動いた!という感じ。これがCIか…!

やったことをざっとメモ
  1. Storycapをインストール。Storybookに登録したStoryの画像キャプチャを撮るツール。
  2. GitHubリポジトリ作成&初期push。
  3. AWS S3で新規バケットを作成。アクセス設定が必要。
  4. AWS IAMでS3にアクセスできるユーザーを作成。
  5. reg-suit をインストール。インストール時にプラグイン選択。reg-keygen-git-hash-pluginreg-publish-s3-plugin はコミットハッシュ値命名されたスナップショットと検証結果レポートをAWS S3に転送する。reg-notify-github-plugin は検証結果をプルリクエストに通知する。reg-suit GitHub Appをリポジトリにインストールする作業もある。
  6. GitHubリポジトリのActions Secretsにクレデンシャル情報を設定。
  7. GitHub Actionsのworkflowを書いてpushする。Storybookをビルドして、Storycapを実行して、reg-suitを実行する感じ。

ちなみに、ローカルで簡易的にキャプチャ比較とかしたい場合は、reg-suitよりもreg-cliが向いてるようだ。

Playwright

Fast and reliable end-to-end testing for modern web apps | Playwright

ブラウザオートメーションでE2Eテストできる。Dockerとかで動かせる環境があればいいので、導入しやすそう。DBのseederとかはちゃんと用意する必要があるけど。 npx playwright show-report で見れるレポートがわかりやすいし、--debugで実行した時のデバッグが楽しい。テストじゃなくても単にブラウザオートメーションとして使っても良さそう。

いつか仕事で使えたらいいな〜!

jest.fn() と jest.spyOn() の用法の違い

Jest で書かれた複雑なテストコードを読んでいると、 モック関数である jest.fn()jest.spyOn() の用法で混乱してしまうので、超シンプルなコードで要点を押さえておく。自分用メモ。

jest.fn()

test("jest.fn", () => {
    const mockFn = jest.fn();
    mockFn(1,2);  
    expect(mockFn).toHaveBeenCalledWith(1,2);
  });
  • 無からモック関数を生み出せる。
  • 適当なコールバック関数を指定したい時とかに便利。
  • .mockReturnValue() .mockImplementation() などで挙動のカスタマイズ可能。

jest.spyOn()

const obj = { add: (a: number, b: number): number => a + b };

test("spyOn", () => {
    const spy = jest.spyOn(obj, "add");
    obj.add(1,2);
    expect(spy).toHaveBeenCalledWith(1,2);
  });
  • 既存の関数(メソッド)を置き換える。ので、あくまでも元となる関数(メソッド)が必要。
  • スパイされた元の関数(メソッド)が呼び出されるたびに、その情報が記録される。基本的には監視目的として使う。
  • モックとして使いたい場合は、.mockImplementation()などで挙動のカスタマイズも可能。

ちなみに、importしたモジュールを spyOn したい場合は、 jest.mock()でモジュールをまるっとモック化する必要があるみたい。

import * as util from ".";

 jest.mock(".");   // これがないと TypeError: Cannot redefine property エラーになる

test("spyOn", () => {
    const spy = jest.spyOn(util, "add");
    util.add(1,2);
    expect(spy).toHaveBeenCalledWith(1,2);
  });