dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Total TypeScriptのZodチュートリアルでZodに入門した

はじめに

Zod というバリデーションライブラリが非常に流行っているようなので、素振りした。

www.totaltypescript.com

このチュートリアルはたしか Twitter で流れてきて知ったのだが
今見ると Zod の公式ドキュメントからも Resources として紹介されているので、そこそこ信頼していいコンテンツなのだと判断した。

チュートリアルについて

チュートリアルと名がついているが、内容は全 10 問のエクササイズを解くという構成。

あらかじめ型チェックのエラーまたはランタイムのエラーが発生するサンプルコードが問題として用意されており、そのコードを修正しながら Zod の基本的な使い方を学ぶ。 チュートリアルには Zod の使い方の説明は特にないので、チュートリアルの問題を解くために Zod の公式ドキュメントを読んで必要な箇所を理解する、という感じ。

Rust でいう rustlings に似てるなと思った。

Web エディタでも解けるし、 https://github.com/total-typescript/zod-tutorial にもコードが公開されており、git clone して手元で動かしながら問題を解くこともできる。
また、全 10 問といったがリポジトリには隠しステージっぽく 11〜14 問目まである。

各問で学べるのは(自分の理解では)以下。

  1. Runtime Type Checking with Zod
    • 基本。プリミティブな値 (number) に対するスキーマ定義および parse メソッドæ
  2. Verify Unknown APIs with an Object Schema
    • 基本。z.object({ … }) でオブジェクトのスキーマを定義する
    • parse 後はスキーマに定義したkeyのみが残る
  3. Create an Array of Custom Types
    • z.array による配列の定義方法
  4. Extract a Type from a Passer Object
    • z.infer<typeof スキーマ>スキーマから型の取り出し
  5. Make Schemas Optional
  6. Set a Default Value with Zod
  7. Be Specific with Allowed Types
  8. Complex Schema Validation
    • https://zod.dev/?id=strings
    • string は文字列長の min/max だけでなく、 emailurl の形式を強制する便利なメソッドがある
  9. Reduce Duplicated Code by Composing Schemas
    • .extend を利用したオブジェクトの拡張と、 .merge を使ったオブジェクトのマージ
  10. Transform Data from Within a Schema
    • .transform を使うとパース後のデータの変換ができる

終わった後に公式ドキュメントをざっと眺めて、たしかに主要なところはカバーしてるのかなと思った。

学び:Zod の基本

ここからはチュートリアルプラス公式ドキュメントも読んで学んだことをメモ。

Zod の特徴

Introduction に挙げられてるのは以下。

  • Zero dependencies
  • Node.js やすべてのモダンブラウザで動く
  • 小さい:minified + zipped で 8kb
  • イミュータブル
  • 簡潔でチェイン可能なインターフェース
  • 関数型アプローチ: parse, don’t validate (←これリンク先の記事まで見てないのでわからなかった)
  • プレーンなJSでも動く

基本的な使い方

const stringSchema = z.string() のようにプリミティブなスキーマも定義できるが、実際使うとなると多くがオブジェクト形式だと思うのでオブジェクトのスキーマ例を載せる。

import { z } from "zod";

const User = z.object({
  username: z.string(),
});

User.parse({ username: "Ludwig" });

// extract the inferred type
type User = z.infer<typeof User>;
// { username: string }

(コードは Basic usage より引用)

その他、 .shape でオブジェクトの特定のkeyのスキーマを取得できたり、

User.shape.name; // => string schema

.merge .pick .omit partial など TypeScript の型操作的な API は一通り提供されている。

.refine:カスタムバリデーション

独自のバリデーションロジックを実装したい場合は .refine メソッドを使う。

const myString = z.string().refine((val) => val.length <= 255, {
  message: "String can't be more than 255 characters",
});

(コードは公式ドキュメントより引用)

.transform:パースした値の変換

定義したスキーマに続けて .transform というメソッドを呼び出すと、パースした値を変換できる。

const StarWarsPerson = z
  .object({
    name: z.string(),
  })
  .transform((person) => ({
    name: person.name,
    nameAsArray: person.name.split(" "),
  }));


const person = {
  name: "Luke Skywalker",
};
const parsed = StarWarsPerson.parse(person);
console.log(parsed);
// { name: 'Luke Skywalker', nameAsArray: [ 'Luke', 'Skywalker' ] }

Yup との違いは?

現職ではフォームのバリデーションに Yup を使っているので、違いが気になる。
最初、スキーマ定義から型を推論できるのは Zod すごいなって思ったけどそれは Yup でもできるらしい(InferType)。知らなかった。。。

というわけでほとんど違いがわからずにいたが、公式ドキュメントの Comparison > Yup で比較されていた。何点か違いが列挙されているが、

  • Yup はオブジェクトのすべてのフィールドがデフォルトで optional(Zod は required が基本)
  • partial や deepPartial などのメソッドがない
  • promise スキーマがない
  • function スキーマがない
  • union や intersection スキーマがない

というわけで、スキーマの充実度では Zod に分があるよという主張のよう。

また、 Yup の方が型推論がいまいちだ、という比較記事も見かけた。
参考:Reactで使えるバリデーションライブラリを紹介! - bagelee(ベーグリー)

参考リンク

Vercel OG(@vercel/og)でOG画像を動的生成する

こちらの記事を読んで。

以前から https://reading-list.zaki-yama.dev/ という自分のサイトに OG 画像を設定したいな〜と思っていたところに @vercel/og なるライブラリが登場したので試してみた。

※ なお、今までずっと「OGP 画像」だと思ってたんだけど、記事中では OG image と表記されていたので、ここでも「OG 画像」と表記する。

@vercel/og の特徴

冒頭の記事を読む限り、今までは vercel/og-image というサービスが提供されていたが、これには以下のような問題があった。

  • 難しい: Serverless Function 内で Chromium を起動し、 Puppeteer でスクリーンショットを撮る、ということをやっていた。これらのツールのセットアップは実装が難しくしばしばエラーを引き起こしていた
  • 遅い:Chromium を Serverless Function 内で起動するため圧縮し、コールドブート中に解凍される必要があるため、遅い(平均4秒)。結果、ソーシャルカードの生成が遅くなったり壊れたりした

今回発表された @vercel/og はこれらの問題を解決したもので、具体的には以下の特徴がある。

  • 簡単 (Easy):ヘッドレスブラウザ不要。HTML と CSS から OG 画像を生成できる
  • 高速 (Fast): vercel.com/docs で試した結果 vercel/og-image より 5倍程度速かったとのこと

加えて @vercel/og を利用したコードは Next.js アプリケーションと共存でき、 vercel/og-image のように別のところにデプロイする必要がない というのもうれしい。

@vercel/og が使える条件

https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#requirements を読む限り

  • Next.js のバージョンが v12.2.3 以降
  • runtime: 'experimental-edge' を有効にして Edge Runtime を使用する

を満たす必要がある。後者はデプロイ先が Vercel であれば問題にならないが、他だとどうなんだろ。やったことないのでわからない。

試してみる

というわけで自分で運用している Next.js アプリケーションの https://reading-list.zaki-yama.dev に @vercel/og を導入してみる。

ソースコードhttps://github.com/zaki-yama/reading-list

公式ドキュメントとしては以下を読めばよさそう。

簡単な OG 画像を表示する

まずは、ライブラリを導入して静的な OG 画像を表示する。
インストールは以下のコマンドで行う。

$ npm i @vercel/og

続いて、OG 画像生成用の API エンドポイントを用意する。
/pages/api/og.tsx というファイルを作る。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';

export const config = {
  runtime: 'experimental-edge',
};

export default function () {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    },
  );
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#getting-started を引用)

これで npm run dev して http://localhost:3000/api/og にアクセスすると Hello world! という画像が表示される。

最後に、これが OG 画像として表示されるようにする。それには <head> に以下のように meta タグを埋め込めばよい。
(自分は Layout コンポーネント内に定義した)

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og"
/>

テキストを動的に設定する

基本的な使い方がわかったので、次は画像内のテキストを動的に設定してみる。
具体的には、現在開いているページのタイトルを OG 画像に埋め込むようにしたい。

OG Image Examples > Dynamic text generated as image にちゃんとサンプルが用意してある。

まず、 /pages/api/og.tsx 側は以下のように、クエリパラメータでタイトルを受け取るようにする。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';

export const config = {
  runtime: 'experimental-edge',
};

export default function handler(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);

    // ?title=<title>
    const hasTitle = searchParams.has('title');
    const title = hasTitle
      ? searchParams.get('title')?.slice(0, 100)
      : 'My default title';

    return new ImageResponse(
      (
        <div>
            // ...略...
            {title}
        </div>
      ),
      {
        width: 1200,
        height: 630,
      },
    );
  } catch (e: any) {
    console.log(`${e.message}`);
    return new Response(`Failed to generate the image`, {
      status: 500,
    });
  }
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-examples#dynamic-text-generated-as-image から抜粋)

そして、使う側は

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og?title=${postData.title}"
/>

というようにクエリパラメータをページごとに変えて設定してやる。

1点サンプルには載ってなかったこととして、自分は以下のように URLSearchParams をかませるようにした。

// /pages/posts/[id].tsx
export default function Post({
  postData,
}: {
  postData: {
    title: string;
    date: string;
    contentHtml: string;
  };
}) {
  const title = `${siteTitle} ${postData.title}`;
  const searchParams = new URLSearchParams(`title=${title}`);
  return (
    <Layout>
      <Head>
        <title>{title}</title>
        <meta
          property="og:image"
          content={`https://reading-list.zaki-yama.dev/api/og?${searchParams.toString()}`}
          key="og-image"
        />
        <meta property="og:title" content={postData.title} key="og-title" />
      </Head>
      ...

https://github.com/zaki-yama/reading-list/blob/main/pages/posts/%5Bid%5D.tsx#L17-L27

おそらくサイトや記事のタイトルに ' (スペース)が含まれていたせいで Open Graph Debugger で見たときにうまく表示されなかったため、これらの文字は URL エンコードするようにした。

結果

https://reading-list.zaki-yama.dev/posts/2022-09-13 という URL を Twitter や Slack に貼ったときに OG 画像が表示されるようになった。

デザイン性皆無なのはしょうがないが、一旦満足。

2022年7〜9月のふりかえり

前回(4〜6月)のふりかえりを完全に忘れてしまっていた。。。

前回:2022年1〜3月のふりかえり - dackdive's blog

もはや前回何を書いてたのか思い出せないが、このままずるずると書かなくなってしまいそうだったので頑張って書く。

✨ やったこと

CRE (Customer Reliability Engineer) としての活動

年明けぐらいから少しずつ社内で CRE というロールを担当しており、その活動内容をブログに書いた。

業務内容をきちんとした記事で公開するのは初めて。


UoPeople: CS2203 Databases 1

2021-22 T5(2022年6月〜8月)はデータベースの基礎的な講義を受講した。
基礎といっても正規化だなんだと今まできちんとデータベースを学んでこなかった自分には厳しかった。。。そしてこの感想も記事にしてないや。


伸ばしたいことリストを作った

職場の同僚に紹介してもらっていいなーと思ったのだが、自分が今伸ばしたいと思っているスキル(技術に限らない)をリストにして、月1ぐらいで棚卸しするようにした。

今はこんな感じ。
自分は興味関心の移り変わりが激しいので、せめてその月だけは上位のテーマをちゃんと探求する制約になればいいなと思う。
あと、今後はリストの棚卸しと合わせて振り返りもするのがいいんじゃないかと思った。

📝ブログ・登壇

イベント登壇は0件。ブログ執筆は以下。

💬 所感

プライベート

この四半期はなんといっても第三子が産まれたというのが自分の人生における大きなイベントだった。

3人目ともなればいい加減子育ても慣れたもんかなと思ったら全然そうでもなくて日々死にそうになりながら育児と仕事をやっている。
久々の新生児はやっぱりかわいい。

あと、歳をとったせいか今まで以上に体力の衰えを感じる。
具体的には夜22時ぐらいになるとむちゃくちゃ眠い。せっかく時間ができてもプライベートの勉強をやる気力が出ず、なかなかやりたいと思ったことがやれない状況が続いている気がする。
運動を習慣化する、というのが今一番必要な取り組みなのかもしれない。

仕事

仕事面では、現職の業務内容がこれまでにやったことがない領域にチャレンジできていて非常に充実している。
反面、フロントエンドもまだまだだなーと思うことが多くもう少し勉強が必要だと感じる。

学業

仕事の方で学ばないといけないことがたくさんある & 学びたいというモチベーションもあるせいか、UoPeople へのモチベーションが若干下がっている感ある。
8月からの term も結局第一週でリタイヤしてしまったし。。。

11 月からはさすがに受講したい。

💪次の四半期(2022年10〜12月)のテーマ

とりあえず10月は react-testing-library にもう少し詳しくなろうかな。
11月から大学の方も新学期始まるはずなのでまた忙しくなりそう。

💸 買ったもの

めちゃくちゃおもしろかった。

ECS とか学びたくて買ったがまだ積読。。。

React公式ドキュメントの「You Might Not Need an Effect」を読んだ

少し前に話題になっていた以下のドキュメントをようやく読んだ。

beta.reactjs.org

記事を書いた時点でドキュメントはまだベータという位置づけ。

長いので先に目次を。
⭐ は中でも特にへえ〜って思ったやつ。💬 はコメント。

続きを読む

スプレッドシートのピボットテーブルで「総計を表示」しているかどうかをGASで判定する

超小ネタ。

Google スプレッドシートでピボットテーブルを作ったとき、行または列の項目ごとに「総計を表示」するかどうか選択できる。
この値を GAS で取得したい。

なんでこんなこと思ったかというと、総計を表示するかどうかで行・列数が変わるため、処理に分岐が必要だったから。

方法

PivotGroup クラスに totalsAreShown() メソッドがある のでそれを使う。

function getTotalsAreShown() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('foo');

  const pivotTables = sheet.getPivotTables();
  const pivotTable = pivotTables[0];

  // 列の場合。行の場合は getRowGroups()
  const pivotGroups = pivotTable.getColumnGroups();

  pivotGroups.forEach(pivotGroup => {
    console.log(pivotGroup.totalsAreShown());
  })
}

SheetPivotTablePivotGroup という3つのクラスが関係しており、それぞれの関係性は下の図のとおり。

なお、 showTotals(boolean) というメソッドで ON/OFF の設定もできる。

参考リンク

Backyard Hatena「#12 id:missasan に聞くMackerelのカスタマーサクセス」を聴いたメモ

貴重な CRE 事例のお話ということで聴いた。以下メモ。

  • [1:50] CREとしての missasan はどんなことやってるの?
    • MackerelのCREは大きく2軸
      • テクニカルサポート
      • カスタマーサクセス←← missasan さんはこっち
  • [2:30] カスタマーサクセスって言葉も耳慣れないかもだけど、どんなことをやる?
    • サブスクリプションモデルのSaaSだと浸透してきたよね
    • 契約した「後」のお客様に対し、プロダクトを利用することによって得られる成果(=成功)を期待通り、期待以上にする
    • 非エンジニアがやってる会社も多い
  • [4:41] CR”E” なのでエンジニアでもあるんですよね?
    • Mackerelはサーバー監視ツールなのでユーザーはエンジニア
    • プロダクトの特性的に自然とエンジニアの知識が求められる→自然とMackerelのCREはエンジニアに
  • [6:00] Mackerelが提供する「サクセス」
    • 難しい。まさに今定義していってるところ
    • サーバー監視でビジネス上どういう成功につながるか。サーバー監視ってプラスというよりかは守り、保険みたいなイメージなので
    • SREとかDevOpsの知識を取り入れてプラスの面を出していきたい
    • Mackerel使うとこういうふうにステップアップできますよ、という良い世界感を作っていく、ということを考えてる
  • [10:10] 世界観を作っていくためにやっていること
    • いろんなロールの人と話している。SREの方とかと定期的に1on1
    • Mackerelのカスタマーがどう成功していくのかちゃんと考えたい→ジャーニーとか成熟度とか、どういう監視の考え方があるのか
    • 一方で、CREがカスタマーサクセスとして日々お客様と接していて、そこで得たユーザー像を開発チームにもっとインプットしていこうと考えて、インプット先として選ばれたのがデザイナーチーム
      • こういうユーザーがいて、こういうペルソナで、こういうカスタマージャーニーで、を伝える、または一緒に作る会
      • そこで作ったカスタマージャーニーをまたSREにフィードバック
    • motemen: いろんな人と話してるんですね
      • ですね。ほぼ全職種
      • こないだまでは登壇もあったのでマーケと連携したりとか、プリセールスみたいな仕事もやってるのでセールスの人とも話したりとか
  • [15:00] お客様のフロントに立ってお客様の声を聞く立場だから、その声をデザイナーや開発チームにフィードバックしている、と
    • ですね
    • プロダクトチームが「お客様にこうなってほしい」って考えて出した機能、意外とお客様に届かない。そこを「こういう機能ありますよ」って気づいてもらうとか「お客様の場合はこう使うと便利ですよ」みたいなアシストをする
    • 逆にお客様はお客様で「こうなりたい」というものを持っている。両者を近づけるのがCREの役目
  • [17:00] お客様の要望・困りごとを開発チームに届けるのはどうやってやっている?
    • いろんな変遷があったが、最近は開発チームのバックログとは分けて管理。セールスやカスタマーサクセス担当から挙がってくるものをなんでもかんでもストックしていく
    • それだけだと開発チーム全部読んで判断するの難しいので、トピックやセグメントで絞ったものをレポートしてる
      • 月1で共有会。開発チーム全員任意参加
    • トピックで絞る、とは?
      • わかりやすいのでいうと機能
      • あとはお客様がMackerelを使うシーンでの分類。初期設定の話なのか障害対応中なのか障害後のふりかえりなのか
  • [21:20] 要望を開発チームにフィードバックすることで良い変化はあった?
    • 一番うれしい変化は実際に開発されること
      • 当時は「難しいよね」で眠ってたものが掘り起こされてリリースに至った、とか
    • 関連する機能を作るときに「そういえばこういう要望あったな」というのもアイディアの種になると思っている
    • 「ユーザーのことで知りたくなったらCREに聞けば何か返ってくる」という認知ができた
      • CRE、いろいろやってて社内でも何してるかわからなかった
  • [25:15] missasan がエンジニアとして手を動かすことってある?
    • あります
    • お客様向けにMackerelの機能紹介をするとき、デモ用の環境作るのでそのときは手を動かす
    • エンジニアの要素も必要
      • 今後、ビジネス要素の強い部分とエンジニア要素の強い部分とで業務を分けて、それぞれで専門性高い人に入ってもらえるようにするというのも検討している
      • Mackerelユーザーのインフラ事情も多種多様。オンプレ、パブリッククラウド、etc.
  • [29:30] missasan: はてなのSREと一緒に仕事できる、現場の運用やってるエンジニアと会話しながら仕事できるというのがMackerel CREの面白いところかな
  • [30:30] 逆に missasan から僕に聞きたいことありますか?
      1. CREがこうなってくれたらいいな、みたいな期待値あります?
      2. 今 missasan がカスタマーサクセスに興味持たれて、そこに注力してるのは一歩前進してるなーと感じてる
      3. Mackerel使うとこんなふうにビジネス・サービス成長しますよ、を提示できるようになってくと良い
      4. SRE的な知識に裏打ちされたプロダクトビジョンに、お客様の声をうまく反映できるといい

感想

弊社でいうとカスタマーサクセスチームの責務・立ち回りにかなり近いなと感じた。お客様からの声をプロダクトチームにフィードバックするような動きとか。

お客様と日々向き合うことで得られたユーザー像をデザイナーチームと連携してペルソナやカスタマージャーニーを作っていく、という活動はめちゃくちゃいいな〜(小並感

StorybookのInteraction Testをようやく試した

社内の10分勉強会駆動でようやく Storybook の Interaction Test についてまとめることができたので、メモ。

Interaction Test とはなにか

Storybook 上でユーザーのインタラクション(ボタンクリックやフォーム入力など)を再現できる機能。
上の GIF のように Storybook でインタラクション結果を確認できるだけでなく、インタラクションを記述したスクリプト中にアサーションを書いてテストとして機能させることもできる。また書いたスクリプトTest Runner により CLI や CI で実行することも可能。

使い方

公式ドキュメントの

に従う。

また公式ブログの

チュートリアル形式で実際に試せるのでわかりやすい。
(上記のチュートリアルを写経したものを https://github.com/zaki-yama-labs/storybook-interaction-test-example に置いてある)

$ yarn add --dev @storybook/testing-library @storybook/jest @storybook/addon-interactions

で必要なパッケージをインストールした後、Story に対し play function と呼ばれる関数を記述する。

冒頭の GIF に相当する play function は以下。

import React from "react";
import { within, findByRole, userEvent } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import { InboxScreen } from "./InboxScreen";

export default {
  component: InboxScreen,
  title: "InboxScreen",
};

// ...中略...

export const EditTask = Template.bind({});
EditTask.parameters = { ...Default.parameters };
EditTask.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const getTask = (name) => canvas.findByRole("listitem", { name });

  const itemToEdit = await getTask("Fix bug in input error state");
  const taskInput = await findByRole(itemToEdit, "textbox");

  await userEvent.type(taskInput, " and disabled state", { delay: 300 });
  await expect(taskInput.value).toBe(
    "Fix bug in input error state and disabled state"
  );
};

https://github.com/zaki-yama-labs/storybook-interaction-test-example/blob/e116e542b1ba652afd4345c1ee8d6f279b39e965/src/InboxScreen.stories.js#L69-L82

react-testing-library でのテストを書いたことがある人からすると、基本的な書き方は大きく変わらないのがわかる。ただ import しているライブラリは @storybook/xxx になっている。

この play function を書いた状態で該当の Story を開くと、下部のペインに Interactions というタブが追加される。インタラクションは Story を開いたときに自動的に開始されるが、 Interactions タブ内の ◀️ ▶️ をクリックするとコマ送りで確認することもできる。

CLI から実行する

@storybook/test-runner をインストールすると test-storybook というコマンドが使えるようになるので、これを CLI から実行する。このとき、

  • play function が定義されていない Story に対しては:Story がエラーなくレンダリングされるかどうかを検証する
  • play function が定義されている Story に対しては:play function がエラーなく実行され、すべてのアサーションが pass するかどうかを検証する

という挙動になる。

Interaction Test で何がうれしいの?

ユースケースを模したテストを目視で確認しながら書けるのでわかりやすいですね、ということに加え、 VRT(Visual Regression Testing)と組み合わせるとよりおいしい んだろうなと思った。

VRT(Visual Regression Testing)

  • UIを画像として保存しておいて差分検出するスナップショットテストの一種
  • マネジドサービスになったものとしては Chromatic が有名(というか他を知らない)
  • OSS でも reg-suit & storycap というツール + S3 などのオブジェクトストレージ組み合わせると比較的簡単に構築できる

Interaction Test + VRT = ?

  • Storybook だけだと難しい「ユーザー入力に伴うUIの状態」を担保してくれる
  • アサーション書いてないところも含めて意図せずUI崩れてないか担保してくれる

ちなみに、VRT って Interaction Test の操作ちゃんと待ってくれるの?flaky にならない?というのが気になったが、こちらの記事で reg-suite と storycap の作者が特に問題なかったと言及していた。

Storybook の play function と VRT - Qiita

わからないこと

というわけで非常に便利そうではあるが、元々 react-testing-library + Jest で書いてたテストとの棲み分けがどうなるのかはちょっとまだ整理ができていない。
play function 内にアサーションも書けるので、極論すべてのコンポーネントのテストを play function に寄せることもできるのでは?と思ったけどどうなんだろう。そうなると実行速度だけが判断基準か。

takepepe さんのこちらの記事では、逆に play function を定義した Story を Jest に取り込む、とある。この場合あくまでテストは Jest 側に集約するイメージだと理解した。

参考リンク