Jest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計

みなさんは、テストコードを書いているときに「このケースもテストすべきかな?」「境界値はどこまで確認すればいいんだろう?」と悩んだことはありませんか。通常のユニットテストでは、個別のケースを手動で列挙していくため、どうしてもテストの網羅性に限界があります。
そんな課題を解決する手法として注目されているのが「プロパティベーステスト」です。今回は、Jest と fast-check を組み合わせて、仕様を壊れにくくする設計手法をご紹介します。
背景
従来のユニットテストの限界
従来のユニットテスト(Example-Based Testing)は、開発者が明示的に用意した入力値に対して、期待される出力を検証する手法です。
typescript// 従来のユニットテスト例
describe('add 関数', () => {
it('正の数同士の加算', () => {
expect(add(2, 3)).toBe(5);
});
it('負の数を含む加算', () => {
expect(add(-1, 3)).toBe(2);
});
it('ゼロとの加算', () => {
expect(add(0, 5)).toBe(5);
});
});
このアプローチには以下のような課題があります。
- テストケースの選定が属人的:開発者の経験や知識に依存します
- 網羅性の保証が困難:考慮漏れが発生しやすくなります
- 境界値の見落とし:エッジケースを見逃す可能性があります
- メンテナンスコスト:仕様変更時に多数のテストケースを修正する必要があります
プロパティベーステストとは
プロパティベーステスト(Property-Based Testing)は、テストすべき「性質(プロパティ)」を定義し、ランダムに生成された大量の入力値で検証する手法です。
以下の図で、従来のテストとプロパティベーステストの違いを比較してみましょう。
mermaidflowchart TB
subgraph traditional["従来のユニットテスト"]
dev1["開発者"] -->|手動で選定| cases1["テストケース<br/>例: [2,3], [-1,3], [0,5]"]
cases1 -->|固定値で検証| result1["限定的な検証"]
end
subgraph property["プロパティベーステスト"]
dev2["開発者"] -->|性質を定義| prop["プロパティ<br/>例: add(a,b) = add(b,a)"]
prop -->|自動生成| cases2["大量のランダムケース<br/>数百〜数千パターン"]
cases2 -->|性質を検証| result2["広範囲な検証"]
end
図から読み取れる要点:
- 従来のテストは固定的な入力値で限定的な検証を行います
- プロパティベーステストはプロパティ定義から自動的に多様なケースを生成します
- 検証の広さと深さが大きく異なります
fast-check の役割
fast-check は、JavaScript/TypeScript 向けのプロパティベーステストライブラリです。以下のような特徴があります。
# | 特徴 | 説明 |
---|---|---|
1 | ランダムデータ生成 | さまざまな型のテストデータを自動生成します |
2 | シュリンク機能 | 失敗したテストケースを最小限まで縮小します |
3 | Jest 統合 | Jest との連携が容易で既存テストに組み込みやすいです |
4 | 豊富な Arbitraries | 文字列、数値、配列、オブジェクトなど多様な型をサポートします |
5 | カスタマイズ性 | 独自のデータ生成ルールを定義できます |
課題
テストケース選定の難しさ
開発者が手動でテストケースを選定する際、以下のような課題に直面します。
typescript// パスワード検証関数の例
function validatePassword(password: string): boolean {
// 8文字以上、大文字・小文字・数字を含む
if (password.length < 8) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
return true;
}
このような関数をテストする場合、以下のようなケースを考える必要があります。
typescript// 手動で考えるべきテストケース
describe('validatePassword', () => {
it('有効なパスワード', () => {
expect(validatePassword('Abc12345')).toBe(true);
});
it('7文字(短すぎる)', () => {
expect(validatePassword('Abc1234')).toBe(false);
});
it('大文字なし', () => {
expect(validatePassword('abc12345')).toBe(false);
});
// 他にも多数のケースが必要...
});
課題:
- すべての条件の組み合わせを網羅するのは現実的ではありません
- 特殊文字や空白、絵文字など予期しない入力への対応漏れが発生します
- 仕様変更時にテストケースの追加・修正が必要になります
エッジケースの見落とし
数値計算や文字列操作では、エッジケースの見落としがバグにつながります。
typescript// 配列の平均値を計算する関数
function average(numbers: number[]): number {
const sum = numbers.reduce((acc, n) => acc + n, 0);
return sum / numbers.length;
}
このコードには以下のような問題があります。
typescript// 見落としがちなエッジケース
describe('average 関数のエッジケース', () => {
it('空配列', () => {
expect(average([])).toBe(NaN); // division by zero
});
it('非常に大きな数値', () => {
expect(
average([Number.MAX_VALUE, Number.MAX_VALUE])
).toBe(Infinity);
});
it('負の数を含む', () => {
expect(average([-10, 10])).toBe(0);
});
});
これらのエッジケースをすべて手動で洗い出すのは困難です。
回帰テストの脆弱性
仕様変更時、既存のテストケースが新しい要件をカバーしているか判断が難しくなります。
mermaidsequenceDiagram
participant Dev as 開発者
participant Spec as 仕様
participant Test as テストコード
participant Bug as バグ
Dev->>Spec: 仕様変更
Spec->>Test: テスト修正が必要?
Test-->>Dev: 既存テストは通る
Dev->>Bug: しかし新しいバグが混入
Note over Dev,Bug: 既存テストでは<br/>新要件を検証できていない
図から読み取れる要点:
- 仕様変更後も既存テストが通ってしまう問題があります
- テストケースが新要件をカバーしていない可能性があります
- 潜在的なバグが本番環境まで到達するリスクがあります
解決策
fast-check の導入
まず、プロジェクトに fast-check をインストールします。
bashyarn add -D fast-check
必要なパッケージがインストールされたら、Jest の設定を確認しましょう。
javascript// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// fast-check は追加の設定なしで動作します
};
プロパティの定義
プロパティベーステストの核心は「不変な性質(プロパティ)」を定義することです。先ほどの add
関数を例に見てみましょう。
typescriptimport fc from 'fast-check';
// add 関数の実装
function add(a: number, b: number): number {
return a + b;
}
加算の性質として、以下のようなプロパティを定義できます。
typescriptdescribe('add 関数のプロパティ', () => {
// プロパティ1: 交換法則(a + b = b + a)
it('交換法則を満たす', () => {
fc.assert(
fc.property(
fc.integer(), // 任意の整数 a
fc.integer(), // 任意の整数 b
(a, b) => {
return add(a, b) === add(b, a);
}
)
);
});
});
このテストは、fast-check が自動生成する数百パターンの整数の組み合わせで検証されます。
typescriptdescribe('add 関数のプロパティ(続き)', () => {
// プロパティ2: 結合法則((a + b) + c = a + (b + c))
it('結合法則を満たす', () => {
fc.assert(
fc.property(
fc.integer(),
fc.integer(),
fc.integer(),
(a, b, c) => {
return add(add(a, b), c) === add(a, add(b, c));
}
)
);
});
// プロパティ3: 単位元(a + 0 = a)
it('0 は単位元である', () => {
fc.assert(
fc.property(fc.integer(), (a) => {
return add(a, 0) === a;
})
);
});
});
Arbitraries の活用
fast-check は、さまざまなデータ型を生成する Arbitraries を提供しています。
typescriptimport fc from 'fast-check';
// 基本的な型の Arbitraries
const examples = {
// 整数
integer: fc.integer(),
// 範囲指定付き整数
positiveInt: fc.integer({ min: 1, max: 100 }),
// 浮動小数点数
float: fc.float(),
// 文字列
string: fc.string(),
// 配列
arrayOfNumbers: fc.array(fc.integer()),
};
複雑な型も組み合わせて定義できます。
typescript// ユーザーオブジェクトの Arbitrary
const userArbitrary = fc.record({
id: fc.nat(), // 0以上の整数
name: fc.string({ minLength: 1, maxLength: 50 }),
email: fc.emailAddress(),
age: fc.integer({ min: 0, max: 120 }),
isActive: fc.boolean(),
});
実際のテストで使用する例を見てみましょう。
typescript// ユーザー検証関数
function isAdult(user: { age: number }): boolean {
return user.age >= 18;
}
describe('isAdult 関数', () => {
it('18歳以上のユーザーは成人と判定される', () => {
fc.assert(
fc.property(
fc.record({
age: fc.integer({ min: 18, max: 120 }),
}),
(user) => {
return isAdult(user) === true;
}
)
);
});
it('18歳未満のユーザーは未成年と判定される', () => {
fc.assert(
fc.property(
fc.record({
age: fc.integer({ min: 0, max: 17 }),
}),
(user) => {
return isAdult(user) === false;
}
)
);
});
});
カスタム Arbitraries の作成
ドメイン固有のルールに従ったデータを生成したい場合、カスタム Arbitraries を作成できます。
typescriptimport fc from 'fast-check';
// パスワードの Arbitrary(8文字以上、大小英数字を含む)
const passwordArbitrary = fc
.string({ minLength: 8, maxLength: 20 })
.filter((s) => {
return (
/[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s)
);
});
より効率的な生成方法として、map を使った変換もあります。
typescript// より効率的なパスワード生成
const betterPasswordArbitrary = fc
.tuple(
fc.char().filter((c) => /[A-Z]/.test(c)), // 大文字1文字
fc.char().filter((c) => /[a-z]/.test(c)), // 小文字1文字
fc.char().filter((c) => /[0-9]/.test(c)), // 数字1文字
fc.stringOf(
fc.char().filter((c) => /[A-Za-z0-9]/.test(c)),
{ minLength: 5, maxLength: 17 }
) // 残りの文字
)
.map(([upper, lower, digit, rest]) => {
// 4つの要素をシャッフルして結合
const chars = [upper, lower, digit, ...rest.split('')];
return chars.sort(() => Math.random() - 0.5).join('');
});
このカスタム Arbitrary を使ってテストを書きます。
typescriptdescribe('validatePassword with custom arbitrary', () => {
it('生成されたパスワードは常に有効', () => {
fc.assert(
fc.property(betterPasswordArbitrary, (password) => {
return validatePassword(password) === true;
})
);
});
});
シュリンク機能の活用
fast-check の強力な機能の一つが「シュリンク」です。テストが失敗したとき、失敗を引き起こす最小限の入力値を自動的に見つけてくれます。
typescript// バグを含む関数
function buggySort(arr: number[]): number[] {
// 意図的なバグ: 配列の長さが10以上で失敗する
if (arr.length >= 10) {
throw new Error('Array too long');
}
return [...arr].sort((a, b) => a - b);
}
この関数をプロパティベーステストで検証してみましょう。
typescriptdescribe('buggySort のシュリンク例', () => {
it('ソート後の配列は昇順である', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = buggySort(arr);
// ソート結果が昇順か確認
for (let i = 0; i < sorted.length - 1; i++) {
if (sorted[i] > sorted[i + 1]) {
return false;
}
}
return true;
})
);
});
});
テストが失敗すると、以下のような出力が得られます。
textError: Property failed after 1 tests
{ seed: 123456789, path: "0:0:0:...", endOnFailure: true }
Counterexample: [[0,0,0,0,0,0,0,0,0,0]]
Shrunk 15 time(s)
Got error: Error: Array too long
fast-check は最初にランダムな大きな配列で失敗を検出し、それを「長さ 10 の配列」まで縮小してくれます。
シュリンクのプロセスを図で表すと以下のようになります。
mermaidflowchart LR
initial["初期の失敗ケース<br/>[324, -1, 0, ..., 999]<br/>(100要素)"]
shrink1["シュリンク中<br/>[0, 0, ..., 0]<br/>(50要素)"]
shrink2["シュリンク中<br/>[0, 0, ..., 0]<br/>(25要素)"]
final["最小の失敗ケース<br/>[0,0,0,0,0,0,0,0,0,0]<br/>(10要素)"]
initial -->|縮小| shrink1
shrink1 -->|縮小| shrink2
shrink2 -->|縮小| final
style final fill:#f99,stroke:#333
図から読み取れる要点:
- シュリンクは失敗した大きな入力を段階的に縮小します
- 最終的に問題を再現する最小限のケースを特定します
- デバッグが格段に容易になります
具体例
例 1:文字列処理関数のテスト
ユーザー名をサニタイズする関数を考えてみましょう。
typescript/**
* ユーザー名をサニタイズする
* - 前後の空白を削除
* - 連続する空白を1つにまとめる
* - 空文字列の場合は "Anonymous" を返す
*/
function sanitizeUsername(username: string): string {
const trimmed = username.trim();
if (trimmed === '') return 'Anonymous';
return trimmed.replace(/\s+/g, ' ');
}
従来のユニットテストでは、以下のようなケースを個別に書く必要があります。
typescriptdescribe('sanitizeUsername - 従来のテスト', () => {
it('前後の空白を削除', () => {
expect(sanitizeUsername(' John ')).toBe('John');
});
it('連続する空白を1つに', () => {
expect(sanitizeUsername('John Doe')).toBe('John Doe');
});
it('空文字列は Anonymous', () => {
expect(sanitizeUsername('')).toBe('Anonymous');
});
it('空白のみは Anonymous', () => {
expect(sanitizeUsername(' ')).toBe('Anonymous');
});
});
プロパティベーステストでは、性質を定義してテストします。
typescriptimport fc from 'fast-check';
describe('sanitizeUsername - プロパティベーステスト', () => {
// プロパティ1: 結果に前後の空白は含まれない
it('結果の前後に空白がない', () => {
fc.assert(
fc.property(fc.string(), (username) => {
const result = sanitizeUsername(username);
// Anonymous の場合は前後に空白はない
if (result === 'Anonymous') return true;
// それ以外の場合も前後に空白がないことを確認
return result === result.trim();
})
);
});
});
typescriptdescribe('sanitizeUsername - プロパティベーステスト(続き)', () => {
// プロパティ2: 連続する空白が存在しない
it('連続する空白が存在しない', () => {
fc.assert(
fc.property(fc.string(), (username) => {
const result = sanitizeUsername(username);
return !/\s{2,}/.test(result);
})
);
});
// プロパティ3: 空文字列または空白のみの場合は Anonymous
it('空入力は Anonymous を返す', () => {
fc.assert(
fc.property(
fc.string().filter((s) => s.trim() === ''),
(username) => {
return sanitizeUsername(username) === 'Anonymous';
}
)
);
});
});
typescriptdescribe('sanitizeUsername - プロパティベーステスト(続き2)', () => {
// プロパティ4: べき等性(2回実行しても結果は同じ)
it('べき等性を満たす', () => {
fc.assert(
fc.property(fc.string(), (username) => {
const once = sanitizeUsername(username);
const twice = sanitizeUsername(once);
return once === twice;
})
);
});
});
これらのプロパティテストは、数百から数千のランダムな文字列で自動的に検証されます。
例 2:ソート関数の網羅的テスト
配列をソートする関数の正しさを検証してみましょう。
typescript// ソート関数の実装
function sortNumbers(arr: number[]): number[] {
return [...arr].sort((a, b) => a - b);
}
ソート関数が満たすべきプロパティは複数あります。
typescriptimport fc from 'fast-check';
describe('sortNumbers のプロパティ', () => {
// プロパティ1: ソート後の長さは変わらない
it('要素数が変わらない', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sortNumbers(arr);
return sorted.length === arr.length;
})
);
});
});
typescriptdescribe('sortNumbers のプロパティ(続き)', () => {
// プロパティ2: ソート後の配列は昇順である
it('結果は昇順である', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sortNumbers(arr);
for (let i = 0; i < sorted.length - 1; i++) {
if (sorted[i] > sorted[i + 1]) {
return false;
}
}
return true;
})
);
});
});
typescriptdescribe('sortNumbers のプロパティ(続き2)', () => {
// プロパティ3: ソート後の配列は元の配列と同じ要素を持つ
it('要素の集合が変わらない', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sortNumbers(arr);
const original = [...arr].sort((a, b) => a - b);
// 両方をソートして比較
return (
JSON.stringify(sorted) ===
JSON.stringify(original)
);
})
);
});
});
typescriptdescribe('sortNumbers のプロパティ(続き3)', () => {
// プロパティ4: べき等性(2回ソートしても結果は同じ)
it('べき等性を満たす', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const once = sortNumbers(arr);
const twice = sortNumbers(once);
return (
JSON.stringify(once) === JSON.stringify(twice)
);
})
);
});
});
このテストにより、さまざまなサイズ・内容の配列で正しくソートされることが保証されます。
例 3:API レスポンスのバリデーション
API から返されるデータの形式を検証する関数を考えます。
typescript// ユーザーデータの型定義
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
// バリデーション関数
function validateUserResponse(data: unknown): data is User {
if (typeof data !== 'object' || data === null)
return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
typeof obj.createdAt === 'string' &&
obj.name.length > 0 &&
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj.email)
);
}
このバリデーション関数をプロパティベーステストで検証します。
typescriptimport fc from 'fast-check';
// 有効な User データの Arbitrary
const validUserArbitrary = fc.record({
id: fc.nat(),
name: fc.string({ minLength: 1, maxLength: 100 }),
email: fc.emailAddress(),
createdAt: fc.date().map((d) => d.toISOString()),
});
typescriptdescribe('validateUserResponse', () => {
// プロパティ1: 有効なユーザーデータは検証を通過する
it('有効なデータを受け入れる', () => {
fc.assert(
fc.property(validUserArbitrary, (user) => {
return validateUserResponse(user) === true;
})
);
});
});
typescriptdescribe('validateUserResponse(続き)', () => {
// プロパティ2: 不正なデータは拒否される
it('id が文字列の場合は拒否', () => {
fc.assert(
fc.property(
fc.record({
id: fc.string(), // 数値であるべきところを文字列に
name: fc.string({ minLength: 1 }),
email: fc.emailAddress(),
createdAt: fc.date().map((d) => d.toISOString()),
}),
(invalidUser) => {
return (
validateUserResponse(invalidUser) === false
);
}
)
);
});
});
typescriptdescribe('validateUserResponse(続き2)', () => {
// プロパティ3: name が空文字列の場合は拒否
it('name が空の場合は拒否', () => {
fc.assert(
fc.property(
fc.record({
id: fc.nat(),
name: fc.constant(''), // 空文字列
email: fc.emailAddress(),
createdAt: fc.date().map((d) => d.toISOString()),
}),
(invalidUser) => {
return (
validateUserResponse(invalidUser) === false
);
}
)
);
});
// プロパティ4: 必須フィールドが欠けている場合は拒否
it('フィールドが欠けている場合は拒否', () => {
fc.assert(
fc.property(
fc.oneof(
fc.record({
name: fc.string(),
email: fc.emailAddress(),
}), // id なし
fc.record({
id: fc.nat(),
email: fc.emailAddress(),
}), // name なし
fc.record({ id: fc.nat(), name: fc.string() }) // email なし
),
(incompleteUser) => {
return (
validateUserResponse(incompleteUser) === false
);
}
)
);
});
});
以下の図で、バリデーションのフローを整理してみましょう。
mermaidflowchart TD
input["入力データ(unknown)"]
check1{"オブジェクト型か?"}
check2{"id は number?"}
check3{"name は非空文字列?"}
check4{"email は正しい形式?"}
check5{"createdAt は string?"}
valid["検証成功<br/>(data is User)"]
invalid["検証失敗<br/>(false)"]
input --> check1
check1 -->|No| invalid
check1 -->|Yes| check2
check2 -->|No| invalid
check2 -->|Yes| check3
check3 -->|No| invalid
check3 -->|Yes| check4
check4 -->|No| invalid
check4 -->|Yes| check5
check5 -->|No| invalid
check5 -->|Yes| valid
style valid fill:#9f9,stroke:#333
style invalid fill:#f99,stroke:#333
図から読み取れる要点:
- バリデーションは段階的にチェックを行います
- どこかの段階で失敗すれば即座に false を返します
- すべてのチェックを通過して初めて検証成功となります
例 4:状態遷移のテスト
状態を持つクラスの挙動をテストする場合、プロパティベーステストは特に有効です。
typescript// シンプルなカウンタークラス
class Counter {
private value: number = 0;
increment(): void {
this.value++;
}
decrement(): void {
this.value--;
}
reset(): void {
this.value = 0;
}
getValue(): number {
return this.value;
}
}
このクラスに対して、ランダムな操作列を生成してテストします。
typescriptimport fc from 'fast-check';
// 操作を表す型
type CounterCommand =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' };
// 操作列の Arbitrary
const commandArbitrary = fc.oneof(
fc.constant<CounterCommand>({ type: 'increment' }),
fc.constant<CounterCommand>({ type: 'decrement' }),
fc.constant<CounterCommand>({ type: 'reset' })
);
const commandsArbitrary = fc.array(commandArbitrary, {
minLength: 1,
maxLength: 50,
});
typescriptdescribe('Counter の状態遷移', () => {
it('操作列を実行した結果は予測可能', () => {
fc.assert(
fc.property(commandsArbitrary, (commands) => {
const counter = new Counter();
let expectedValue = 0;
// 各コマンドを実行し、期待値を計算
for (const cmd of commands) {
switch (cmd.type) {
case 'increment':
counter.increment();
expectedValue++;
break;
case 'decrement':
counter.decrement();
expectedValue--;
break;
case 'reset':
counter.reset();
expectedValue = 0;
break;
}
}
// 最終的な値が期待値と一致するか
return counter.getValue() === expectedValue;
})
);
});
});
typescriptdescribe('Counter の不変条件', () => {
// プロパティ: reset 後は必ず 0
it('reset 後の値は常に 0', () => {
fc.assert(
fc.property(commandsArbitrary, (commands) => {
const counter = new Counter();
// ランダムな操作を実行
for (const cmd of commands) {
switch (cmd.type) {
case 'increment':
counter.increment();
break;
case 'decrement':
counter.decrement();
break;
case 'reset':
counter.reset();
break;
}
}
// 最後に reset を実行
counter.reset();
// 値は必ず 0
return counter.getValue() === 0;
})
);
});
});
このテストにより、どのような操作の組み合わせでも Counter が正しく動作することが保証されます。
まとめ
プロパティベーステストは、従来のユニットテストを補完する強力な手法です。fast-check を Jest と組み合わせることで、以下のメリットが得られます。
# | メリット | 詳細 |
---|---|---|
1 | テストの網羅性向上 | 数百〜数千のランダムケースで自動検証されます |
2 | エッジケースの発見 | 開発者が想定していなかった入力パターンを検出します |
3 | 仕様の明確化 | プロパティ定義により関数の本質的な性質が明確になります |
4 | リファクタリングの安全性 | 実装を変更しても性質が保たれることを確認できます |
5 | メンテナンスコストの削減 | 個別ケースではなく性質を定義するため変更に強いです |
導入のステップ:
- fast-check をプロジェクトに追加します
- 既存のテストから 1 つ選び、プロパティに変換してみます
- 新機能の開発時にプロパティベーステストを書く習慣をつけます
- チームでプロパティの設計パターンを共有します
注意点:
- すべてのテストをプロパティベースにする必要はありません
- 従来のユニットテストと組み合わせて使うのが効果的です
- プロパティの定義には慣れが必要ですが、一度理解すれば強力な武器になります
プロパティベーステストを活用して、より堅牢で保守性の高いコードを実現していきましょう。
関連リンク
- article
Jest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計
- article
Jest expect.extend チートシート:実務で使えるカスタムマッチャー 40 連発
- article
Jest を Yarn PnP で動かす:ゼロ‐node_modules 時代の設定レシピ
- article
Jest の TS 変換速度を検証:ts-jest vs babel-jest vs swc-jest vs esbuild-jest
- article
Jest で ESM が通らない時の解決フロー:type: module/transform/resolver を総点検
- article
Jest アーキテクチャ超図解:ランナー・トランスフォーマ・環境・レポーターの関係を一望
- article
Convex で Presence(在席)機能を実装:ユーザーステータスのリアルタイム同期
- article
Next.js の RSC 境界設計:Client Components を最小化する責務分離戦略
- article
Mermaid 矢印・接続子チートシート:線種・方向・注釈の一覧早見
- article
Codex とは何か?AI コーディングの基礎・仕組み・適用範囲をやさしく解説
- article
MCP サーバー 設計ベストプラクティス:ツール定義、権限分離、スキーマ設計の要点まとめ
- article
Astro で動的 OG 画像を生成する:Satori/Canvas 連携の実装レシピ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来