1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

簡単なアプリ開発1:おみくじアプリを作ってみる

Last updated at Posted at 2025-08-01

WebAPIを作ると、基本部分は大体同じような実装をすることが多いのでその雛型となるような簡単なアプリを作ってみようと思います。

内容

凶から大吉までをランダムに返すだけです。めっちゃシンプルです。

ソースコードはここに置いてあります。
https://github.com/hiro949/omikujiApp/tree/0.1.0

バックエンド

Flaskで書いています。

プロダクトコード

backend/app.py
from flask import Flask, jsonify
from flask_cors import CORS
import random

app = Flask(__name__)
CORS(app)  # allow cross-origin requests

fortunes = [
    "大吉!今日は最高の一日になるわよ!",
    "中吉…まぁまぁね。油断しないでよ!",
    "小吉…ちょっとだけいいことあるかも?",
    "凶…アンタ、今日は大人しくしてなさい!"
]

@app.route('/api/fortune')
def get_fortune():
    result = random.choice(fortunes)
    return jsonify({'fortune': result})

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000, debug=True)

テストコード

内容はランダムなので、どれかが返ってくればPASSとしています。
ルートディレクトリから

python -m pytest

で実行できます。

backend/test/test_app.py
import pytest
from backend.app import app

def test_get_fortune():
    # Flaskのテストクライアントを使う
    client = app.test_client()
    response = client.get('/api/fortune')

    # ステータスコードが200であること
    assert response.status_code == 200

    # JSONレスポンスに 'fortune' キーがあること
    data = response.get_json()
    assert 'fortune' in data

    # fortuneの内容が期待されるリストの中にあること
    expected_fortunes = [
        "大吉!今日は最高の一日になるわよ!",
        "中吉…まぁまぁね。油断しないでよ!",
        "小吉…ちょっとだけいいことあるかも?",
        "凶…アンタ、今日は大人しくしてなさい!"
    ]
    assert data['fortune'] in expected_fortunes

フロントエンド

プロダクトコード

npmwをinstallして、

npm create vite@latest frontend
cd frontend
npm install

で画面表示に必要なファイルは自動で作ってくれます。
あとはその中のApp.jsxApp.cssを以下のように修正すればOKです。

frontend/src/App.jsx
import React, { useState } from 'react';
import './App.css';

function App() {
  const [fortune, setFortune] = useState('');

  const fetchFortune = async () => {
    const response = await fetch('/api/fortune');
    const data = await response.json();
    setFortune(data.fortune);
  };

  return (
    <div className="container">
      <h1>おみくじ React版</h1>
      <button onClick={fetchFortune} className="button">
        占ってみる?…別にアンタのためじゃないけど
      </button>
      <p className="result">{fortune}</p>
    </div>
  );
}

export default App;
frontend/App.css
.container {
  text-align: center;
  padding: 50px;
  background-color: #fff0f5;
  font-family: Arial, sans-serif;
}

.button {
  background-color: #ff69b4;
  color: white;
  border: none;
  padding: 10px 20px;
  font-size: 16px;
  border-radius: 8px;
  cursor: pointer;
}

.result {
  margin-top: 20px;
  font-size: 20px;
  color: #800080;
}

テストコード

frontend/ディレクトリから

npm test

で実行できます。
作成の際は

npm install --save-dev jest @testing-library/react @testing-library/jest-dom babel-jest jest-environment-jsdom @babel/preset-env @babel/preset-react
npm audit fix

である程度必要なコードは自動で揃えてくれます。

自分で書く必要があるのは

  • frontend/setupTest.js
  • vite.configs.js
  • babel.configs.js
  • frontend/__test___/App.test.jsx
  • frontend/package.json (自動生成されたファイルを修正)

です。以下のように書きます。

setupTest.js
import '@testing-library/jest-dom';
jest.config.js
export default {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
  },
  extensionsToTreatAsEsm: ['.jsx'],
  moduleFileExtensions: ['js', 'jsx', 'json', 'node'],
  moduleNameMapper: {
    '\\.css$': '<rootDir>/__mocks__/styleMock.js',
  },
};
package.json(自動生成されたファイル)
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "test": "jest"
  },
  :
  :

追加箇所:"test":"jest"

babel.configs.js
export default {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

jestでApp.cssは読み込めないので代わりのmockを作ります。

frontend/__mocks__/styleMock.js
module.exports = {};
App.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import App from '../src/App.jsx';

test('renders title and button', () => {
  render(<App />);
  expect(screen.getByText(/おみくじ React版/i)).toBeInTheDocument();
  expect(screen.getByText(/占ってみる?/i)).toBeInTheDocument();
});

test('fetchFortune updates fortune text', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ fortune: '大吉!今日は最高の一日になるわよ!' }),
    })
  );

  render(<App />);
  fireEvent.click(screen.getByText(/占ってみる?/i));
  const result = await screen.findByText(/大吉!/i);
  expect(result).toBeInTheDocument();
});

(バックエンドはmockしています)

docker composeでデプロイ

frontendとbackendの通し尿のproxyを設定します。

vite.configs.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    host: true,
    proxy: {
      '/api': {
        target: 'http://backend:5000',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '/api')
      }
    }
  }
})

次にfrontend,backendのDockerfileをそれぞれ作ります。

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 5173
CMD ["npm", "run", "dev"]

を用意してルートの

docker-compose.yaml
services:
  backend:
    build: ./backend
    ports:
      - "5000:5000"
    volumes:
      - ./backend:/app
    environment:
      - FLASK_ENV=development

  frontend:
    build: ./frontend
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
    depends_on:
      - backend

に対して

docker-compose up -d --build

でコンテナを起動します。

で以下のようなページにアクセスできるはずです。

image.png

ボタンをクリックしたときのページ
image.png

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?