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

テスト

Last updated at Posted at 2025-04-30

いるもん

pip install pytest 

p/ に移動し PYTHONPATH=app pytest app/tests


# [ルートディレクトリで]テスト実行
pytest -s

テスト実行されなかったら

pytest -v

ディレクトリ構造

 tests/
    └── conftest.py
    └── test_auth.py

  app
  ├──init__.py

  p
  ├──pytest.ini

pytest.ini中身

[pytest]
pythonpath = .

inite_py中身

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager

# 拡張機能の初期化(アプリとはまだ紐づけない)
db = SQLAlchemy()
bcrypt = Bcrypt()
login_manager = LoginManager()

def create_app():
    app = Flask(__name__)
    
    # 設定
    app.config['SECRET_KEY'] = 'your-secret-key'  # フォームなどで必要
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    # 拡張機能とアプリを紐づけ
    db.init_app(app)
    bcrypt.init_app(app)
    login_manager.init_app(app)

    # Blueprint やルーティングを後から登録するならここ
    # from .routes import main as main_blueprint
    # app.register_blueprint(main_blueprint)

    return app

テストユーザーを作っておき、作ったtestユーザーは@pytest.fixtureで呼び出す

@pytest.fixture
def test_user():
pytest.ini
    [pytest]
    testpaths = tests
    python_files = conftest.py,  test_auth.py 
    addopts = --maxfail=1 --disable-warnings -q
conftest.py(お決まり文 テンプレ)
import pytest
from app import app, db, User
from app import bcrypt


bcrypt = Bcrypt(app)

import pytest
from app import app, dbUser
from flask_bcrypt import Bcrypt

bcrypt = Bcrypt()

@pytest.fixture(scope='session')
def app():
    app.config['TESTING'] = True
    app.config['WTF_CSRF_ENABLED'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture(scope='function')
def client(app):
    return app.test_client()

@pytest.fixture(scope='function')
def db_session(app):
    # トランザクション制御用
    with app.app_context():
        db.session.begin(subtransactions=True)
        yield db.session
        db.session.rollback()

@pytest.fixture
def create_user(db_session):
    def _create_user(username, email, raw_password):
        hashed_pw = bcrypt.generate_password_hash(raw_password).decode('utf-8')
        user = User(username=username, email=email, password=hashed_pw)
        db_session.add(user)
        db_session.commit()
        return user
    return _create_user
[ログインさせるテスト] "test_auth.py"

 def test_login_success(client, init_db):
    create_user('testuser', 'test@example.com', 'testpassword')
    
    login_data = {
        'username': 'testuser',
        'password': 'testpassword'
    }

    # ログインフォームにデータを送信
    response = client.post('/login', data=login_data)

    # レスポンスがリダイレクトされることを確認(成功時の挙動)
    assert response.status_code == 302
    assert '/mypage' in response.location
    
ログインさせたら、文章表示
response = client.post('/login', data={
        'username': 'testuser',
        'password': 'testpassword'
    }, follow_redirects=True)

    # 成功したか確認(リダイレクトやメッセージ)
    assert 'ログイン成功' in response.data.decode('utf-8') 

テスト考え方

def test_login_success(client, init_db):
    
    # なんのデータを?
   入れたいデータの配列名 
     
      = 入れたいデータ変数名 + 入れたいデータ 

    
    # どこのURLに?
    response = client.post('/行きたいURL', data=入れたいデータの配列名)

    # 処理の結果、どこに行きたい?
    assert response.status_code == 302
    assert '/行きたいページ' in response.location
リダイレクトしたい
response = client.post('/login', data={
       なんちゃらなんちゃら,
    }, follow_redirects=True)
新規登録成功 → ログインページへ
def test_register_success(client, db):
    response = client.post('/register', data={
        'username': 'newuser',
        'email': 'new@example.com',
        'password': 'password',
        'confirm': 'password'
    }, follow_redirects=True)

    assert 'ログインしてください' in response.data.decode('utf-8')
新規登録失敗
def test_register_fail_password_mismatch(client, db):
    response = client.post('/register', data={
        'username': 'user',
        'email': 'bad@example.com',
        'password': 'pass1',
        'confirm': 'pass2'
    }, follow_redirects=True)

    assert 'パスワードが一致しません' in response.data.decode('utf-8')
ログイン成功 → mypageへ
def test_login_success_redirect_to_mypage(client, db, create_user):
    create_user('testuser', 'test@example.com', 'testpassword')

    response = client.post('/login', data={
        'username': 'testuser',
        'password': 'testpassword'
    }, follow_redirects=True)

    assert 'ようこそ' in response.data.decode('utf-8')  # mypageに「ようこそ」などが書かれている前提
ログイン失敗 [パスワエラー]
def test_login_fail_wrong_password(client, db, create_user):
    create_user('testuser', 'test@example.com', 'correctpass')

    response = client.post('/login', data={
        'username': 'testuser',
        'password': 'wrongpass'
    }, follow_redirects=True)

    assert 'ログインに失敗しました' in response.data.decode('utf-8')

大規模アプリ

アプリが大規模になったらpytest.iniファイル作成
 p/
    └──pytest.ini

データ編集

from pathlib import Path

# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"

def test_edit_user(client):
    response = client.post("/user/2/edit", data={
        "name": "Flask",
        "theme": "dark",
        "picture": (resources / "picture.png").open("rb"),
    })
    assert response.status_code == 200

ログアウトリンクに行ったら、リダイレクトさせる

assert len(response.history) == 1
    # Check that the second request was to the index page.
    assert response.request.path == "/index"

文法間違い

SyntaxError: unterminated string literal (detected at line 13)

 内容 assert response.location == ''http://127.0.0.1:5000/mypage'
SyntaxError: bytes can only contain ASCII literal characters

 修正前
assert b'ログインに失敗しました' in response.data

 修正後
assert 'ログインに失敗しました' in response.data.decode('utf-8')

初期設定エラー

tests/conftest.py:10: AttributeError
_ ERROR at setup of test_register_fail_password_mismatch _

    @pytest.fixture()
    def app():
>       app.config['TESTING'] = True
E       AttributeError: 'function' object has no attribute 'config'


# 解決策
 .[conftest.pyにこれ追記]
  from app.app import create_app
  
 2.[app.pyにこれ追記]
 from app import create_app

 3.conftest.py app関数 変更
  flask_app = create_app()
  app.config['TESTING'] = Trueを全部
  flask_appにする

init.pyに初期設定 + app.pyはこう書く

[app.py]
from flask import Blueprint,

bcrypt = Bcrypt()

# Blueprint の作成
main = Blueprint('main', __name__)


[init.py]
# __init__.py の create_app 関数内に追加

from app.app import main  # Blueprint を import
app.register_blueprint(main)

ログイン機能全部

from app import db, bcrypt, login_manager
from flask import Flask, render_template, redirect, url_for, flash, request, send_from_directory, Blueprint
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail, Message
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length, Email, EqualTo, ValidationError, Optional
from flask_login import login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt
from itsdangerous import URLSafeTimedSerializer, SignatureExpired
from pathlib import Path
from werkzeug.utils import secure_filename


main = Blueprint('main', __name__)

# ユーザーモデル
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), nullable=False, unique=True)
    email = db.Column(db.String(120), unique=True, nullable=True)
    password = db.Column(db.String(150), nullable=False)
    email_verified = db.Column(db.Boolean, default=False)

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class RegisterForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。"),
        Length(min=4, max=150, message="ユーザー名は4文字以上150文字以下にしてください。")
    ])
    email = StringField('Email', validators=[Optional(), Email()])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。"),
        Length(min=4, max=150)
    ])
    submit = SubmitField('登録')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('このユーザー名は既に使用されています。')

    def validate_email(self, email):
        if not email.data:
            return  # メールが空なら重複チェックしない
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('このメールアドレスは既に登録されています')


class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。")
    ])
    email = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    remember = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')

class PasswordResetRequestForm(FlaskForm):
    email = StringField('メールアドレス', validators=[
        InputRequired(message="メールアドレスは必須です。"),
        Email()
    ])
    submit = SubmitField('パスワードリセットリンク送信')


# ログイン
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user, remember=form.remember.data)
            return redirect(url_for('mypage'))
        else:
            flash('ユーザー名 / パスワ違ってるよん。', 'error')
    return render_template('login.html', form=form)

# マイページ
@app.route("/mypage")
@login_required
def mypage():
    print(f"ようこそ、{current_user.username}さん!")
    return render_template('mypage.html', username=current_user.username, images=[p.relative_to(BASE_DIR) for p in list(UPLOAD_FOLDER.glob('*.*'))])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def upload():
    if request.method == 'POST':
    
        if 'file' not in request.files:
        # ファイルが選択されていない場合
            flash('ファイルがありません')
        return redirect(url_for('mypage'))

    file = request.files['image']
    # 画像として読み込み

    if file.filename == "":
        # ファイル名がついていない場合
        flash("ファイル not found")
        return redirect(url_for('mypage'))

    if file and allowed_file(img.filename):
        filename = secure_filename(file.filename)

        os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)  # 保存処理
    return redirect(url_for('uploaded_file', filename=filename))
    # GET時はアップロードフォームを表示
    return render_template('mypage.html')

@app.route('/mypage/<filename>')
@login_required
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)



# ログアウト
@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("login"))

# 新規登録
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')

        # メールアドレスが入力されていたら「メール認証あり」
        if form.email.data:
            user = User(username=form.username.data, email=form.email.data, password=hashed_pw)
            db.session.add(user)
            db.session.commit()
            s = URLSafeTimedSerializer(app.config['SECRET_KEY'])
            token = os.dumps(user.email, salt='email-confirm')
            link = url_for('confirm_email', token=token, _external=True)
            print(link)
            msg = Message('メール認証を完了してください', sender='あなたのメールアドレス', recipients=[user.email])
            msg.body = f'こちらのリンクをクリックして認証を完了してください): {link}'
            mail.send(msg)

            flash('認証メールを送りました。メールを確認してください!', 'success')

        # メールアドレスなし → 通常登録
        else:
            new_user = User(username=form.username.data, password=hashed_pw)
            db.session.add(new_user)
            db.session.commit()

            flash('登録が完了しました!ログインしてください。', 'success')
        return redirect(url_for('register_complete'))

    return render_template('register.html', form=form)

@app.route('/confirm_email/<token>')
def confirm_email(token):
    if user.email_verified:
        flash('すでに認証済みです。', 'info')
    else:
        user.email_verified = True
        db.session.commit()
        flash('メール認証が完了しました!', 'success')
    return redirect(url_for('login'))

@app.route('/register_complete')
def register_complete():
    return render_template('register_complete.html')

@app.route('/reset_password', methods=['GET', 'POST'])
def reset_request():
    form = PasswordResetRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            token = s.dumps(user.email, salt='password-reset')
            link = url_for('reset_token', token=token, _external=True)
            msg = Message('パスワードリセット', sender='あなたのメールアドレス', recipients=[user.email])
            msg.body = f'パスワードリセットはこちら: {link}'
            mail.send(msg)
        flash('リセットリンクを送信しました。メールを確認してください。', 'success')
        return redirect(url_for('login'))
    return render_template('reset_request.html', form=form)

@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_token(token):

    user = User.query.filter_by(email=email).first_or_404()

    form = RegisterForm()
    if form.validate_on_submit():
        hashed_pw = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
        user.password = hashed_pw
        db.session.commit()
        flash('パスワードをリセットしました。', 'success')
    return redirect(url_for('login'))

[Loginform]

class LoginForm(FlaskForm):
    username = StringField('ユーザー名', validators=[
        InputRequired(message="ユーザー名は必須です。")
    ])
    password = PasswordField('パスワード', validators=[
        InputRequired(message="パスワードは必須です。")
    ])
    remember = BooleanField('ログイン状態を保持する')
    submit = SubmitField('ログイン')
0
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
0
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?