説明
前回は簡単なローカルサイト作成とCypressの説明をしましたが、今回はその続きになります。
小規模なサイトとCypressの組み合わせには、SQLiteが最適です。セットアップ不要、ファイルベース、ローカル環境でもCIでも動作します。以下は、メモリ内データを置き換え、シンプルなテストリセットを追加する、Node.js + Express + SQLiteの最小限の例です。
インストール
$ npm i better-sqlite3
DBの準備
DB層を作成(src/db.js)、SQLiteファイルを作る、DB層を作成(src/db.js)し、SQLiteファイルを作り、users / items テーブルを作成します。また、初回だけデモデータを投入します。
取得/追加用の関数を用意
import Database from 'better-sqlite3';
import fs from 'node:fs';
import path from 'node:path';
const DB_PATH = path.join(process.cwd(), 'data.sqlite');
const firstRun = !fs.existsSync(DB_PATH);
export const db = new Database(DB_PATH);
スキーマ作成:
db.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
`);
初回実行時にシード
テストのためにハードコーディングし、DBのエンコードされてるデータを使うべきです。
if (firstRun) {
const seedUser = db.prepare(
`INSERT INTO users (email, password, name) VALUES (?, ?, ?)`
);
seedUser.run('demo@example.com', 'password123', 'Demo User');
const seedItem = db.prepare(`INSERT INTO items (name) VALUES (?)`);
['Sample Item A', 'Sample Item B', 'Sample Item C'].forEach(n => seedItem.run(n));
}
ヘルパー
export const findUser = db.prepare(
`SELECT id, email, name FROM users WHERE email = ? AND password = ?`
);
export const listItems = db.prepare(`SELECT id, name FROM items`);
export const insertItem = db.prepare(`INSERT INTO items (name) VALUES (?)`);
テスト用: DBの内容をリセット(スキーマは保持)
export function resetDb() {
db.exec(`DELETE FROM users; DELETE FROM items; VACUUM;`);
const seedUser = db.prepare(
`INSERT INTO users (email, password, name) VALUES (?, ?, ?)`
);
seedUser.run('demo@example.com', 'password123', 'デモユーザー');
const seedItem = db.prepare(`INSERT INTO items (name) VALUES (?)`);
['Sample Item A', 'Sample Item B', 'Sample Item C'].forEach(n => seedItem.run(n));
}
サーバー更新 (src/server.js)
これまでのメモリ配列を削除して、db.jsの関数を呼びます。また、認証後のトークンは簡易なメモリ保持のままでOKです。(デモ用)
import express from 'express';
import path from 'path';
import cors from 'cors';
import { fileURLToPath } from 'url';
import { findUser, listItems, insertItem, resetDb } from './db.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(cors());
app.use(express.json());
非常に単純なトークンストア(デモのみ)
const tokens = new Set();
app.post('/api/login', (req, res) => {
const { email, password } = req.body || {};
const user = findUser.get(email, password);
if (!user) return res.status(401).json({ error: '認証情報が無効です' });
const token = Buffer.from(`${user.id}:${Date.now()}`).toString('base64');
tokens.add(token);
res.json({ token, user });
});
function requireAuth(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7): null;
if (!token || !tokens.has(token)) return res.status(401).json({ error: 'Unauthorized' });
next();
}
app.get('/api/items', requireAuth, (req, res) => {
res.json({ items: listItems.all() });
});
app.post('/api/items', requireAuth, (req, res) => {
const name = (req.body?.name || '').trim();
if (!name) return res.status(400).json({ error: 'name is required' });
const info = insertItem.run(name);
res.status(201).json({ id: info.lastInsertRowid, name });
});
テスト専用のリセットルート(本番環境では有効化しないでください)
if (process.env.NODE_ENV === 'test') {
app.post('/test/reset', (req, res) => {
resetDb();
tokens.clear();
res.json({ ok: true });
});
}
静的ファイル
app.use(express.static(path.join(__dirname, '..', 'public')));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server http://localhost:${PORT}`));
(オプション) Cypress ヘルパー
テストから素早くシード/クリーンアップ:
Cypress.Commands.add('resetDb', () => {
cy.request('POST', '/test/reset');
});
検証コード
describe('Items with DB', () => {
beforeEach(() => {
// アプリは NODE_ENV=test で起動されているため、/test/reset が有効です
cy.request('POST', '/api/login', {
email: 'demo@example.com',
password: '***********'
}).then(({ body }) => {
window.localStorage.setItem('token', body.token);
});
cy.resetDb(); // クリーンでシード済みの DB であることを確認します
});
it('lists seeded items', () => {
cy.visit('/');
cy.intercept('GET', '/api/items').as('getItems');
cy.get('#load-items').click();
cy.wait('@getItems');
cy.contains('Sample Item A').should('be.visible');
});
});
テスト中は、リセットルートをオンにしてアプリを実行してください。
NODE_ENV=test npm run dev
# その後、別のターミナルでCypressを実行してください。
npm run cypress:open
実施コード
1回目はローカル
Cypressの中ではなくて、ローカルのブラウザの動作確認しましょう。
http://localhost:3000 にアクセスするといつもの画面が表示されます。
ログインすると、「Welcome, Demo User!」成功する!
うらを見ると:
ちゃんとAPIを読んで、DBのデータを比べて確認出来ます。
パラメーターを送る時もJWTやエンコード方法を使ってください。
注
better-sqlite3は、小規模なアプリやCI向けに同期型で高速かつシンプルです。
本番環境では、パスワードのハッシュ化、本格的な認証、適切なDB(Postgres/MySQL)またはORM(Prisma/Drizzle)の使用を検討してください。
/test/resetはNODE_ENV=testの背後に配置し、本番環境では公開しないでください。
ご希望であれば、パッケージ化することも可能です。

