未熟学生エンジニアのブログ

TetsuFeの個人開発ブログ

TetsuFeはテツエフイー と読みます。FlutterやWeb周り全般、チーム開発について語るブログ

最低限のDjangoアプリ自動テスト

テストは不安を退屈に変える賢者の石だ。 ストレスを感じれば感じるほど、頻繁にテストを走らせるようになる。テストをすぐに走らせれば、ミスをしでかす確率が減っている実感が得られ、結果的にストレスが減っていく。


私たちは、完璧を求めているのではない。すべてのものをコードとテスト両方の視点から捉えることによって欠陥を減らし、 自信を持って前に進めるようになろうと考えているのだ。

ーーー「テスト駆動開発」より引用ーーー

複数人開発でテストコードがなくて困った

最近「スタマチ」の開発中、Aさんが書いたサーバーサイド(Django)のコードをBさんやCさんがフロントエンド(Flutter)から使用しようとしたとき、サーバーサイド側のコードにミスがありデバッグに非常に手間取ったということがありました。

もし、サーバーサイドでテストが書かれていたら、どうだったでしょうか?以下のことができたと思います。

  • Aさんはテストでミスを事前に発見することができた
  • BさんとCさんはテストを読むことでAPIの使い方を正しく知る事ができた

あるエンドポイントに対してのテストコードを書くことは、

  • 品質の担保
  • 手動デバッグの手間の削減
  • フロントエンドエンジニアのためのわかりやすいドキュメント

の3つの意味で重要だと思っています。

特にフロントエンドエンジニアは、

  1. エンドポイントにどのようなHTTPリクエストを送ればどのようなレスポンスが返ってくるかだけが分かればよい
  2. その内部が正しく動いているかどうかまでは気にしたくない

と考えると思います。

テストコードを書くことで、1を明示し、2を担保することができます。

DjangoDRF)で最小限のテストを書いてみる

今回はDjango REST Framework で作った APIサーバーに対するテストを書いてみます。

最小限性

  • ある一つのエンドポイントに対して
  • リクエストを送り、そのレスポンスを検証

具体的には、reportという名前のユーザ通報機能用のアプリケーションのcreateメソッド(REST FrameworkのModelViewSetを継承)に対してのテストを書きます。

views.py

from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from .models import Report
from .serializers import ReportSerializer


class ReportViewSet(viewsets.ModelViewSet):
    queryset = Report.objects.all()
    serializer_class = ReportSerializer
    permission_classes = [IsAuthenticated]

    def create(self, request, *args, **kwargs):
      # 省略
      return Response({'message': '通報が完了しました。'}, status=201)

tests.py

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Report
from account.tests import *


class ReportTests(APITestCase):
    def test_create_report(self):
        reporting_user = create_user('test1', 'xxx@xxx.com', 'xxx')
        reported_user = create_user('test2', 'yyy@yyy.com', 'yyy')

        url = reverse('report-list')
        data = {'reporting_user_id': reporting_user.id,
                'reported_user_id': reported_user.id,
                'content': 'test'
                }

        self.client.login(username='test1', password='xxx')
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data, {'message': '通報が完了しました。'})

このようにテストを書くことで、フロントエンドエンジニア側から見て、先ほど挙げた二つの要素を満たすことができます。

  1. エンドポイントにどのようなHTTPリクエストを送ればどのようなレスポンスが返ってくるかだけが分かればよい(テストを読めば分かる。)
  2. その内部が正しく動いているかどうかまでは気にしたくない(テストが通っていれば、正しく動いている。)

また、 テストが通っていることを確認しておけば、サーバーサイドエンジニアは「自信を持って」コードをフロントエンドエンジニアに渡すことができます。

テストの実行

$ python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.410s

OK
Destroying test database for alias 'default'...

これでテストの追加は完了です。

テストが失敗する例

これで、例えば views.py で201ではなく200を返すように書き換えた場合、以下のようにテストが失敗します。

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_create_report (report.tests.ReportTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/xxx/report/tests.py", line 40, in test_create_report
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)
AssertionError: 200 != 201

----------------------------------------------------------------------
Ran 1 test in 0.411s

FAILED (failures=1)
Destroying test database for alias 'default'...

参考

DRFのテストについてのドキュメント https://www.django-rest-framework.org/api-guide/testing/

reverseに対応するurl名がわからなかったので、こちらを調べました https://www.django-rest-framework.org/api-guide/routers/#simplerouter

例えば、accounts-list で GET(list) と POST(create) の両方にあたります。これにハマりました