Slide 1

Slide 1 text

職業Pythonista歴半年の私が「君、 修正早いね」 先輩を唸らせた話 2019/06/12 @Stapy mizzsugar0425

Slide 2

Slide 2 text

お前誰よ ・みずきと申します ・2019/01~ 職業Pythonista(Djangoで受託開発してます。案件 募集中!!!) ・好き→Python/Django/Pyramid/React/PostgreSQL/コーヒー/ ロードバイク/一眼レフ

Slide 3

Slide 3 text

ある日のこと・・・

Slide 4

Slide 4 text

上司に仕様変更を伝えられる

Slide 5

Slide 5 text

それを聞いた先輩「これはエグい変更ですね」

Slide 6

Slide 6 text

しかし30分程で変更を実装した私

Slide 7

Slide 7 text

先輩「ぎゃふん」

Slide 8

Slide 8 text

YOU LOSE!! 俺の勝ち! 戦ってないけど

Slide 9

Slide 9 text

速攻ポイント ①テストコード書いた ②疎結合な実装を試みた

Slide 10

Slide 10 text

仕事のコード見せるわけにもいかないので FEの問題を参考にしました 仕様書 https://www.fe-siken.com/kakomon/30_haru/pm05.html 今回書いた詳しいコード(まだ途中) https://github.com/mizzsugar/medical-checkup

Slide 11

Slide 11 text

健診管理システムでこんな仕様があったとして https://www.fe-siken.com/kakomon/30_haru/pm05.html より 1. 当月が誕生月である従業員を抽出する。 2. ①で抽出した従業員を受診対象者とし,健康診断コースを決定する。 3. 受診日を決定し,受診対象者の健康診断レコードを健康管理システ ムに登録する。

Slide 12

Slide 12 text

突如打ち込まれる新たな手順 https://www.fe-siken.com/kakomon/30_haru/pm05.html より 1. 当月が誕生月である従業員を抽出する。 2. 前月の定期健康診断において,再検査が必要と判定された従業員を抽出 する。 3. ①と②で抽出した従業員を受診対象者とし,健康診断コースを決定する。 4. 受診日を決定し,受診対象者の健康診断レコードを健康管理システムに登 録する。

Slide 13

Slide 13 text

突如打ち込まれる新たな手順 https://www.fe-siken.com/kakomon/30_haru/pm05.html より 1. 当月が誕生月である従業員を抽出する。 2. 前月の定期健康診断において,再検査が必要と判定された従業員を抽出 する。 3. ①と②で抽出した従業員を受診対象者とし,健康診断コースを決定する。 4. 受診日を決定し,受診対象者の健康診断レコードを健康管理システムに登 録する。 さあ、 どうする!?!?

Slide 14

Slide 14 text

①テストコード書いた

Slide 15

Slide 15 text

テストコード書かないと  プロダクトコードを修正 →ブラウザ開いて画面ぽちぽちして動作確認 →エラーになったらエラーの該当箇所を確認 →プロダクトコードを修正 →ブラウザ開いて画面ぽちぽちして動作確認 ・・・

Slide 16

Slide 16 text

テストコード書かないと  プロダクトコードを修正 →ブラウザ開いて画面ぽちぽちして動作確認 →エラーになったらエラーの該当箇所を確認 →プロダクトコードを修正 →ブラウザ開いて画面ぽちぽちして動作確認 ・・・ 大変!!!!(>_<)

Slide 17

Slide 17 text

テストコード書くと ①「当月が誕生月である従業員を抽出する。」のテストケースのテストのみ書いたテスト がある(すでに通っている) ー当月誕生日である従業員が抽出されている ー当月誕生日でない従業員は抽出されていない ②「前月の定期健康診断において,再検査が必要と判定された従業員を抽出する」のテ ストケースのテストを追加 ー前月の定期健康診断を受けて再検査必要な従業員が抽出されている ー前月の定期健康診断を受けて再検査不要な従業員が抽出されていない ー前月以前に定期健身を受けた従業員は抽出されない ③テスト回す→落ちる→プロダクトコード修正→通る→リファクタリング

Slide 18

Slide 18 text

①「当月が誕生月である従業員を抽出する。」のテストケースのテストのみ書いたテスト がある(すでに通っている) ー当月誕生日である従業員が抽出されている ー当月誕生日でない従業員は抽出されていない

Slide 19

Slide 19 text

class TestExtractExamees(TestCase): @classmethod def setUpTestData(cls): """テスト用のデータを作成 長いので省略 """ ... def test_iter_birthday_month_employees(self): actual = medical_checkup.core.extract_examinee.iter_birthday_month_employees(today=datetime.date(2019, 5, 1)) actual_list = list(actual) with self.subTest('5月が誕生日である社員がリストに入ってる'): self.assertTrue( employee.types.Employee( id=self.emp_1.id, birthday=datetime.date(1990, 5, 10), gender=employee.types.Gender(1), is_manager=False ) and employee.types.Employee( id=self.emp_2.id, birthday=datetime.date(1980, 5, 11), gender=employee.types.Gender(0), is_manager=True ) in actual_list )

Slide 20

Slide 20 text

) with self.subTest('5月が誕生日でない社員がリストに入っている'): self.assertFalse( employee.types.Employee( id=self.emp_3.id, birthday=datetime.date(1980, 6, 11), gender=employee.types.Gender(0), is_manager=True ) in actual_list ) self.assertFalse( employee.types.Employee( id=self.emp_4.id, birthday=datetime.date(1980, 6, 12), gender=employee.types.Gender(1), is_manager=True ) in actual_list ) @classmethod def tearDownClass(cls): pass

Slide 21

Slide 21 text

②「前月の定期健康診断において,再検査が必要と判定された従業員を抽出する」のテ ストケースのテストを追加 ー前月の定期健康診断を受けて再検査必要な従業員が抽出されている ー前月の定期健康診断を受けて再検査不要な従業員が抽出されていない ー前月以前に定期健身を受けた従業員は抽出されない

Slide 22

Slide 22 text

def test_iter_reexamine_employees(self): # 前月(このテストでは2019年5月)の定期健康診断において,再検査が必要と判定された従業員のみがリストに入っている actual = medical_checkup.core.extract_examinee.iter_reexamine_employees( conducted_year=2019, conducted_month=5 ) actual_list = list(actual) with self.subTest('前月の定期健康診断を受けて再検査必要な従業員が抽出されている'): self.assertTrue( employee.types.Employee( id=self.emp_5.id, birthday=datetime.date(1980,5,11), gender=employee.types.Gender(0), is_manager=True ) in actual_list ) with self.subTest('前月の定期健康診断を受けて再検査不要な従業員が抽出されていない'): self.assertTrue( employee.types.Employee( id=self.emp_6.id, birthday=datetime.date(1980,5,11), gender=employee.types.Gender(0), is_manager=True ) in actual_list

Slide 23

Slide 23 text

gender=employee.types.Gender(0), is_manager=True ) in actual_list ) with self.subTest('前月の定期健康診断を受けて再検査不要な従業員が抽出されていない'): self.assertFalse( employee.types.Employee( id=self.emp_6.id, birthday=datetime.date(1980,5,11), gender=employee.types.Gender(0), is_manager=True ) in actual_list ) with self.subTest('前月以前に定期健身を受けた従業員は抽出されない'): self.assertFalse( employee.types.Employee( id=self.emp_7.id, birthday=datetime.date(1980,5,11), gender=employee.types.Gender(0), is_manager=True ) in actual_list )

Slide 24

Slide 24 text

テスト用コマンド打つだけで 結果確認できて楽!

Slide 25

Slide 25 text

②疎結合な実装を試みた

Slide 26

Slide 26 text

ここを実装するとします https://www.fe-siken.com/kakomon/30_haru/pm05.html より 1. 当月が誕生月である従業員を抽出する。 2. ①で抽出した従業員を受診対象者とし,健康診断コースを決定する。 3. 受診日を決定し,受診対象者の健康診断レコードを健康管理システ ムに登録する。

Slide 27

Slide 27 text

Viewに全てをまとめるとこうなります。 import datetime from django.http import HttpRespons import employee.models.employee def show_examiees_fat_controller(request): examinees = [ { 'id': emp.id, 'birthday': emp.birthday } for emp in employee.models.employee.Employee.objects.all() if emp.birthday.month==datetime.date.today().month ] return HttpResponse( { 'examinees': examinees } status=200, content_type='application/json' )

Slide 28

Slide 28 text

追加されます https://www.fe-siken.com/kakomon/30_haru/pm05.html より 1. 当月が誕生月である従業員を抽出する。 2. 前月の定期健康診断において,再検査が必要と判定された従業員を抽出 する。 3. ①と②で抽出した従業員を受診対象者とし,健康診断コースを決定する。 4. 受診日を決定し,受診対象者の健康診断レコードを健康管理システムに登 録する。

Slide 29

Slide 29 text

def show_examiees_fat_controller(request): # 当月誕生日の人 employees = [ { 'id': emp.id, 'birthday': emp.birthday } for emp in employee.models.employee.Employee.objects.all() if emp.birthday.month==datetime.date.today().month ] # 前月を定義 last_month = datetime.date.today() - relativedelta(month=1) # 前月の検査で再検査が必要と言われた人 re_exam_employees = [ { 'id': mc.employee.id, 'birthday': mc.employee.birthday } for mc in medical_checkup.models.checkup.MedicalCheckUp.objecsts.all() if mc.conducted_year==last_month.year and mc.conducted_month==last_month.month and mc.need_reexamination ] # 当月誕生日の人と前月の検査で再検査が必要と言われた人の配列を結合 employees += re_exam_employees return HttpResponse(

Slide 30

Slide 30 text

{ 'employees': employees } status=200, content_type='application/json' ) 動きはするけれども、ひとつの場所で いろんなことやってて大変だなあ・・・

Slide 31

Slide 31 text

viewと先ほどの計算部分のモジュールを分割すると

Slide 32

Slide 32 text

# (1)1 当月が誕生月である従業員を抽出する。 def iter_birthday_month_employees(today: datetime.date)->Iterator[employee.types.Employee]: """ today: 基本的にはdatetime.date.today()が入ります """ return ( emp for emp in employee.models.employee.Manager.iter_all() if emp.birthday.month==today.month ) # (1)2 前月の定期健康診断において,再検査が必要と判定された従業員を抽出する。 def iter_reexamine_employees(conducted_year: int, conducted_month: int) -> Iterator[employee.types.Employee]: return ( mc.employee for mc in medical_checkup.models.checkup.Manager.iter_all() if mc.conducted_year==conducted_year and mc.conducted_month==conducted_month and mc.need_reexamination ) # (1)と(2)を合わせたメソッド。View関数内にはこちらをいれる def extract_examiee(today: datetime.date) -> Iterator[employee.types.Employee]: last_month = today - relativedelta(month=1) conducted_year = last_month.year conducted_month = last_month.month return itertools.chain( iter_birthday_month_employees((today)), iter_reexamine_employees(conducted_year, conducted_year) ) service.py

Slide 33

Slide 33 text

views.py def show_examiees_fat_controller(request): # 対象者を抽出するメソッド employees = [ asdict(emp) for emp in extract_examiee(datetime.date.today()) ] return HttpResponse( { 'employees': employees } status=200, content_type='application/json' )

Slide 34

Slide 34 text

Viewで表示する項目が変わった訳ではないので Viewの確認はほとんどしなくて良くなった

Slide 35

Slide 35 text

まとめ ・テストコードで楽々動作確認 ・疎結合で修正箇所を小さく抑える

Slide 36

Slide 36 text

ありがとうございました!!!