title | emoji | type | topics | published | publication_name | |||
---|---|---|---|---|---|---|---|---|
テストしやすくリファクタリングしたら、いろいろうれしかった |
🧪 |
tech |
|
true |
x_point_1 |
開発をしていて、「あれ、テストしにくいな・・・」と思うことがあり、テストしやすくなるようにリファクタリングしてみると、いろいろメリットを感じることができたので共有します!
def lambda_handler(event, context):
try:
# eventオブジェクトからクエリパラメータを取得
query_params = event.get("queryStringParameters")
user_id = int(query_params.get("id")) if query_params.get("id") else None
first_name = ・・・
last_name = ・・・
prefecture = ・・・
address = ・・・
phone_number = ・・・
# レコード取得
records = []
for user in execute_sql(
sql="実行したいSQL文",
params={ # SQL文内のプレースホルダに渡すパラメータ
"user_id": id,
"first_name": first_name,
"last_name": last_name,
"prefecture": prefecture,
"address": address,
"phone_number": phone_number,
},
):
records.append(user)
response = {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(records),
}
〜〜〜(省略)〜〜〜
上記は、AWS Lambdaで実装された、ユーザー検索画面等でユーザー一覧を取得するAPIの一部です。
execute_sql関数
はSQL文とパラメータを引数として受け取り、そのクエリを実行し、(何か取得するSQLであれば)結果をジェネレータで返す関数です。
execute_sql関数
に渡す 実行したいSQL文
は以下です。
SELECT
id,
first_name,
last_name,
prefecture,
address,
phone_number
FROM
users
WHERE
((%(id)s IS NULL) OR (id = %(id)s))
AND
((%(first_name)s IS NULL) OR (first_name LIKE CONCAT('%%', %(first_name)s, '%%')))
AND
((%(last_name)s IS NULL) OR (last_name LIKE CONCAT('%%', %(last_name)s, '%%')))
AND
((%(prefecture)s IS NULL) OR (prefecture = %(prefecture)s))
AND
((%(address)s IS NULL) OR (address LIKE CONCAT('%%', %(address)s, '%%')))
AND
((%(phone_number)s IS NULL) OR (phone_number = %(phone_number)s))
ORDER BY
id DESC
SQLクエリ内の %(id)s
のような表記は、Pythonのプレースホルダと呼ばれるもので、SQLクエリの一部を後から動的に置換するために使います。
def test_get_users():
from lambda_function import lambda_handler
# 200
event = {
"queryStringParameters": {
"id": "1",
"firstName": "太郎",
"lastName": "山田",
"prefecture": "東京都",
"address": "渋谷区1-1-1",
"phone_number": "09012345678",
}
}
expected = 200
result = lambda_handler(event, None)
assert expected == result["statusCode"] # assertステートメントで結果を検証
このテストではLambdaが返すステータスコードを検証していますが、SQL内部の条件分岐に対するテストケースが網羅されていません。 実際は、eventオブジェクトの中身のテストケースを増やせば網羅できるのですが、「ちょっとテストしにくいな・・・」と感じました。
この時に感じた「テストの困難さ」は「LambdaがSQL実行部分を含んでいて、責務が分けられていない」つまり、単一責任の原則に反しているからだと後でわかりました。 :::details 単一責任の原則とは モジュール、クラスまたは関数は、単一の機能について責任を持ち、 その機能をカプセル化するべきである。 :::
Lambda全体のテストに加えて、SQLを実行している部分もテストしたいと考え、リファクタリングを行いました。
# 新たに作成
def get_users(
self,
*,
user_id: int = None,
first_name: str = None,
last_name: str = None,
prefecture: str = None,
address: str = None,
phone_number: str = None,
) -> Generator[mysql.connector.cursor.RowType, None, None]:
for user in execute_sql(
sql="実行したいSQL文",
params={
"id": user_id,
"first_name": first_name,
"last_name": last_name,
"prefecture": prefecture,
"address": address,
"phone_number": phone_number,
},
):
yield user
def lambda_handler(event, context):
try:
# eventオブジェクトからクエリパラメータを取得
〜〜〜(省略)〜〜〜
# get_usersでレコード取得
records = []
for user in get_users(
user_id=user_id,
first_name=first_name,
last_name=last_name,
prefecture=prefecture,
address=address,
phone_number=phone_number,
):
records.append(user)
response = {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(records),
}
〜〜〜(省略)〜〜〜
SQL実行部分を get_users関数
に独立させることができました。
get_users関数
に対して以下のテストが追加でき、SQLの内部の条件分岐を網羅することができました。
def test_get_users():
from lambda_function import get_users
# スタッフID
assert 1 == len(list(get_users(user_id=1)))
# 名完全一致
assert 1 == len(list(get_users(first_name="花子")))
# 名部分一致
assert 3 == len(list(get_users(first_name="子")))
# 姓完全一致
assert 2 == len(list(get_users(last_name="山田")))
# 姓部分一致
assert 4 == len(list(get_users(last_name="山")))
# 姓名完全一致
assert 1 == len(list(get_users(first_name="太郎", last_name="山田")))
# 都道府県
assert 3 == len(list(get_users(prefecture="北海道")))
# 住所完全一致
assert 1 == len(list(get_users(address="札幌市16-16-16")))
# 住所部分一致
assert 3 == len(list(get_users(address="札幌市")))
# 電話番号
assert 1 == len(list(get_users(phone_number="09012345678")))
今回のリファクタリングによりうれしかったこと
-
テスト網羅性の向上:
get_users関数
はSQLの実行とその結果を返す役割を持ちます。これにより、Lambda全体のテストだけでなく、SQLの実行部分について個別にテストを行うことが容易になりました。テストの網羅性が向上し、各パラメータが正しくSQLに適用されるか、期待通りの結果を返すかを確認できるようになりました。 -
可読性の向上:
get_users関数
を見るだけで、何をするための関数であるかが理解しやすくなりました。SQLの実行に必要なパラメータが明示的に関数の引数として示されており、それらがどのように使用されるかが一目瞭然です。(単一責任の原則の適用) -
再利用性の向上:
get_users関数
を切り出すことで、同じユーザ取得ロジックを他の箇所でも再利用することが可能になりました。同じコードを複数箇所に書く必要がなくなり、保守性が向上しました。
よい経験ができました🙌🏻