mizzsugar’s blog

日々感じていることや学んだことを書きます。エンジニアリング以外にも書くかもしれません。

Stapy50回記念で登壇しました

10/9(水)に行われた「みんなのPython勉強会」で登壇した振り返りをします。

startpython.connpass.com

「みんなのPython勉強会」とは、Python初学者からベテランまでいろんな層を対象にしてWebから科学までいろんな題材を扱う「みんな」のための勉強会です。

御縁があってその50回目の会の登壇枠にお誘いいただき、Pythonのunittest.mockモジュールについてお話させていただきました。

speakerdeck.com

ソースコードもまとめました。

https://github.com/mizzsugar/2019_stapy


いいことも改善点もありますが、ちゃんと振り返って精算して次の登壇に活かします!

(※以下、反省文)

登壇前準備

今回は御縁があって登壇のお誘いがあったので承諾してからがスタートです。普段からネタを貯めていた訳ではないのですがぼんやり話したいことはあったしせっかくなのでということで承諾しました。こんな手順で資料を作成しました。

トークの内容とトークの対象となる人を決める

前々からぼんやり「書籍や実践を重ねてテストに関する知見が溜まってきたので1年前の自分に伝えるつもりでいつか話したいな」と思っていました。特にunittest.mockモジュールの使い方に苦戦したので、これについて話そう!と決めました。ただ、話すだけだと焦点が分かりづらい発表になってしまうので「誰にどういう気づきを提供したいか」を詰めました。

結果、「テストコードの基本的な文法(assertEqualとかassertTrueとかレベル)はわかるけれども、unittest.mockモジュールの使いみちがよくわからない人」としました。「こういう時にこうunittest.mockモジュールを使うといい」かのヒントを得ることを目的としました。

トークの目次を作る

特に加筆することはないです。

③ 2で作ったトークの目次が対象の人にとって過不足がないか見直す

最初はunittest.mockモジュールをどういう時に使うかということだけ扱おうとしました。しかし、そもそも単体テストが何を目的として書くのかという話をしてからのほうが何をモックオブジェクトに置き換えるのか考えやすいなと思い、単体テストの例を最初に扱うことにしました。

サンプルコードを何にするかは悩んだので他の人に相談しました。最初に書いたのは軽減税率ではなかったのですが、「この話こそ軽減税率でしょ」という意見をもらい軽減税率にしました。軽減税率は詳しい人からのマサカリが怖かったので一番慎重に何回も書き直しました笑 一番自信がなかった軽減税率の部分は、いろんな人からお褒めいただいたので嬉しかったです。

④ 詳細を詰めながらスライド作り始める。

特に加筆することはないです。

⑤ 15分に収まるか練習

ひたすらストップウォッチと原稿ともに読む練習をしました。読んでいると直したい点が出てくるので読んでスライド手直して読んで・・・を繰り返しました。

また、人に聞いてもらって間違えた伝え方や誤解を生みやすい言い回しを修正しました。協力してくれた人には感謝しかありません。ありがとうございます。

登壇本番

ブラウザのトラブル

Googleスライドで原稿とストップウォッチを見ながら読む練習をしていたのですが、途中でブラウザの再読込をしないと次のスライドが映らないというトラブルに見舞われました。急いでスライドをspeakerdeckに変えたのですが、原稿とストップウォッチが見れないのと焦りの気持ちで15分オーバーしてしまいました・・・

原因は定かではないのですが、firefoxGoogle slideいじってておかしくなったかもしれない説が自分の中で有力です。Chrome試してみた時はなんともなかったので。次からはChromeで発表しようと思います。(※ちゃんと原因特定できていないのでもしかしたらfirefoxは何も悪くない可能性があります)

質疑応答

slidoという質疑応答アプリに匿名の質問が集まっていたので回答したのですが、匿名が故の難しさを感じました。というのも、2-3分の質疑応答で一方的な答えをちょちょっと言っただけでは質問者を納得させることが難しいような質問があったからです笑 

「モックとはなんですか」という極めて抽象的な質問をいただきました。抽象的な概念についての質問は質問者のレベル感を把握した上で、どこがどう疑問なのか対話を通して理解してもらうのが望ましいですが、それを考慮せず一方的に自分が思う答えをただ言ってしまったのが反省点です。

プログラミングを初めて1ヶ月の人が質問したとして、どう答えても理解は難しかったと思うので、「今回わからないのはどうあがいても仕方ないので、勉強を進めてテストコードをより良くしたい段階になった時にこの資料を見返して、その時にわからなかったらDMなりメールなりしてください」と答えるのが良かったかも、と今は思います。その場でわかってもらうことだけが正しいわけではないかもしれません。

初学者もいるコミュニティでの題材は初学者にも理解できる題材にすべきか

SNSやslido(質疑応答用アプリ)を見ると、「余計に混乱した」「結局モックがわからなかった」という声がありました。「発表を聞いてわからなくなった」という声がある場に居合わせたことがなかったので、「表立って言いたくなるほどわからなくなるようなだめな発表だったのかな」とショックを受けました。

15分の制約があったので、申し訳ないけれども、全くのプログラミング初学者はこのトークの対象から外す選択をしました。

やっぱ選択間違えたかなと思ったのですが、後々に

「誰もが楽しめる発表はないので対象を絞ったほうが良い内容になる」というアドバイスや「全く発表がわからなかったけれども発表を聞いてunitttest.mockを勉強したいと思いました」という感想をいただいたので、

内容はもっとブラッシュアップしつつ方向性はこの方針で行こうと決めました。

もしこのトークでもう1回登壇するならば

  • とはいえ「モック」の概念の説明が不足していたのでモックの概念をもう少ししっかり説明する

  • 本題に入る前に予めこの発表の対象となる人を伝えておく

  • 「モック」と「スタブ」の概念も扱って、どんな観点で単体テストを書くかをより深く扱う

  • 対話を通してでないと理解が難しいであろう質問は2-3分の質疑応答の時間で理解されなくても落ち込まない。交流会とかで詳しく話そうねというスタンスで挑む。

  • Chromeで登壇する

で行こうと思います。

登壇に誘って下さった方、聞いてくださった方、フォローしてくださった方、準備に協力してくれた方、ありがとうございました。

ざっくりPyConJP2019のSprint Dayの感想とDjangogirlsTutorial翻訳の感想

PyConJP本体の前にSprintに参加しました。

Sprintは、Pythonの何かしらの開発をする短期集中型イベントです。Sprint Dayに開発したいテーマを提案する人と、それをお手伝いする人で構成されています。

https://pycon.jp/2019/sprint

機械学習・Web・Core Python・ガジェットなど多岐にわたるテーマが揃っていました!

テーマ一覧↓

docs.google.com

きっちりしているものかと思いきや、どのチームも和気あいあいとしていて楽しかったです。

オープンな雰囲気でリラックスして開発に挑めました。

好きな時間に休めるし出入りも自由で、初参加の私はその自由さにいい意味で驚きました。

当日の様子↓


また、会場を貸してくださったHENNGEさんが

昼食はピザを夕食は豪華なケータリングを用意してくださって

豪華さに驚きました!! ありがとうございます!!!

このイベントが無料参加とは思えない・・・


こんな素敵なイベントで、私はDjangogirls Tutorialの翻訳のお手伝いをしました。

Djangogirls Tutorial翻訳をやってみて

翻訳は理解を深めるためにたまにやるのですが、査読付きのちゃんとしたものは大学受験ぶりでした笑

(最後に技術関連の翻訳をちゃんとアウトプットしたのいつだっけ?と思ったら今年の2月でした↓)

【非公式翻訳】ForeignKey in Django公式ドキュメント - mizzsugar’s blog


ちゃんとできるか心配でしたが、Djangogirls Tutorialの英語が比較的易しかったのと

翻訳ツールがとても優れているおかげでなんとかなりました。

「これをどうやって訳すか悩んでて」と気楽に相談できる雰囲気もありがたかったです。

なんと、このSprint DayでDjangogirls Tutorialの翻訳率は100%に到達しました!!

これから翻訳のレビューがあるので完全に、というわけではありませんがこれは嬉しかったです。貢献できてよかったです。


Djangogirls Tutorialの翻訳は、普段ドキュメントを読んだり書いたりするのとは違う観点での伝え方が必要だと感じました。

普段の開発中でのコミュニケーションでは「雰囲気でざっくりこんな感じ」というのは好ましく思われておらず

いかに正確に伝えるかに焦点を当てています。また、基本的に会話なのである概念についてわからなかったら理解できるように粘り強く対話します。

なので、「厳密にはもうちょっと言いたいけど初心者が今の段階ではこの概念だけわかればOK」という範囲を絞るのと(まあ、これは普段のDjangogirlsのイベントでもやってるけど)

Tutorialというある意味一方的?な媒体でどう伝えるのかを考えるのは新鮮で面白い体験でした。


Sprint Dayでいろんな人とお話できたし、微力ながらもPythonの世界に貢献できたし、とても満足です!

来年はどんなテーマのものに参加するか、もしくは自分がテーマを掲げるかわかりませんが

また参加したいと思います。

ざっくりPyConJP2019の2日目感想

ざっくり2日目の感想を書き残します。


Pythonで始めてみよう関数型プログラミング

Twitter

twitter.com

スライド

Pythonで始めてみよう関数型プログラミング - Speaker Deck

YouTube

www.youtube.com

安全でわかりやすいコードを書くためのプログラミングの考え方としてとても面白かったです。

mypyのFinalを使って変数がImmutableであると宣言したり、Dictを型クラスのような役割に定義したクラスのインスタンスにしたりは普段やっているのですが、

それらを便利に実装できるようなライブラリがあるというのは勉強になりました。

また、F#から刺激をうけている様子を拝見して、久々に他言語の人とPython以外の言語でプログラミングしたくなりました。


Python Website is Slow? Think Again!

Twitter

twitter.com

スライド

https://freedomofkeima.com/pyconjp2019.pdf

YouTube

www.youtube.com

PythonのWebアプリケーションが遅いと言われているのはI/OバウンドがボトルネックになっていてPython自体が悪いんじゃないのではないか?というお話でした。

DBの呼び出しを何回もしないようにPythonのデコレーターを使って余分にDB呼び出ししていたのを抑えたり

AsyncIOを使うという解決策を紹介してくれました。

また、CPUに高負荷かける処理するようなあらCとかGo使ったほうがいいかもとのことでした。

入門自作検索エンジン

Twitter

twitter.com

スライド

The first step self made full text search - Speaker Deck

YouTube

www.youtube.com

検索のためのアルゴリズムの説明から説明してくださり、勉強になりました。

Pythonで簡潔なコードを書いてくださったのもありがたかったです。1回の講演では理解しきれなかったので改めて資料を読み直したいと思います。

PyConJPのAPIを使うというのも面白いかったです。


当日飛び込みLTに当選した話

去年参加したときはまさか次の年に自分があの段に立っているとは思いませんでした。

PyConJP2018で_勇気をもらって_Pythonエンジニアになった話.pdf - Speaker Deck

こぼれ話ですが、去年のPyCon後に転職活動して内定を獲得後、

たまたま参加した勉強会にこのLTのきっかけとなった方がいまして。

「あのLTのおかげで勇気もらってPythonエンジニアになれることになりました!」と報告したら「来年は同じ内容でぜひあの場所で登壇してください」と言ってもらいました。(ご本人覚えていないかもしれませんが・・・)

無事果たせてよかったです。

ざっくりPyConJP2019の1日目感想

今年もPyConJPに参加しました!

1日目に拝聴したセッションの感想をざっくり書き残そうと思います。

なお、今日の発表の様子はYouTubeにすべて上がっています。

t.co

個人的に、YouTube版はちゃんと聞き取れるしスライドの内容もちゃんと見えるので良かったです!


Djangoで実践ドメイン駆動設計による実装

Twitter

twitter.com

YouTube

youtu.be

めちゃめちゃ大きいテーマに挑戦されていて、尊敬します。

DDDの考え方もしっかり説明してくださってありがたかったです。Pythonで具体的なコードまで載せており、こういう実装方法もあるんだと学びになりました。

「あれ? 結局これ取り入れてで何を解決したいんだっけ?」とならないように

戦略的DDDをしっかり行うという前提でDjangoでの戦術的DDDに挑戦したいなあと身が引き締まりました。


Pythonを使った APIサーバー開発を始める際に 整備したCIとテスト機構

Twitter

twitter.com

スライド

speakerdeck.com

「CIに教わる」という名言が印象的でした。

モックにどれだけたよっているか、どんなふうにたよっているかは

きれいにDIできていて修正時の影響範囲を抑えたコードになっているかを確かめる観点ということで

改めて自分のテストコードを見て確認したいと思いました。

Pythonのモック、なんでもモックできちゃうので

「ただモックした!」から前進するヒントになりそうな発表でした。

YouTube

youtu.be


ListはIteratorですか?

Twitter

twitter.com

スライド

docs.google.com

YouTube

youtu.be


とても楽しそうにお話していたのが印象的でした。

スライドにあるListやSetやIterableなどの各クラスの継承関係の図はありそうで今までないやつだったので感動しました・・・! あれは重宝しそう・・・!

あの図を見ながらなら、広く引数の型を受け入れる使いやすい関数を書けそうな気がします。

初学者にもわかりやすくも、継承関係や豆知識で深いところまで扱っていて面白かったです。


[おまけ]セッション以外で感じたこと

去年の初参加のときはPython始めたばかりで全然知り合いがいなかったのですが、

今年は同じ会社の人がいたり勉強会で一緒だったorTwitterでやりとりしていた方など

いろんな人とお話できて楽しかったです!

知り合いがまったくいなかった去年もトークを聞くだけでも十分楽しかったですが、

いろんな人がいっしょくたに集まるのは感慨深いですね。

アフターパーティで私の友達が私の会社の人と友達になる(友達の友達は友達的なやつ?)というのもこういうイベントならではなのかな〜など月並にも思いました。

【Python】Poetryでパッケージ管理 on Docker Container

DockerでPython開発する時、パッケージ管理はpip install -r requirement.txtで行なっていましたが、

requirement.txtのみではパッケージ同士の依存関係まで管理できないことが問題でした。

そこで、Poetryを使ってみることにしました。

github.com

使用技術

- Python 3.7.3
- Docker 19.03.1
- poetry 0.12.17

ディレクトリ構成です。 今回は、Djangoプロジェクトをappというコンテナに、DBをdbというコンテナで管理することにしました。

poetry.lockとpyproject.tomlをどこに入れるか迷いましたが、Pythonのコードを管理しているapp配下に。


環境関連は全てdocker以下に!ということでdocker/appに入れてみたのですが、新しいパッケージを導入したいと思うたびに

appからdockerに移動しないといけないのが面倒だと気づいてしまいました汗

Dockerコンテナに入って開発する際にworkdirがapp以下になるだろうということで、 app以下にこの2つを置いてpoetryの操作をしやすいようにしました。

ディレクトリ構成

├── app
│   ├── poetry.lock
│   ├── project
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── manage.py
│   │   │   ├── settings
│   │   │   │   ├── __init__.py
│   │   │   │   └── base.py
│   │   │   ├── urls.py
│   │   │   └── wsgi.py
│   │   ├── manage.py
│   │   ├── sample_app
│   │   │   ├── __init__.py
│   │   │   ├── admin.py
│   │   │   ├── apps.py
│   │   │   ├── migrations
│   │   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   └── tests
│   │       └── __init__.py
│   └── pyproject.toml
├── docker
│   ├── app
│   │   ├── Dockerfile
│   │   └── start-server.sh
│   ├── etc
│   │   └── gunicorn.conf
│   ├── db
│   │   └── Dockerfile
│   └── var
│       └── log
│           └── gunicorn
└── docker-compose.yaml


PythonコンテナのDockerfileです。

poetryのインストールのみにとどめ、 poetry installアプリケーションサーバーの立ち上げは 独自にシェルスクリプトを作成して、そちらに実行させることにしました。

Dockerfile

FROM python:latest
ENV PYTHONUNBUFFERED 1
LABEL maintainer "sample_name <hogehoge@example.com>"
RUN pip install --upgrade pip
RUN pip install poetry


シェルスクリプトの中身をみましょう。

Dockerコンテナを立ち上げる際に下記のコマンドが実行されるようにしました。 serverも立ち上がるようにし、コンテナが立ち上がった後にlocalhost:8080にアクセスするとDjangoで作成した画面が見れ流ようにしました。

本番環境と開発環境で別コマンドが実行されるようにします。

#!/bin/bash
cd /home/app

# /home/app下にpoetry.lockとpyproject.tomlがあるのでインストールします。
poetry install
cd project


# poetry run [実行したいコマンド]でpipenvで作成される仮想環境でコマンドを実行します。
if [ "${DJANGO_ENV}" = 'production' ]; then
    # 本番環境
    poetry run python manage.py migrate --settings config.settings
    poetry run python manage.py collectstatic --noinput
    poetry run gunicorn config.wsgi:application -c /home/docker/etc/gunicorn.conf -b :8080
else
 # 開発環境
    poetry run python manage.py migrate
    poetry run python manage.py runserver 0.0.0.0:8000
fi


docker-compose.yaml

version: "3"
services:
  app:
    build:
      context: ./docker/app
      dockerfile: Dockerfile
    env_file: .env
    environment:
      - POSTGRESQL_HOST=db
      - POSTGRESQL_PORT=5432
    volumes:
      - ./app:/home/app
      - ./docker/app:/home/docker
      - ./docker/etc/gunicorn.conf:/etc/gunicorn.conf
      - ./docker/var/log/gunicorn:/var/log/gunicorn
    working_dir: /home/app
    entrypoint: "/bin/sh"
    command: "/home/docker/start-server.sh"
    ports:
      - "8080:8080"
    tty: true
    depends_on:
      - db
  db:
    build:
      context: ./docker/postgres
      dockerfile: Dockerfile
    env_file: .env
    ports:
      - 5432:5432
    volumes:
      - ./docker/db/pgsql-data:/var/lib/postgresql/data


つづいて、操作方法について。

なんかしらPythonの操作したい

まずコンテナに入ります。

docker-compose exec app bash

poetryの仮想環境下で操作する必要があるので、 まずpoetry shellでpoetryが作成する仮想環境に入ってからコマンドを実行

poetry shell

[実行したいコマンド]

またはこちらで。 poetry runを頭につけると、poetryの仮想環境に入った状態でコマンドが実行される動きになります。

poetry run [実行したいコマンド]

新しくパッケージを導入したい

docker-compose exec app bash
# poetryにてインストール
poetry add [パッケージ名]

パッケージのバージョンをあげたい

docker-compose exec app bash
# 全部のパッケージをアップデートしたい時
poetry update

# 特定のパッケージのみアップデートしたい時
poetry update [パッケージ名]


リポジトリはこちら。

github.com


おまけ。Pipenv編リポジトリ

(やることはあまり変わらなかった)

GitHub - mizzsugar/pipenv_on_docker

【Django】独自ヘッダーをつけてリクエストを送る

地味に苦労したやつです。

利用技術

- python 3.7.3

- Django 2.2.1

ヘッダーに関して、Djangoの公式ドキュメントはなんて言っているでしょうか。

HttpRequest.META
利用できるすべての HTTP ヘッダーが格納されたディクショナリです。
-- Django公式ドキュメントより

https://docs.djangoproject.com/ja/2.2/ref/request-response/#django.http.HttpRequest.META


なるほど、request.METAから取り出せば良いのか。

curl -X POST -H 'Content-Type:application/json' -H "CUSTOM_HEADER:customheader" http://127.0.0.1/api/


def sample_view(request):
    print(request.META.get('CUSTOM_HEADER')
    ...

printの中身がNoneになる・・・ なぜや〜


もうちょっと公式ドキュメントを読むと・・・

With the exception of CONTENT_LENGTH and CONTENT_TYPE, as given above,
any HTTP headers in the request are converted to META keys by converting all characters to uppercase,
replacing any hyphens with underscores and adding an HTTP_ prefix to the name. 
So, for example, a header called X-Bender would be mapped to the META key HTTP_X_BENDER.

-- Django公式ドキュメントより

https://docs.djangoproject.com/ja/2.2/ref/request-response/#django.http.HttpRequest.META


CONTENT_LENGTHとCONTENT_TYPEは例外として、上記に示されているように、
リクエストのHTTPヘッダーはMETAキーに変換されます。全て大文字になり、ハイフンがアンダースコアになり、ヘッダー名の頭に「HTTP_」がつきます。
例えば、「X-Bender」というヘッダーは「HTTP_X_BENDER」というMETAキーに格納されます。
-- mizzsugarによる意訳

request.METAに変換する際にヘッダー名が変換されるということっぽい。

これならいけるか?

request.META.get('HTTP_CUSTOM_HEADER')

ダメだった・・・

「ハイフンがアンダースコアになり」。ということは、アンダースコアはダメ?

これでどうだ↓

curl -X POST -H 'Content-Type:application/json' -H "CUSTOM-HEADER:customheader" http://127.0.0.1/api/


request.META.get('HTTP_CUSTOM_HEADER')


取得できました!

どうやら、こういう仕様のようです。

* 名前に区切りを使いたいならハイフンを使わないといけない
* request.METAから取り出す場合は全て大文字にして、名前の頭に「HTTP_」をつけ、ハイフンの箇所をアンダースコアにする

ハイフンじゃないと反応しないというところが個人的にハマりポイントでした(^^;

【Django】複数のファイルをまとめてテストするとTransactionErrorやIntegurityErrorになってしまう事件について

仕事でテスト周りについて色々あったので備忘録として

例えば、こういうViewとModelがあったとして・・・

(まあ、こんなこと普通しないとは思いつつ簡単な例を出したく)

models.py

from typing import Dict

from django.db import models
from django.db import IntegrityError


class Customer(models.Model):
    GENDER_CHOICES = (
        (0, 'Male'),
        (1, 'Female'),
        (2, 'Other')
    )
    name = models.TextField()
    email = models.EmailField(unique=True)
    birthday = models.DateField()
    gender = models.PositiveSmallIntegerField(choices=GENDER_CHOICES)


    @classmethod
    def create(cls, data: Dict) -> None:
        """フォームのデータから顧客を新規登録するメソッド
        
        dataの中身
        {
            'name': 'name',
            'email': 'email@example.com',
            'birthday': datetime.date(1990, 1, 1),
            'gender': 0
        }
        """
        try:
            cls.objects.create(
                name=data.get('name'),
                email=data.get('email'),
                birthday=data.get('birthday'),
                gender=data.get('gender')
            )
        except IntegrityError:
            raise

views.py

from django.http import HttpResponse
from django.views import View
from django.shortcuts import render
from django import forms
from django.db import IntegrityError

from .models import Customer

# ブログを簡素にしたいのでViewに書いたけどforms.pyに書いてもよし
class CustomerForm(forms.Form):
    GENDER_CHOICES = (
        (0, 'Male'),
        (1, 'Female'),
        (2, 'Other')
    )
    name = forms.CharField(max_length=255)
    email = forms.EmailField()
    gender = forms.ChoiceField(choices=GENDER_CHOICES)


class CreateCustomerView(View):
    def get(self, request):
        """顧客名一覧を表示
        """
        costomers = [
            customer.name
            for customer in Customer.objects.iterator()
        ]
        return render(
            request,
            'form.html',
            {
                'costomers': costomers,
                'form': CustomerForm()
            }
        )

    def post(self, request):
        """顧客を新規登録

        request.postの形式
        {
            "name": "Taro Tanaka",
            "birthday": "1980-01-01",
            "gender": 0
        }
        """
        form = CustomerForm(request.POST)
        if form.is_valid():
            try:
                Customer.create(data=form.cleaned_data)
            except IntegrityError:
                form.add('email', 'すでに登録しているメールアドレスは登録できません')
        return render(
            requset,
            'form.html',
            {
                'costomers': costomers,
                'form': form
            }
        )

こうテストを書くと

test_models.py

import datetime

from django.db import IntegrityError
from django.test import TestCase

from .models import Customer


class TestCustomerModel(TestCase):
    @classmethod
    def setUpTestData(cls):
        Customer.objects.create(
            name='Taro Yamada',
            email='sample@example.com',
            birthday=datetime.date(1980, 1, 1),
            gender=0
        )

    def test_email_duplicate(self):
        data = {
            name: 'Jiro Yamada',
            email: 'sample@example.com',  # すでに登録しているメールアドレス
            birthday: datetime.date(1980, 2, 2),
            gender: 0
        }
        with self.assertRaises(IntegrityError):
            Customer.create(data=data)

    def test_create_valid_customer(self):
        data = {
            name: 'Hanako Takada',
            email: 'sample_2@example.com',
            birthday: datetime.date(1980, 3, 3),
            gender: 1
        }
        Customer.create(data=data)

test_views.py

from django.test import TestCase, Client

from .models import Customer
from .views import CreateCustomerView


class TestCustomerView(TestCase):
    @classmethod
    def setUpTestData(cls):
        Customer.objects.create(
            name='Taro Yamada',
            email='sample@example.com',
            birthday=datetime.date(1980, 1, 1),
            gender=0
        )

    def test_email_duplicate(self):
        data = {
            name: 'Jiro Yamada',
            email: 'sample@example.com',  # すでに登録しているメールアドレス
            birthday: datetime.date(1980, 2, 2),
            gender: 0
        }
        response = self.client.post(
            '',
            {
                "name": "Taro Yamada",
                "email": "sample@example.com",
                "gender": "0",
                "birthday": "1980-01-01",
                "position": "100",
            }
        )
        self.assertContains(
            'すでに登録しているメールアドレスは登録できません'
            response
        )

TransactionErrorが〜

IntegurityErrorが〜


・・・なぜ起こるか。

TestCaseを継承するクラスにsetUpClassとtearDownClassがないことに注目。

Djangoでは、setUpClassがそのテストクラスでテストを実行するための準備をするために、テストクラスが初期化される際に一度だけ呼ばれます。

tearDownClassがテストクラス内のテストがすべて終わりテストクラスが解放される際に一度だけ呼ばれる仕組みになっています。tearDownClassはsetUpClassが行った準備をすべてクリーンにするイメージかと思います。

DjangoのTestCaseを継承している場合、tearDownClassによって、トランザクションロールバックされます。テスト用データベース内のデータがすべて一掃されます。

上記の例だと、

各テストクラスにsetUpClassとtearDownClassがないため、データベースのトランザクションが終わってない状態であるがゆえにTransactionError

最初に実行したテストクラスでのデータが残っているためユニークであるはずのデータが重複しようとしてしまい意図しないIntegurityError・・・

となってしまいました。

じゃあこうすればいいの!?  

class TestCustomerModel(TestCase):
    @classmethod
    def setUpClass(cls):
        pass

# 中略
...
    
    @classmethod
    def tearDownClass(cls):
        pass

いいえ、違います。

ただ最初にsetUpClassとtearDownClassを追加しただけだとトランザクション開始と後処理の働きをしないので super()をつけることによってDjangoのTestCaseのsetUpClassとtearDownClassの働きを継承してあげましょう。

class TestCustomerModel(TestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()

# 中略
...
    
    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()

他にもテストクラス内のテストメソッドは順番関係なく実行されてもOKなように・・・などありますが

他のファイルが影響するということは避けられるようになりました!

(Viewのテストは実際にトランザクション走らせずモック使っても・・・というのはまた別の記事で!)

<参考>

qiita.com