Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

ローカルのgigetテンプレートをテストするためのサーバーを作った

gigetはまだfile:///をサポートしてないので、作ったテンプレートを試したかったらアップロードするしかない。そこで、ローカルにtarballを返すサーバーを立てるようにした。その紹介の記事。

github.com

使い方

  1. サーバーを起動する node server.js {テンプレートディレクトリ}
  2. gigetする npx giget@latest http://localhost:3000/template.tar.gz my-app --install

これだけ。シンプル。

サーバーは動的に生成するtarballと静的に生成したtarballの2種類を返す。用途に合わせて適切な方を選んで使おう。

  • template.tar.gz: リクエスト時にtarballを動的に生成するのでやや遅い。
  • cached.tar.gz: サーバー起動時に生成したtarballを返すのでレスポンスが早い。ただし、サーバー起動後のテンプレートへの変更は反映されない。

『アジャイルに効く アイデアを組織に広めるための48のパターン』を読んだ

マネージャーからおすすめされた本。でも買ったまま2年くらい積んでしまっていた。マネージャー、すみませんでした…

www.maruzen-publishing.co.jp

どういう本か?

タイトルにある通り、アイデアを組織に広めるための48のパターンについて紹介する本。

構成としては大きく3部に分かれている。

  • 第1部と第2部: パターンの使い方と体験レポート
  • 第3部: 48のパターンのリファレンス

1部は48パターンが網羅されるようにストーリー仕立てで書かれており、2部は実際にあった様々な事例についてのレポートになっている。

忙しい場合、 第1部を読むだけでも十分に価値がある。 「確かにこういうやり方ならうまくいきそうだな〜」とか、「自分がやってたこれってこういうパターン名がつくんだな」という発見がある。パターンの詳細は3部にしか書いていないが、パターンの命名がわかりやすいので雰囲気でだいたいわかる。もしわからなければ、そこだけ先に読むのも良いと思う。

誰が読むべきか?

この本はタイトルが示す読者層より、 かなり広い層がターゲットになっている。

  • アイデアを組織に広めたい、布教したい人
  • コミュニティを作る方法を知りたい人
  • 会社内で改善活動を進めようとしている人
  • 起業したい人

布教活動、改善活動などをしている人 にはぜひ読んでほしい。

あと、意外と プログラミングの設計に通ずる箇所も多かった。 「空間を演出する: 思い出すきっかけがなければ、新しいアイデアは忘れ去られてしまう」はドキュメントの管理に通ずるし、194ページの「技術を売るな、仕事上の解決を売りなさい」はプログラマが技術選定でよくやらかすミスだ。

読んでてよかった箇所、面白かった箇所

以下は読んでて面白いと思ったところなどの感想。

小さく試して、成功したらそれを祝い、失敗したら反省して次の糧にしていこう、未来は不確実でアンコントローラブルでどこに辿り着くかは誰にもわからないから、まずは小さくトライしていこうというのはその通りだなと思った。タイトルにある「アジャイルに効く」という部分がそれを表しているが、本当に大事。

成功の確率は助けを求める能力に比例しているものだ (128ページ)

この言葉、めっちゃ良い。実際そう。ひとりでタスクを抱え込んでいては進むことはない。

俺がよく唱えてる「仕事の恥はかき捨て」とだいたい同じことも書いてあってびっくりした。

あなたの最高な武器のひとつは、恥をかくのを恐れないところだ (236ページ)

改善活動をしていると、大なり小なりコミュニティを作って活動していくことになると思うが、その時に「メンバーに自分ごとと思ってもらうことが大事」って書いてあって確かにな…ととても思った。あんまりできてなかったと思う。独善的に1人で全部コントロールするんじゃなくて、ちゃんと協力してもらうの大事。

tmux.confを分割するときはtpmに気をつけよう

ハマって1時間半くらいかかったのでメモ

問題: tpmの設定を行うファイルのパス指定にHOME以外の環境変数が使えない

source-fileを使ってtmux.confを分割するのはよくあるパターンだと思います。

source-file "$HOME/dotfiles/tmux/plugins.conf"
# ~/dotfiles/tmux/plugins.conf
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'

しかし、set -g @plugin しているファイルをsource-fileするときに、パスにHOME以外の環境変数があるとtpmが動かなくなります。

# 指してるファイルは同じなのにDOTDIRの方は動作しない
source-file "$HOME/dotfiles/tmux/plugins.conf"
source-file "$DOTDIR/tmux/plugins.conf"

原因: tpm側が自力でパス解析してるから

tpmはインストール対象のプラグインを、以下のようにして集めます

  1. set -g @plugin をtmux.confから探して列挙する (コード)
  2. source-file先のファイルも展開する(コード)、ただし HOME以外の環境変数は展開しない (コード)
  3. 得られたプラグインリストを元にインストール/更新などを行う

このように、tpmはプラグイン一覧を取得するために、 tmuxを介さずに手動で静的にtmux.confを解析するという荒業 をしています。 (昔のtpm_pluginsでの指定がdeprecatedになってるのを見るに、こうしないといけない事情があったのでしょうが…)

まとめ

というわけでまとめると以下になります。

  • HOME以外の環境変数がsource-fileのパスに入っている場合、set -g @plugin はtmux.conf側で行う
  • 環境変数がHOMEのみであれば、source-file先に記述しても問題ない

tpmのコードを読まないと読み解けない仕様でそこそこ手こずりました…

テストガイドライン level 1

本ガイドラインの目的、目指すところ

  • テストの基本的な書き方、考え方を習得する
  • 原因調査時などに手動確認を減らせるテストを書けるようになる

本ガイドラインの目的外のこと: リファクタしやすくなるようにテストファイルを整備する。QAの代替を目指す。 (level2として書くかも)

テストを書く際の流れ

テストは以下の流れに沿って書く。

  1. テスト分析 (テストで確認したい事項を考え、メソッド名を決める)
  2. テスト実装 (実際のテストメソッドの中身を実装する)
  3. テスト実行 (実際にテストしてみる)

特に テスト分析を実施すること。 いきなりテストの実装から始めない。

1. 分析: テストで確認したい事項を考え、メソッド名を決める

テストの確認事項は 「手動でQAするとしたら何を調べるか?」を基準にまず考える。 そして、それを自動化できるようなソフトウェアテストを作る。

確認事項の悪い例:

  • 青森県の場合 (→その場合に期待されているアプリの挙動がが分からない)
  • method is OK (→同じく期待するアプリの挙動がわからない)
  • 返り値が正しいか? (→仕様が変化して「正しさ」の基準が変わったときに追従できない)
  • 与えられたひらがなに対応するローマ字の配列を返しているか? (→QA観点として大雑把すぎる)

確認事項の良い例:

  • 東北地方の場合送料が1000円か?
  • 選択している項目が6件未満だったら保存できないか?
  • 人数が0名の場合予約できないか? / 人数が1名以上4名以下の場合予約できるか?

悪い例は手動QAするのが難しい。 例えば「返り値が正しいか?」は、具体的にどうなれば正しいのか分からず、手動QAできない。

また、 悪い例のほうはテストがパスしても得られる情報が少ない。 たとえば「青森県の場合」というテストケースがパスしたという情報にどういう意味があるかは、テスト実装を確認しなければ分からない。

良い例のように、テストが何を確認しているのか、テストがどういう挙動を保証するのかを明確にして、情報量を増やすべき。

参考: テストコードにはテストの意図を込めよう #vstat

1つのテストケースで確認する項目は1つに絞る

1つのテストケースで確認する項目は、原則として1つとする。確認項目を絞ると様々なメリットがある(テスト実装が短くなって読みやすくなる、テストを修正しやすくなる、テストを追加するのが容易となるなど)。

例として、ひらがなをローマ字に変換する関数(transformToRoman)のテスト項目を考える。

悪いテスト項目:

  • 与えられたひらがなに対応するローマ字の配列を返しているか?

良いテスト項目:

  • うしろに"な行"か"や行"が続く場合、"ん"がnに変換されるか? 続かない場合はnnに変換されるか?
  • アルファベットが続く場合"っ"はxtuに変換されるか?
  • "っ"は後ろに続くひらがなの子音を含んで変換されるか?

悪い例は観点の粒度が大きく、複数の仕様が包含されている。 良い例では、これを細かい仕様に分割している。

良い例は粒度が細かく、 ローマ字変換の仕様についても明確に読み取ることができる。

(参考: リーダブルコード p.190)

期待値を具体的にする

期待値は「正しいか」や「問題ないか」ではなく、具体的に書く。 「正しいか」「問題ないか」は期待結果が曖昧である。

期待結果があいまいだと、異なる解釈をされる可能性があり、テストコード修正時やテスト失敗時に困る。

  • 悪い例: transformToRoman は正しい返り値を返しているか? → 関数に期待している挙動がわからない
  • 良い例: transformToRoman はアルファベットが続く場合に"っ"をxtuに変換するか? → 関数に期待する挙動が明確。

またそもそも、テストがpassする条件が揃ったときに結果として「正しい」のである。そのため、 「正しいか」という確認事項は何も言ってないのと同じである。

メソッド名について

基本的にメソッド名は、テスト項目を英訳した文章をつける。たとえば「東北地方の場合送料が1000円か?」をメソッド名にすると「calcPostage returns 1000yen for Tohoku region」となる。

describe('calcPostage()', () => {
  // 東北地方の場合送料が1000円か?
  it('returns 1000yen for Tohoku region', () => {
  });
});

このようにすることで、 メソッド名を読むだけで、コードがどういう仕様を満たせているかすぐに分かる。

2. 実装: 実際のテストメソッドの中身を実装する

テスト実装は、テスト分析の際に決めた確認したい事項を確認できるように書く。

気をつけるポイント:

  • カバレッジを上げる際、確認項目にぬけもれがないことを確認する

カバレッジを上げる際、確認項目にぬけもれがないことを確認する

たとえば以下のテストはカバレッジ100%だが重要なアルゴリズムのミスを見落としている。

// スターリンソート(前から見ていってソートされてない要素を粛清する)
const sort = arr => {
  const acc = [];
  for (const x of arr[i]) {
    if (acc.length === 0 || acc[acc.length - 1] <= x) {
      acc.push(x);
    }
  }
  return acc;
};

// 小さい順に並んでいるか?
test('sort() orders elements from smallest to highest', () => {
  const result = sort([0, 1, 3, 2, 4]);
  expect(
    result.every((val, idx) => idx === 0 || result[idx-1] < val )
  ).toBe(true);
});

// ほんとは「ソート前後で要素数が変化してないか?」も必要
// test('sort() preserves number of given array', () => {

このように、カバレッジが100%でも満たしてほしい仕様を網羅できてないことがある。 そのため、カバレッジを上げる際には気をつける (カバレッジはテストされてない領域を探すためにしか使えない!)

参考: ソフトウェアテストの7原則とは? (テストでは欠陥があることしか示せない)

3. 実行: 実際にテストしてみる

実際にテストをするのも大事だ。

たとえば、ソフトウェアが壊れているときにテストが成功しないようになっているか(false negativeになっていないか)を確認してみる。コードを壊してもテストが通ったら確認事項が漏れている。参考: Make Your Test Fail

また、テストケースの順序を入れ替えたときに破綻しないかも確認する。もしテストケースを入れ替えたときにテストがパスしなくなった場合、テスト後のクリーンアップができていない可能性が高い。テスト後に環境が元通りになるようにしておくこと。

参考

next.jsのみで簡易的な多言語対応をする

next.jsのドキュメントには多言語対応のページがあります。

nextjs.org

これを参考にすると、LPやホームページ程度であれば十分に多言語対応することができます。

next.jsだけでも出来ること

  • 文字列を翻訳する
  • アクセス時のaccept-languageヘッダーを見て自動的に言語を振り分ける (middleware.js)
  • SSGで英語版、日本語版ページそれぞれ生成する (generateStaticParams())

あとは途中にコンポーネントが挟まるパターンに対応できれば、簡易的な翻訳としては十分です。

足りないもの: 途中にコンポーネントが挟まる翻訳

途中にコンポーネントが挟まるようなコンポーネントは、このようなコンポーネントです。

// 日本語版
<span><a href="http://example.com">解説</a>を読んでください</span>
//英語版
<span>read <a href="http://example.com">guide page</a></span>

日本語と英語はたいてい語順が異なるため(上の例でもa要素の位置が異なる)、コンポーネントが挟まった翻訳は難しいです。

これを解決するために、RichText というコンポーネントを導入してやります。

function RichText({
  children,
  componentMap,
}: {
  children: string;
  componentMap: Record<string, (children: ReactNode) => JSX.Element>;
}): JSX.Element {
  const result: ReactNode[] = [];

  // [plainText, taggedText, plainText, taggedText, ...] という感じにパースして、taggedTextはコンポーネントで置き換える (["foo", <Link>link</Link> "bar"]みたいな)
  const regex = /<(\w+)>(.*?)<\/\1>/g;
  let lastIndex = 0,
    match;
  while ((match = regex.exec(children)) !== null) {
    const [fullMatch, tag, innerText] = match;
    const { index } = match;

    // plainTextを追加
    result.push(children.slice(lastIndex, index));

    // タグの中身を対応コンポーネントで包む。なければそのままテキストとして表示する
    result.push(componentMap[tag]?.(innerText) ?? fullMatch);

    lastIndex = index + fullMatch.length;
  }
  // 残りのplainTextを追加
  result.push(children.slice(lastIndex));

  return <>{result.filter((x) => typeof x !== 'string' || x.length > 0)}</>;
}

これを使うと上のコンポーネントはこう書けます

const Page = ({params}: params: Promise<{ lang: string }>}) => {
  const dict = await getDictionary((await params).lang);
  return (
    <span>
      <RichText
        componentMap={{
          link: (children) => <a href="http://example.com">{children}</a>,
        }}
      >
        {dict['<link>解説</link>を読んでください']}
      </RichText>
    </span>
  );
};

まとめ: 簡易的な対応ならライブラリなしでできる

このように、RichText さえ追加してやれば ライブラリなしでnext.jsだけでも基本的な翻訳対応をできます。 ホームページやLPくらいであれば十分です。簡易的なアプリも対応できるでしょう。

もちろん、もっと本格的に対応しようと思ったらライブラリを入れたほうがよいです(日時、通貨、複数形、数値の区切りの対応などなど)。しかし、案外next.jsだけでもできるんだなということは覚えておくと役に立つかもしれません。

最後に、本記事をもとに実際に簡易的なi18nを実装したサンプルリポジトリを置いておきます。

GitHub - pandanoir/next-i18n-simple-demo