7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CypressでE2EテストとDB連携の話

Last updated at Posted at 2025-12-20

説明

前回は簡単なローカルサイト作成と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 テーブルを作成します。また、初回だけデモデータを投入します。

取得/追加用の関数を用意

src/db.js
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);

スキーマ作成:

src/db.js
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のエンコードされてるデータを使うべきです。

src/db.js
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));
}

ヘルパー

src/db.js
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の内容をリセット(スキーマは保持)

src/db.js
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です。(デモ用)

src/server.js
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());

非常に単純なトークンストア(デモのみ)

src/server.js
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 });
});

テスト専用のリセットルート(本番環境では有効化しないでください)

src/server.js
if (process.env.NODE_ENV === 'test') {
app.post('/test/reset', (req, res) => {
resetDb();
tokens.clear();
res.json({ ok: true });
});
}

静的ファイル

src/server.js
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/support/e2e.js
テストから素早くシード/クリーンアップ:
Cypress.Commands.add('resetDb', () => {
cy.request('POST', '/test/reset');
});

検証コード

cypress/e2e/items.cy.js
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!」成功する!

Screenshot 2025-12-10 at 17.51.07.png

うらを見ると:

Screenshot 2025-12-10 at 17.49.14.png

ちゃんとAPIを読んで、DBのデータを比べて確認出来ます。

パラメーターを送る時もJWTやエンコード方法を使ってください。

better-sqlite3は、小規模なアプリやCI向けに同期型で高速かつシンプルです。

本番環境では、パスワードのハッシュ化、本格的な認証、適切なDB(Postgres/MySQL)またはORM(Prisma/Drizzle)の使用を検討してください。

/test/resetはNODE_ENV=testの背後に配置し、本番環境では公開しないでください。

ご希望であれば、パッケージ化することも可能です。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?