4
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の自動テストする

Last updated at Posted at 2025-11-19

Cypressとは

CypressでE2E(End-to-End)テストは「ユーザーがブラウザで行う操作を自動化して、アプリを“本物の環境に近い形”で丸ごと検証するテスト」です。
ブラウザを立ち上げて、ページ遷移・入力・クリック・ネットワーク通信・表示結果までを一連で確認します。

image.png

インストール方法

$ npm install cypress --save-dev

GUIはこちらです:

Screenshot 2025-11-07 at 13.44.22.png

準備

簡単な触るために、簡単のサイトを準備しましょう

テスト用のappを作りましょう :

/ 
    cypress/
      e2e/
        login.cy.js
        items.cy.js
        smoke.cy.js
    src/
        server.js
    public/
        app.js
        index.html
    cypress.config.js
    package.json

/cypress.config.js :

import { defineConfig } from 'cypress'
export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.js',
    supportFile: false,
    retries: { runMode: 2, openMode: 0 },
    video: true,
  },
})

/src/server.js :

import express from 'express';
import path from 'path';
import cors from 'cors';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
app.use(cors());
app.use(express.json());

const USERS = [{ id: 1, email: 'demo@example.com', password: 'password123', name: 'Demo User' }];
const ITEMS = [{ id:1,name:'Sample Item A'},{ id:2,name:'Sample Item B'},{ id:3,name:'Sample Item C'}];

app.post('/api/login', (req,res) => {
  const { email, password } = req.body || {};
  const u = USERS.find(x => x.email===email && x.password===password);
  if (!u) return res.status(401).json({ error: 'Invalid credentials' });
  const token = Buffer.from(`${u.id}:${Date.now()}`).toString('base64');
  res.json({ token, user:{ id:u.id, email:u.email, name:u.name } });
});

app.get('/api/items', (req,res) => {
  const auth = req.headers.authorization || '';
  if (!auth.startsWith('Bearer ')) return res.status(401).json({ error:'Unauthorized' });
  res.json({ items: ITEMS });
});

app.use(express.static(path.join(__dirname, '..', 'public')));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server http://localhost:${PORT}`));

/public/app.js :

const loginForm = document.getElementById('login-form');
const loginInfo = document.getElementById('login-info');
const itemsBtn = document.getElementById('load-items');
const itemsList = document.getElementById('items-list');

const setToken = t => localStorage.setItem('token', t);
const getToken = () => localStorage.getItem('token');

loginForm.addEventListener('submit', async (e) => {
  e.preventDefault();
  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;
  const res = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ email, password }) });
  if (!res.ok) return loginInfo.textContent = 'Login failed';
  const data = await res.json();
  setToken(data.token);
  loginInfo.textContent = `Welcome, ${data.user.name}!`;
});

itemsBtn.addEventListener('click', async () => {
  const res = await fetch('/api/items', { headers:{ Authorization: `Bearer ${getToken()||''}` } });
  itemsList.innerHTML = '';
  if (!res.ok) return itemsList.innerHTML = '<li>Unauthorized. Please login first.</li>';
  const data = await res.json();
  data.items.forEach(it => {
    const li = document.createElement('li');
    li.textContent = it.name;
    itemsList.appendChild(li);
  });
});

あとは簡単のhtml :

/public/index.html :

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Cypress Starter App</title>
</head>
<body>
  <main>
    <h1>Cypress Starter</h1>

    <section id="auth">
      <h2>Login</h2>
      <form id="login-form">
        <label>Email <input id="email" type="email" value="demo@example.com" required /></label>
        <label>Password <input id="password" type="password" value="password123" required /></label>
        <button data-testid="login-button" type="submit">Log In</button>
      </form>
      <p id="login-info"></p>
    </section>

    <hr/>

    <section id="items">
      <h2>Items</h2>
      <button id="load-items">Load Items</button>
      <ul id="items-list"></ul>
    </section>
  </main>
  <script type="module" src="./app.js"></script>
</body>
</html>

起動

それでインストールして

$ npm install
$ npm run dev

それで無事にテストを始まれる!

Screenshot 2025-11-07 at 14.24.17.png

cypressの起動

$ npm run cypress:open

Screenshot 2025-11-07 at 13.44.31.png

/cypress/e2e/items.cy.js :

describe('Items list', () => {
  beforeEach(() => {
    cy.request('POST', '/api/login', { email: 'demo@example.com', password: 'password123' })
      .then(({ body }) => { window.localStorage.setItem('token', body.token); });
  });

  it('loads items after clicking the button', () => {
    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');
    cy.contains('Sample Item B').should('be.visible');
  });
});

/cypress/e2e/login.cy.js :

describe('Login flow', () => {
  it('logs in with demo user', () => {
    cy.visit('/');
    cy.get('#email').clear().type('demo@example.com');
    cy.get('#password').clear().type('password123');
    cy.get('[data-testid="login-button"]').click();
    cy.contains('Welcome, Demo User!').should('be.visible');
    cy.window().then(win => {
      const token = win.localStorage.getItem('token');
      expect(token).to.be.a('string').and.not.be.empty;
    });
  });
});

今回はテストの説明の為にですが、必要であればログイン情報はCypressの環境変数などに格納してご利用ください。

/cypress/e2e/smoke.cy.js :

describe('Smoke', () => {
  it('shows title and login button', () => {
    cy.visit('/');
    cy.title().should('include', 'Cypress Starter App');
    cy.get('[data-testid="login-button"]').should('be.visible');
  });
});

GUI

スタートすると、ブラウザーが選べます:

Screenshot 2025-11-07 at 13.44.46.png

やっとテストが見えるようになります:

Screenshot 2025-11-07 at 14.28.14.png

specsでクリックすると、テスト開始になります:

Screenshot 2025-11-07 at 14.29.07.png

コマンド

コマンドも出来ます!

$ npm run cypress:run

Screenshot 2025-11-07 at 14.43.41.png

レポート

テストが多いだと、コマンドラインで見にくいなのでレポートも作りましょ!

設定:

/package.json :

{
  "scripts": {
  ...
    "delete:reports": "rm cypress/results/* || true",
    "combine:reports": "jrm cypress/results/combined-report.xml \"cypress/results/*.xml\"",
    "prereport": "npm run delete:reports",
    "report": "cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json",
    "postreport": "npm run combine:reports"
  }
}

mochawesomeってスタイルをおすすめします:

npm install mochawesome mochawesome-merge mochawesome-report-generator --save-dev

/cypress.config.ts :

export default defineConfig({
  ...
  reporter: 'mochawesome',
  reporterOptions: {
    reportDir: 'cypress/results',
    overwrite: false,
    html: false,
    json: true,
  },
})

実行:

$ npx cypress run --reporter mochawesome \
  --reporter-options reportDir="cypress/results",overwrite=false,html=false,json=true

各テストについて、結果のファイルが発生されます:

$ npx mochawesome-merge "cypress/results/*.json" > mochawesome.json

各ファイルをマージして、一個にまとめます:

$ npx marge mochawesome.json

それでローカルでhtmlのファイルが生成されます!

Screenshot 2025-11-07 at 14.56.31.png

テストの説明

今回はloginのテストフローを簡単に説明します。

describe('Login flow', () => {
  it('logs in with demo user', () => {
    cy.visit('/');

トップページへアクセス。

以後のコマンドはこのページを前提に動きます。cy.visit はページ読み込み完了まで適切に待機します(固定の wait は不要)。

    cy.get('#email').clear().type('demo@example.com');
    cy.get('#password').clear().type('password123');

メールとパスワードを入力。

cy.get は要素取得(デフォで自動リトライ)、それで .clear().type で既存文字があっても確実に上書きできます。

    cy.get('[data-testid="login-button"]').click();

ログインボタンをクリック。

data-testid などの 壊れにくいセレクタ を使っていて、CSSクラスやテキストに依存しないのでUI変更に強いです。

    cy.contains('Welcome, Demo User!').should('be.visible');

ログイン成功のUI確認。

画面に成功メッセージが表示されるまで自動で待機してから可視性を検証します。

    cy.window().then(win => {
      const token = win.localStorage.getItem('token');
      expect(token).to.be.a('string').and.not.be.empty;
    });

副作用(認証トークン)を検証。

cy.window() はアプリの window を取得します。localStorage に token が保存されたかを Chai のアサーションでチェックします。

メリット / 注意点

メリット

実利用に近い信頼性の高いフィードバック

バックエンド・フロント間の“つなぎ目”のバグを拾いやすい

注意点

実行時間が長くなりがち → 重要シナリオに絞る/並列化

不安定要因(外部API・時間依存) → cy.intercept / cy.clock で制御

セレクタは data-testid など壊れにくいものを使う

まとめ

今まで色んなテストをやっており、特にユニットテストが多かったですが、ちゃんと画面で確認出来るので本当に安心します。

本番に一番近いし、ミスが発見しやすく、レポートもちゃんとして、確認しやすいです。

ただ、大きなサイトだと重くなる可能性があります。

4
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
4
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?