Docker+Django+Next+TypeScript+ECSでアプリを作った話(4) ~ Djangoのテスト編~

#　はじめに　　
前回の続きです。

前の記事： [Docker+Django+Next+TypeScript+ECSでアプリを作った話(3) ~ Djangoのスキーマ作成からデータ取得まで~](https://qiita.com/rys0707/items/94263ed21f489fb21aae)

今回はDjangoプロジェクトのスキーマのテストを作成する所を書きました。
factroy-boyというライブラリにて、テストデータを作成しています。

# テスト作成
以下のようにフォルダ、ファイルを作成します。
UserモデルとProfileモデルのスキーマのテストを、QueryとMutationで分けます。

```diff
  myProject/
      app/
          api/
+             tests/
+                 __init__.py
+                 test_profile_query.py
+                 test_profile_mutation.py
+                 test_user_mutation.py
              utils/
+                 __init__.py
+                 factory,py
+                 test_helper.py
             　・・・
```

- factory.py  

UserモデルとProfileモデルのFactory(テストデータの型)を作成します。
UserモデルのemailとpasswordはSequenceメソッドで連番で生成されるようにしており、
関連するprofileはUserモデルデータが作成されると同時に生成されるよう、
RelatedFactoryメソッドを使用しております。

```py:factory.py
from factory.django import DjangoModelFactory
from factory import Sequence, SubFactory, RelatedFactory

from api.models import CustomUser, Profile

class ProfileFactory(DjangoModelFactory):
    class Meta:
        model = Profile

    nickname = Sequence(lambda n: 'user{0}'.format(n))
    user = SubFactory('api.utils.factory.UserFactory', profile=None)

class UserFactory(DjangoModelFactory):
    class Meta:
        model = CustomUser

    profile = RelatedFactory(ProfileFactory, factory_related_name='user')
    email = Sequence(lambda n: 'user{0}@example.com'.format(n))
    password = Sequence(lambda n: 'password{0}'.format(n))
```

- test_helper.py  

テストにてログイン状態を再現するために、　　
トークンを取得して、ヘッダーにセットするメソッドを作成します。


```py:test_helper.py
from graphql_jwt.shortcuts import get_token

def create_token_headers(user):
    token = get_token(user)
    headers = {"HTTP_AUTHORIZATION": f"JWT {token}"}
    return headers
```

- test_profile_query.py  

自身のプロフィールと全プロフィールを取得するQueryのテストを作成します。
テスト開始前にUserモデルのテストデータを6つ作成するよう記載します。
ログインしている状態とログアウトしている状態のテストを記載します。

```py:test_profile_query.py
import json

from graphene_django.utils.testing import GraphQLTestCase

from api.utils.factory import UserFactory
from api.utils.test_helper import create_token_headers

GET_MY_PROFILE_QUERY = '''
    query myProfile {
        myProfile {
            id
            nickname
        }
    }
'''

GET_ALL_PROFILE_QUERY = '''
    query allProfile {
        allProfile {
            edges {    
                node {
                    id
                }
            }
        }
    }
'''

class ProfileQueryTestCase(GraphQLTestCase):
    @classmethod
    def setUpTestData(self):
        self.user = UserFactory(
            profile__nickname="user",
        )
        for i in range(5):
            UserFactory(
                profile__nickname="user{0}".format(i),
            )

    def test_success_get_my_profile(self):
        response = self.query(
            GET_MY_PROFILE_QUERY,
            op_name="myProfile",
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertResponseNoErrors(response)
        self.assertEqual(content["data"]["myProfile"]['nickname'], 'user')

    def test_failed_get_my_profile_because_not_login(self):
        response = self.query(
            GET_MY_PROFILE_QUERY,
            op_name="myProfile",
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]['message'], 'You do not have permission to perform this action')

    def test_success_get_all_profile(self):
        response = self.query(
            GET_ALL_PROFILE_QUERY,
            op_name="allProfile",
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertResponseNoErrors(response)
        self.assertEqual(len(content["data"]["allProfile"]['edges']), 6)

    def test_failed_get_all_profile_because_not_login(self):
        response = self.query(
            GET_ALL_PROFILE_QUERY,
            op_name="allProfile"
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]['message'], 'You do not have permission to perform this action')
```

- test_profile_mutation.py

プロフィール編集のMutationのテストを作成します。
テスト開始前にUserモデルのテストデータを１つ作成するよう記載します。
nicknameが有効な場合、無効な場合のテストを記載します。

```py:test_profile_mutation.py
import json

from graphene_django.utils.testing import GraphQLTestCase

from api.models import Profile
from api.utils.factory import UserFactory
from api.utils.test_helper import create_token_headers

UPDATE_PROFILE_MUTATION = '''
     mutation updateProfile($nickname: String!) {
         updateProfile(input: { nickname: $nickname }) {
             profile {
                 id
             }
         }
     }
'''

class ProfileMutationTestCase(GraphQLTestCase):
    @classmethod
    def setUpTestData(self):
        self.user = UserFactory(profile__nickname="user")

    def test_success_update_my_profile(self):
        response = self.query(
            UPDATE_PROFILE_MUTATION,
            op_name="updateProfile",
            variables={'nickname': 'update user'},
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertResponseNoErrors(response)
        self.assertFalse(Profile.objects.filter(nickname="user").exists())
        self.assertTrue(Profile.objects.filter(nickname="update user").exists())

    def test_failed_update_my_profile_because_nickname_is_blank(self):
        response = self.query(
            UPDATE_PROFILE_MUTATION,
            op_name="updateProfile",
            variables={'nickname': ''},
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Value is required")
        self.assertFalse(Profile.objects.filter(nickname="").exists())
        self.assertTrue(Profile.objects.filter(nickname="user").exists())

    def test_failed_update_my_profile_because_nickname_is_too_long(self):
        response = self.query(
            UPDATE_PROFILE_MUTATION,
            op_name="updateProfile",
            variables={'nickname': 'a'*21},
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Value is too long")
        self.assertFalse(Profile.objects.filter(nickname="a"*21).exists())
        self.assertTrue(Profile.objects.filter(nickname="user").exists())
```

- test_user_mutation.py

ユーザー作成、削除のMutationのテストを作成します。
テスト開始前にUserモデルのテストデータを1つ作成するよう記載します。
ユーザー作成ではnickname、email、passwordの値が有効な場合、無効な場合のテストを記載します。


```py:test_user_mutation.py
import json

from graphene_django.utils.testing import GraphQLTestCase

from api.models import CustomUser, Profile
from api.utils.factory import UserFactory
from api.utils.test_helper import create_token_headers

CREATE_USER_MUTATION = '''
   mutation createUser($nickname: String!, $email: String!, $password: String!) {
       createUser(input: { nickname: $nickname, email: $email, password: $password }) {
           user {
               id
           }
       }
   }
'''

DELETE_USER_MUTATION = '''
   mutation deleteUser($confirm: Boolean!) {
       deleteUser(input: { confirm: $confirm }) {
           user {
               id
           }
       }
   }
'''

class UserMutationTestCase(GraphQLTestCase):
    @classmethod
    def setUpTestData(self):
        self.user = UserFactory(email="user@example.com", profile__nickname="user")


    def test_success_create_user(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={ 'nickname': "success user", 'email': 'success@example.com', 'password': 'password' }
        )
        json.loads(response.content)
        self.assertResponseNoErrors(response)
        self.assertEqual(CustomUser.objects.all().count(), 2)
        self.assertEqual(Profile.objects.all().count(), 2)
        self.assertTrue(CustomUser.objects.filter(email="success@example.com").exists())
        self.assertTrue(Profile.objects.filter(nickname="success user").exists())

    def test_failed_create_user_because_email_is_duplicate(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={ 'nickname': 'failed user', 'email': 'user@example.com', 'password': 'password' }
        )
        content = json.loads(response.content)
        self.assertIn("duplicate", content["errors"][0]["message"])

    def test_failed_create_user_because_email_invalid(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={ 'nickname': 'failed user', 'email': 'failed.12345@failed', 'password': 'password' }
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Invalid Email Address")
        self.assertEqual(CustomUser.objects.all().count(), 1)
        self.assertEqual(Profile.objects.all().count(), 1)
        self.assertFalse(CustomUser.objects.filter(email="failed.12345@failed").exists())
        self.assertFalse(Profile.objects.filter(nickname="failed userr").exists())

    def test_failed_create_user_because_nikname_is_blank(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={ 'nickname': '', 'email': "failed@failed.com", 'password': 'password'}
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Value is required")
        self.assertEqual(CustomUser.objects.all().count(), 1)
        self.assertEqual(Profile.objects.all().count(), 1)
        self.assertFalse(CustomUser.objects.filter(email="failed@failed.com").exists())
        self.assertFalse(Profile.objects.filter(nickname="").exists())

    def test_failed_create_user_because_nikname_is_too_long(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={'nickname': "a"*21, 'email': "failed@failed.com", 'password': 'password'}
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Value is too long")
        self.assertEqual(CustomUser.objects.all().count(), 1)
        self.assertEqual(Profile.objects.all().count(), 1)
        self.assertFalse(CustomUser.objects.filter(email="failed@failed.com").exists())
        self.assertFalse(Profile.objects.filter(nickname="a"*21).exists())

    def test_failed_create_user_because_password_is_too_short(self):
        response = self.query(
            CREATE_USER_MUTATION,
            op_name='createUser',
            variables={'nickname': "failed", 'email': "failed@failed.com", 'password': 'a'*5}
        )
        content = json.loads(response.content)
        self.assertEqual(content["errors"][0]["message"], "Password is too short")
        self.assertEqual(CustomUser.objects.all().count(), 1)
        self.assertEqual(Profile.objects.all().count(), 1)
        self.assertFalse(CustomUser.objects.filter(email="failed@failed.com").exists())
        self.assertFalse(Profile.objects.filter(nickname="failed").exists())

    def test_success_delete_user(self):
        response = self.query(
            DELETE_USER_MUTATION,
            op_name="deleteUser",
            variables={ 'confirm': True },
            headers=create_token_headers(self.user)
        )
        content = json.loads(response.content)
        self.assertResponseNoErrors(response)
        self.assertEqual(CustomUser.objects.all().count(), 0)
        self.assertEqual(Profile.objects.all().count(), 0)  
```
  
ターミナルにて以下のコマンドにてテストを実行します。

```
$ make django-test
```

問題なければ以下のように表示されます。

```
..
----------------------------------------------------------------------
Ran 14 tests in 0.398s

OK
```

# まとめ 

今回はDjangoプロジェクトのUserMutation、ProfileQuery, ProfileMutationのテストを作成し、テストが通るのを確認する所まで書きました。　　
次回はNext.jsのプロジェクトの初期設定を書きたいと思います。