備忘録:DRF×Reactのテスト 

#開発環境
Python 3.8

#Anaconda Navigatarにて仮想環境を作成
- Anaconda Navigatarのターミナルにて以下3つのライブラリーの導入

```terinal
$ pip install Django==3.1.3
$ pip install djangorestframework==3.12.2
$ pip install django-cors-headers==3.5.0
```
- PyCharmにてプロジェクトの作成

#Djangoアプリの作成 
```terminal
$ django-admin startproject rest_api .
$ django-admin startapp api
```

##settings.pyを編集
```settings.py
INSTALLED_APPS = [
    #以下の4行を追加(上記は省略)
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
    'api.apps.ApiConfig',
]

MIDDLEWARE = [
    #以下の1行を追加
    'corsheaders.middleware.CorsMiddleware'
    #以下省略
]
#以下の４行を追加
CORS_ORIGIN_WHITELIST = [
    "http://localhost:3000"
]
#以下の８行を追加
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}
```
- corsheadersはMIDDLEWAREに追加する必要がある
- REST_FRAMEWORKというセクションを作成し、その中にPERMISSION(ログインの認証が通ったユーザーのみがアクセスできる)とAUTHENTICATION（認証にはトークンを使う）のデフォルトの設定を書いている(viewsへのアクセスを制限するためのもの)

##モデルの作成
```models.py
from django.db import models
from django.contrib.auth.models import User

class Segment(models.Model):
    segment_name = models.CharField(max_length=100)

    def __str__(self):
        return self.segment_name

class Brand(models.Model):
    brand_name = models.CharField(max_length=100)

    def __str__(self):
        return self.brand_name

class Vehicle(models.Model):
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE
    )
    vehicle_name = models.CharField(max_length=100)
    release_year = models.IntegerField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    segment = models.ForeignKey(
        Segment,
        on_delete=models.CASCADE
    )
    brand = models.ForeignKey(
        Brand,
        on_delete=models.CASCADE
    )
    
    def __str__(self):
        return self.vehicle_name
```
- 小数点を使用する場合は`models.DecimalField`を使用し、`decimal_places=2`は小数点以下の桁数を指定していて、`max_digits=6`は小数点以下の桁数も含めた最大桁数

```terminal
$ python manage.py makemigrations
$ python manage.pymigrate
```
splite3に反映させる

##adminダッシュボードの作成
```admin.py
from django.contrib import admin
from .models import Segment, Brand, Vehicle

admin.site.register(Segment)
admin.site.register(Brand)
admin.site.register(Vehicle)
```

```terminal
$ python manage.py createsuperuser
```
adminダッシュボードを確認する

##Serializerの作成
apiディレクトリ直下にserializers.pyを作成

```serializers.py
from rest_framework import serializers
from .models import Segment, Brand, Vehicle
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'password']
        extra_kwargs = {'password': {'write_only': True, 'required': True, 'min_length': 5}}

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
         return user

class SegmentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Segment
        fields = ['id', 'segment_name']

class BrandSerializer(serializers.ModelSerializer):
    class Meta:
        model = Brand
        fields = ['id', 'brand_name']

class VehicleSerializer(serializers.ModelSerializer):
    segment_name = serializers.ReadOnlyField(source='segment.segment_name', read_only=True)
    brand_name = serializers.ReadOnlyField(source='brand.brand_name', read_only=True)

    class Meta:
        model = Vehicle
        fields = ['id', 'vehicle_name', 'release_year', 'price', 'segment', 'brand', 'segment_name', 'brand_name']
        extra_kwargs = {'user': {'read_only': True}}
```
- メソッドの引数に辞書型は持ってこれないが、`create_user(**validated_data)`のように引数の前に`**`をつけることによってpythonがうまくやってくれる
- `ReadOnlyField`は引数の`source`に与えられたオブジェクトの属性にアクセスできる

##Viewsの作成
```views.py
from django.shortcuts import render
from rest_framework import generics, permissions, viewsets, status
from .serializers import UserSerializer, SegmentSerializer, BrandSerializer, VehicleSerializer
from .models import Segment, Brand, Vehicle
from rest_framework.response import Response

class CreateUserView(generics.CreateAPIView):
    serializer_class = UserSerializer
    permission_classes = (permissions.AllowAny,)

class ProfileUserView(generics.RetrieveUpdateAPIView):
    serializer_class = UserSerializer

    def get_object(self):
        return self.request.user

    def update(self, request, *args, **kwargs):
        response = {'message': 'PUT method is not allowed' }
        return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED)

    def partial_update(self, request, *args, **kwargs):
        response = {'message': 'PUT method is not allowed'}
        return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED)

class SegmentViewSet(viewsets.ModelViewSet):
    queryset = Segment.objects.all()
    serializer_class = SegmentSerializer

class BrandViewSet(viewsets.ModelViewSet):
    queryset = Brand.objects.all()
    serializer_class = BrandSerializer

class VehicleViewSet(viewsets.ModelViewSet):
    queryset = Vehicle.objects.all()
    serializer_class = VehicleSerializer

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)
```
- ログインしているユーザーを検索して返したいので`RetrieveUpdateAPIView`を継承している
- `request.user`がログインしているユーザーを指している
- `ModelViewSet`を継承した場合は`queryset`にオブジェクトの一覧を格納する必要がある
- `perform_create`メソッドをオーバーライドしてVehicleオブジェクトが作成されたときにuser属性にログインしているユーザーを格納する様にしている

##urls.pyの作成
apiディレクトリ直下にurls.pyを作成

```api/urls.py
from django.urls import path, include
from rest_framework.authtoken.views import obtain_auth_token
from . import views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('segments', views.SegmentViewSet)
router.register('brands', views.BrandViewSet)
router.register('vehicles', views.VehicleViewSet)

app_name = 'api'

urlpatterns = [
    path('create/', views.CreateUserView.as_view(), name='create'),
    path('profile/', views.ProfileUserView.as_view(), name='profile'),
    path('auth/', obtain_auth_token, name='auth'),
    path('', include(router.urls)),
]
```
- `obtain_auth_token`は`auth/`にuser_nameとpasswordでpostメソッドを送った際にユーザーのトークンを返してくれるエンドポイント

```rest_api/urls.py
from django.contrib import admin
from django.urls import path, include　#includeを追加

urlpatterns = [
    path('admin/', admin.site.urls),
    #以下の1行を追加
    path('api/', include('api.urls')),
]
```

#テストを作成する
-　テストは`test`のように小文字で表記の`test`と表記する必要がある
-　カスタムのテストを作成するのでデフォルトのテストは削除する

```terminal
$ python manage.py test -v 2
```
上記はテストの実行コマンド

```test_1_user.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status

CREATE_USER_URL = '/api/create/'
PROFILE_URL = '/api/profile/'
TOKEN_URL = '/api/auth/'

class AuthorizedUserApiTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username='dummy', password='dummy_pw')
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_1_1_should_get_user_profile(self):
        res = self.client.get(PROFILE_URL)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, {
            'id': self.user.id,
            'username': self.user.username,
        })

    def test_1_2_should_not_allowed_by_put(self):
        payload = { 'username': 'dummy', 'password': 'dummy_pw' }
        res = self.client.put(PROFILE_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

    def test_1_3_should_not_allowed_by_patch(self):
        payload = { 'username': 'dummy', 'password': 'dummy_pw' }
        res = self.client.patch(PROFILE_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

class UnauthorizedUserApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_1_4_should_create_new_user(self):
        payload = { 'username': 'dummy', 'password': 'dummy_pw' }
        res = self.client.post(CREATE_USER_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        user = get_user_model().objects.get(**res.data)
        self.assertTrue(
            user.check_password(payload['password'])
        )
        self.assertNotIn('password', res.data)

    def test_1_5_should_not_create_user_by_same_credentiale(self):
        payload = { 'username': 'dummy', 'password': 'dummy_pw' }
        get_user_model().objects.create_user(**payload)
        res = self.client.post(CREATE_USER_URL, payload)

        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1_6_should_not_create_user_with_short_password(self):
        payload = { 'username': 'dummy', 'password': 'pw' }
        res = self.client.post(CREATE_USER_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1_7_should_response_token(self):
        payload = { 'username': 'dummy', 'password': 'dummy_pw' }
        get_user_model().objects.create_user(**payload)
        res = self.client.post(TOKEN_URL, payload)

        self.assertIn('token', res.data)
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    def test_1_8_should_not_response_token_with_invalid_credential(self):
        get_user_model().objects.create_user( username='dummy', password='dummy_pw' )
        payload = {'username': 'dummy', 'password': 'wrong'}
        res = self.client.post(TOKEN_URL, payload)

        self.assertNotIn('token', res.data)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1_9_should_not_response_token_with_non_exist_credential(self):
        payload = {'username': 'dummy', 'password': 'dummy_pw'}
        res = self.client.post(TOKEN_URL, payload)
        self.assertNotIn('token', res.data)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1__10_should_not_response_token_with_missing_field(self):
        payload = {'username': 'dummy', 'password': ''}
        res = self.client.post(TOKEN_URL, payload)
        self.assertNotIn('token', res.data)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1__11_should_not_response_token_with_missing_field(self):
        payload = {'username': '', 'password': ''}
        res = self.client.post(TOKEN_URL, payload)
        self.assertNotIn('token', res.data)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_1__12_should_not_get_user_profile_when_unauthorized(self):
        res = self.client.get(PROFILE_URL)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
```

```test_2_segment.py
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from .models import Segment
from .serializers import SegmentSerializer

SEGMENTS_URL = '/api/segments/'

def create_segment(segment_name):
    return Segment.objects.create(segment_name=segment_name)

def detail_url(segment_id):
    return reverse('api:segment-detail', args=[segment_id])

class AuthorizedSegmentApiTest(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username="dummy", password="dummy_pw")
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_2_1_should_get_all_segments(self):
        create_segment(segment_name="SUV")
        create_segment(segment_name="Sedan")
        res = self.client.get(SEGMENTS_URL)
        segments = Segment.objects.all().order_by('id')
        serializer = SegmentSerializer(segments, many=True)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    def test_2_2_should_get_single_segments(self):
        segment = create_segment(segment_name="SUV")
        url = detail_url(segment.id)
        res = self.client.get(url)
        serializer = SegmentSerializer(segment)
        self.assertEqual(res.data, serializer.data)


    def test_2_3_should_create_new_segment_successfully(self):
        payload = {'segment_name': 'K-Car'}
        res = self.client.post(SEGMENTS_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        exists = Segment.objects.filter(
            segment_name=payload['segment_name']
        ).exists()
        self.assertTrue(exists)

    def test_2_4_should_create_new_segment_with_invalid(self):
        payload = {'segment_name': ''}
        res = self.client.post(SEGMENTS_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_2_5_should_partial_update_segment(self):
        segment = create_segment(segment_name="SUV")
        payload = {'segment_name': 'Compact SUV'}
        url = detail_url(segment.id)
        self.client.patch(url, payload)
        segment.refresh_from_db()
        self.assertEqual(segment.segment_name, payload['segment_name'])


    def test_2_6_should_update_segment(self):
        segment = create_segment(segment_name="SUV")
        payload = {'segment_name': 'Compact SUV'}
        url = detail_url(segment.id)
        self.client.put(url, payload)
        segment.refresh_from_db()
        self.assertEqual(segment.segment_name, payload['segment_name'])


    def test_2_7_should_delete_segment(self):
        segment = create_segment(segment_name="SUV")
        self.assertEqual(1, Segment.objects.count())
        url = detail_url(segment.id)
        self.client.delete(url)
        self.assertEqual(0, Segment.objects.count())

class UnAuthorizedSegmentApiTest(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_2_8_should_not_get_segments_when_unauthorized(self):
        res = self.client.get(SEGMENTS_URL)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
```
- `reverse('api:segment-detail', args=[segment_id])`はpathを生成していて、`api:(モデル名)-detail`とすることによってdjangoが自動で`api/モデル名/１`などの詳細ページのpathを生成してくれる
- testの場合はrendererがdict型をJSON型に変換せずに返ってくる。辞書型同士で比較したい場合はデータベースで取得したオブジェクトもSerializerに通して辞書型に変換する必要がある
- `serializer = SegmentSerializer(segments, many=True)`の様にSerializerに渡すオブジェクトが複数ある場合は`many=True`にする必要がある
- `Segment.objects.filter(segment_name=payload['segment_name']).exists()`の`exists()`はTrue Falseの判定に変えている(`exists()がない場合はヒットしたオブジェクト自身を返すがexists()を使用することによってtrue falseを返している`)
- 属性が多いオブジェクトを更新したいときはpartial updateを使用するのではなく、patchを使用する
- `segment.refresh_from_db()`は`(モデル名).refresh_from_db()`で指定したモデルに対するデータベースを更新している

```test_3_brand.py
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from .models import Brand
from .serializers import BrandSerializer

BRANDS_URL = '/api/brands/'

def create_brand(brand_name):
    return Brand.objects.create(brand_name=brand_name)

def detail_url(brand_id):
    return reverse('api:brand-detail', args=[brand_id])

class AuthorizedBrandApiTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username='dummy', password='dummy_pw')
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_3_1_should_get_brands(self):
        create_brand(brand_name="Toyota")
        create_brand(brand_name="Tesla")
        res = self.client.get(BRANDS_URL)
        brands = Brand.objects.all().order_by('id')
        serializer = BrandSerializer(brands, many=True)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)


    def test_3_2_should_get_single_brands(self):
        brand = create_brand(brand_name="Toyota")
        url = detail_url(brand.id)
        res = self.client.get(url)
        serializer = BrandSerializer(brand)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    def test_3_3_should_create_new_brand_successfully(self):
        payload = {'brand_name': 'Audi'}
        res = self.client.post(BRANDS_URL, payload)
        exists = Brand.objects.filter(
            brand_name=payload['brand_name']
        ).exists()
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        self.assertTrue(exists)

    def test_3_4_should_create_new_brand_with_invalid(self):
        payload = {'brand_name': ''}
        res = self.client.post(BRANDS_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_3_5_should_partial_update_brand(self):
        brand = create_brand(brand_name="Toyota")
        payload = {'brand_name': 'Lexus'}
        url = detail_url(brand.id)
        self.client.patch(url, payload)
        brand.refresh_from_db()
        self.assertEqual(brand.brand_name, payload['brand_name'])


    def test_3_6_should_update_brand(self):
        brand = create_brand(brand_name="Toyota")
        payload = {'brand_name': 'Lexus S'}
        url = detail_url(brand.id)
        self.client.put(url, payload)
        brand.refresh_from_db()
        self.assertEqual(brand.brand_name, payload['brand_name'])


    def test_3_7_should_delete_brand(self):
        brand = create_brand(brand_name="Toyota")
        self.assertEqual(1, Brand.objects.count())
        url = detail_url(brand.id)
        self.client.delete(url)
        self.assertEqual(0, Brand.objects.count())

class UnAuthorizedBrandApiTest(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_3_8_should_not_get_brands_when_unauthorized(self):
        res = self.client.get(BRANDS_URL)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
```

```test_4_vehicle.py
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from .models import Segment, Brand, Vehicle
from .serializers import VehicleSerializer

SEGMENTS_URL = '/api/segments/'
BRANDS_URL = '/api/brands/'
VEHICLES_URL = '/api/vehicles/'

def create_segment(segment_name):
    return Segment.objects.create(segment_name=segment_name)

def create_brand(brand_name):
    return Brand.objects.create(brand_name=brand_name)

def create_vehicle(user, **params):
    defaults = {
        'vehicle_name': 'MODEL S',
        'release_year': 2019,
        'price': 500.00,
    }
    defaults.update(params)

    return Vehicle.objects.create(user=user, **defaults)

def detail_seg_url(segment_id):
    return reverse('api:segment-detail', args=[segment_id])

def detail_brand_url(brand_id):
    return reverse('api:brand-detail', args=[brand_id])

def detail_vehicle_url(vehicle_id):
    return reverse('api:vehicle-detail', args=[vehicle_id])

class AuthorizedVehicleApiTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username='dummy', password='dummy_pw')
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_4_1_should_get_vehicles(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        create_vehicle(user=self.user, segment=segment, brand=brand)
        create_vehicle(user=self.user, segment=segment, brand=brand)

        res = self.client.get(VEHICLES_URL)
        vehicles = Vehicle.objects.all().order_by('id')
        serializer = VehicleSerializer(vehicles, many=True)
        self.assertEqual(res.status_code, status.HTTP_200_OK)


    def test_4_2_should_get_single_vehicle(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        url = detail_vehicle_url(vehicle.id)
        res = self.client.get(url)
        serializer = VehicleSerializer(vehicle)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    def test_4_3_should_create_new_vehicle_successfully(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        payload = {
            'vehicle_name': 'MODEL S',
            'release_year': 2019,
            'price': 500.00,
            'segment': segment.id,
            'brand': brand.id,
        }
        res = self.client.post(VEHICLES_URL, payload)
        vehicle = Vehicle.objects.get(id=res.data['id'])
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        self.assertEqual(payload['vehicle_name'], vehicle.vehicle_name)
        self.assertEqual(payload['release_year'], vehicle.release_year)
        self.assertEqual(payload['price'], vehicle.price)
        self.assertEqual(payload['segment'], vehicle.segment.id)
        self.assertEqual(payload['brand'], vehicle.brand.id)

    def test_4_4_should_not_create_vehicle_with_invalid(self):
        payload = {
            'vehicle_name': 'MODEL S',
            'release_year': 2019,
            'price': 500.00,
            'segment': '',
            'brand': '',
        }
        res = self.client.post(VEHICLES_URL, payload)
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)

    def test_4_5_should_partial_update_vehicle(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        payload = {'vehicle_name': 'MODEL X'}
        url = detail_vehicle_url(vehicle.id)
        self.client.patch(url, payload)
        vehicle.refresh_from_db()
        self.assertEqual(vehicle.vehicle_name, payload['vehicle_name'])

    def test_4_6_should_update_vehicle(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        payload = {
            'vehicle_name': 'MODEL X',
            'release_year': 2019,
            'price': 600.00,
            'segment': segment.id,
            'brand': brand.id
        }
        url = detail_vehicle_url(vehicle.id)
        self.assertEqual(vehicle.vehicle_name, 'MODEL S')
        self.client.put(url, payload)
        vehicle.refresh_from_db()
        self.assertEqual(vehicle.vehicle_name, payload['vehicle_name'])


    def test_4_7_should_delete_vehicle(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        self.assertEqual(1, Vehicle.objects.count())
        url = detail_vehicle_url(vehicle.id)
        self.client.delete(url)
        self.assertEqual(0, Vehicle.objects.count())

    def test_4_8_should_cascade_delete_vehicle_by_segment_delete(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        self.assertEqual(1, Vehicle.objects.count())
        url = detail_seg_url(segment.id)
        self.client.delete(url)
        self.assertEqual(0, Vehicle.objects.count())

    def test_4_9_should_cascade_delete_vehicle_by_brand_delete(self):
        segment = create_segment(segment_name='Sedan')
        brand = create_brand(brand_name='Tesla')
        vehicle = create_vehicle(user=self.user, segment=segment, brand=brand)
        self.assertEqual(1, Vehicle.objects.count())
        url = detail_brand_url(brand.id)
        self.client.delete(url)
        self.assertEqual(0, Vehicle.objects.count())

class UnAuthorizedVehicleApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_4__10_should_not_get_vehicles_when_unauthorized(self):
        res = self.client.get(VEHICLES_URL)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
```

#フロントエンド側の事前準備
Reactのtesting-libraryとJestを使用しますが、両方create-react-appコマンドによってインストールされます。
本記事でVSCodeに以下のライブラリをインストールし、使用して開発しています。
- prettier
- ES7
- Jest
- 
#React側のテスト
```terminal
$ npx create-react-app . --template redux
$ npm i axios
$ npm i react-router-dom
$ npm i @material-ui/core @material-ui/icons
$ npm i msw
```
- テストにはcreate-react-appでインストールされるreactのtestingライブラリと　jestを使用する
- mswとはapiをモックするためのもの

```features/authSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const apiUrl = "http://localhost:8000/";

export const fetchAsyncLogin = createAsyncThunk("login/post", async (auth) => {
  const res = await axios.post(`${apiUrl}api/auth/`, auth, {
    headers: {
      "Content-Type": "application/json",
    },
  });
  return res.data;
});

export const fetchAsyncRegister = createAsyncThunk(
  "register/post",
  async (auth) => {
    const res = await axios.post(`${apiUrl}api/create/`, auth, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    return res.data;
  }
);

export const fetchAsyncGetProfile = createAsyncThunk(
  "profile/get",
  async () => {
    const res = await axios.get(`${apiUrl}api/profile/`, {
      headers: {
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

const initialState = {
  profile: {
    id: 0,
    username: "",
  },
};

export const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchAsyncLogin.fulfilled, (state, action) => {
      localStorage.setItem("token", action.payload.token);
    });
    builder.addCase(fetchAsyncGetProfile.fulfilled, (state, action) => {
      return {
        ...state,
        profile: action.payload,
      };
    });
  },
});

export const selectProfile = (state) => state.auth.profile;

export default authSlice.reducer;
```

```features/vehicleSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

const apiUrl = "http://localhost:8000/";

export const fetchAsyncGetSegments = createAsyncThunk(
  "segment/get",
  async () => {
    const res = await axios.get(`${apiUrl}api/segments/`, {
      headers: {
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncCreateSegment = createAsyncThunk(
  "segment/post",
  async (segment) => {
    const res = await axios.post(`${apiUrl}api/segments/`, segment, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncUpdateSegment = createAsyncThunk(
  "segment/put",
  async (segment) => {
    const res = await axios.put(
      `${apiUrl}api/segments/${segment.id}/`,
      segment,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `token ${localStorage.token}`,
        },
      }
    );
    return res.data;
  }
);

export const fetchAsyncDeleteSegment = createAsyncThunk(
  "segment/delete",
  async (id) => {
    await axios.delete(`${apiUrl}api/segments/${id}/`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return id;
  }
);

export const fetchAsyncGetBrands = createAsyncThunk("brand/get", async () => {
  const res = await axios.get(`${apiUrl}api/brands/`, {
    headers: {
      Authorization: `token ${localStorage.token}`,
    },
  });
  return res.data;
});

export const fetchAsyncCreateBrand = createAsyncThunk(
  "brand/post",
  async (brand) => {
    const res = await axios.post(`${apiUrl}api/brands/`, brand, {
      headers: {
        "Content-Type": "application/json",
        Authorizaton: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncUpdateBrand = createAsyncThunk(
  "brand/patch",
  async (brand) => {
    const res = await axios.patch(`${apiUrl}api/${brand.id}/`, brand, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncDeleteBrand = createAsyncThunk(
  "brand/delete",
  async (id) => {
    await axios.delete(`${apiUrl}api/${id}/`, {
      headers: {
        "Content-Type": "appliaction/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return id;
  }
);

export const fetchAsyncGetVehicles = createAsyncThunk(
  "vehicles/get",
  async () => {
    const res = await axios.get(`${apiUrl}api/vehicles/`, {
      headers: {
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncCreateVehicle = createAsyncThunk(
  "vehicle/post",
  async (vehicle) => {
    const res = await axios.post(`${apiUrl}api/vehicles/`, vehicle, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncUpdateVehicle = createAsyncThunk(
  "vehicle/patch",
  async (vehicle) => {
    const res = await axios.patch(`${apiUrl}api/vehicles/`, vehicle, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return res.data;
  }
);

export const fetchAsyncDeleteVehicle = createAsyncThunk(
  "vehicle/delete",
  async (id) => {
    await axios.delete(`${apiUrl}/api/vehicle/${id}/`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${localStorage.token}`,
      },
    });
    return id;
  }
);

export const vehicleSlice = createSlice({
  name: "vehicle",
  initialState: {
    segments: [
      {
        id: 0,
        segment_name: "",
      },
    ],
    brands: [
      {
        id: 0,
        brand_name: "",
      },
    ],
    vehicles: [
      {
        id: 0,
        vehicle_name: "",
        release_year: 2020,
        price: 0.0,
        segment: 0,
        brand: 0,
        segment_name: "",
        brand_name: "",
      },
    ],
    editedSegment: {
      id: 0,
      segment_name: "",
    },
    editedBrand: {
      id: 0,
      brand_name: "",
    },
    editedVehicle: {
      id: 0,
      vehicle_name: "",
      release_year: 2020,
      price: 0.0,
      segment: 0,
      brand: 0,
    },
  },
  reducers: {
    editSegment(state, action) {
      state.editedSegment = action.payload;
    },
    editBrand(state, action) {
      state.editedBrand = action.payload;
    },
    editVehicle(state, action) {
      state.editedVehicle = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder.addCase(fetchAsyncGetSegments.fulfilled, (state, action) => {
      return {
        ...state,
        segments: action.payload,
      };
    });
    builder.addCase(fetchAsyncCreateSegment.fulfilled, (state, action) => {
      return {
        ...state,
        segments: [...state.segments, action.payload],
      };
    });
    builder.addCase(fetchAsyncUpdateSegment.fulfilled, (state, action) => {
      return {
        ...state,
        segments: state.segments.map((seg) =>
          seg.id === action.payload.id ? action.payload : seg
        ),
      };
    });
    builder.addCase(fetchAsyncDeleteSegment.fulfilled, (state, action) => {
      return {
        ...state,
        segments: state.segments.filter((seg) => seg.id !== action.payload),
        vehicles: state.vehicles.filter(
          (veh) => veh.segment !== action.payload
        ),
      };
    });
    builder.addCase(fetchAsyncGetBrands.fulfilled, (state, action) => {
      return {
        ...state,
        brands: action.payload,
      };
    });
    builder.addCase(fetchAsyncCreateBrand.fulfilled, (state, action) => {
      return {
        ...state,
        brands: [...state.brands, action.payload],
      };
    });
    builder.addCase(fetchAsyncUpdateBrand.fulfilled, (state, action) => {
      return {
        ...state,
        brands: state.brands.map((brand) =>
          brand.id === action.payload.id ? action.payload : brand
        ),
      };
    });
    builder.addCase(fetchAsyncDeleteBrand.fulfilled, (state, action) => {
      return {
        ...state,
        brandnds: state.brandnds.filter((brand) => brand.id !== action.payload),
        vehicles: state.vehicles.filter(
          (veh) => veh.brandment !== action.payload
        ),
      };
    });
    builder.addCase(fetchAsyncGetVehicles.fulfilled, (state, action) => {
      return {
        ...state,
        vehicles: action.payload,
      };
    });
    builder.addCase(fetchAsyncCreateVehicle.fulfilled, (state, action) => {
      return {
        ...state,
        vehicles: [...state.vehicles, action.payload],
      };
    });
    builder.addCase(fetchAsyncUpdateVehicle.fulfilled, (state, action) => {
      return {
        ...state,
        vehicles: state.vehicles.map((vehicle) =>
          vehicle.id === action.payload.id ? action.payload : vehicle
        ),
      };
    });
    builder.addCase(fetchAsyncDeleteVehicle.fulfilled, (state, action) => {
      return {
        ...state,
        vehicles: state.vehicles.filter(
          (vehicle) => vehicle.id !== action.payload
        ),
      };
    });
  },
});

export const { editSegment, editBrand, editVehicle } = vehicleSlice.actions;

export const selectSegment = (state) => state.vehicle.segments;
export const selectEditedSegment = (state) => state.vehicle.editedSegment;
export const selectBrand = (state) => state.vehicle.brands;
export const selectEditedBrand = (state) => state.vehicle.editedBrand;
export const selectVehicle = (state) => state.vehicle.vehicles;
export const selectEditedVehicle = (state) => state.vehicle.editedVehicle;

export default vehicleSlice.reducer;

```

```Auth.js
import React, { useState } from "react";
import styles from "./Auth.module.css";
import FlipCameraAndroidIcon from "@material-ui/icons/FlipCameraAndroid";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
import { fetchAsyncLogin, fetchAsyncRegister } from "../features/authSlice";
import { setThePassword } from "whatwg-url";

const Auth = () => {
  const history = useHistory();
  const dispatch = useDispatch();
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isLogin, setIsLogin] = useState("");
  const [successMsg, setSuccessMsg] = useState("");

  const login = async () => {
    const result = await dispatch(
      fetchAsyncLogin({ username: username, password: password })
    );
    if (fetchAsyncLogin.fulfilled.match(result)) {
      setSuccessMsg("Successfully logged in!");
      history.push("/vehicle");
    } else {
      setSuccessMsg("Login error!");
    }
  };

  const authUser = async (e) => {
    e.preventDefault();
    if (isLogin) {
      login();
    } else {
      const result = await dispatch(
        fetchAsyncRegister({ username: username, password: password })
      );
      if (fetchAsyncRegister.fulfilled.match(result)) {
        login();
      } else {
        setSuccessMsg("Registration error!");
      }
    }
  };

  return (
    <div className={styles.auth__root}>
      <span className={styles.auth__status}>{successMsg}</span>
      <form onSubmit={authUser}>
        <div className={styles.auth__input}>
          <label data-testid="lavel-username">Username: </label>
          <input
            data-testid="input-username"
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}></input>
        </div>
        <div className={styles.auth__input}>
          <label data-testid="lavel-password">Password: </label>
          <input
            data-testid="input-password"
            type="password"
            value={password}
            onChange={(e) => setThePassword(e.target.value)}
          />
        </div>
        <button type="submit">{isLogin ? "Login" : "Register"}</button>
        <div>
          <FlipCameraAndroidIcon
            data-testid="toggle-icon"
            className={styles.auth__toggle}
            onClick={() => setIsLogin(!isLogin)}
          />
        </div>
      </form>
    </div>
  );
};

export default Auth;
```
- submitによるページのリフレッシュを防ぐために`e.preventDefault`を使用している

```features/MainPage.js
import React from "react";
import { useHistory } from "react-router-dom";

const MainPage = () => {
  const history = useHistory();

  const Logout = () => {
    localStorage.removeIcon("token");
    history.push("/");
  };

  return (
    <div>
      <button data-testid="btn-logout" onClick={Logout}>
        Logout
      </button>
    </div>
  );
};

export default MainPage;

```

```features/Auth.js
import React, { useState } from "react";
import styles from "./Auth.module.css";
import FlipCameraAndroidIcon from "@material-ui/icons/FlipCameraAndroid";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
import { fetchAsyncLogin, fetchAsyncRegister } from "../features/authSlice";

const Auth = () => {
  const history = useHistory();
  const dispatch = useDispatch();
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isLogin, setIsLogin] = useState(true);
  const [successMsg, setSuccessMsg] = useState("");

  const login = async () => {
    const result = await dispatch(
      fetchAsyncLogin({ username: username, password: password })
    );
    if (fetchAsyncLogin.fulfilled.match(result)) {
      setSuccessMsg("Successfully logged in!");
      history.push("/vehicle");
    } else {
      setSuccessMsg("Login error!");
    }
  };

  const authUser = async (e) => {
    e.preventDefault();
    if (isLogin) {
      login();
    } else {
      const result = await dispatch(
        fetchAsyncRegister({ username: username, password: password })
      );
      if (fetchAsyncRegister.fulfilled.match(result)) {
        login();
      } else {
        setSuccessMsg("Registration error!");
      }
    }
  };

  return (
    <div className={styles.auth__root}>
      <span className={styles.auth__status}>{successMsg}</span>
      <form onSubmit={authUser}>
        <div className={styles.auth__input}>
          <label data-testid="label-username">Username: </label>
          <input
            data-testid="input-username"
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>
        <div className={styles.auth__input}>
          <label data-testid="label-password">Password: </label>
          <input
            data-testid="input-password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">{isLogin ? "Login" : "Register"}</button>
        <div>
          <FlipCameraAndroidIcon
            data-testid="toggle-icon"
            className={styles.auth__toggle}
            onClick={() => setIsLogin(!isLogin)}
          />
        </div>
      </form>
    </div>
  );
};

export default Auth;
```

```features/MainPage.js
import React, { useEffect } from "react";
import styles from "./MainPage.module.css";
import Grid from "@material-ui/core/Grid";
import { useHistory } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchAsyncGetProfile, selectProfile } from "../features/authSlice";
import Segment from "./Segment";
import Brand from "./Brand";
import Vehicle from "./Vehicle";

const MainPage = () => {
  const history = useHistory();
  const dispatch = useDispatch();
  const profile = useSelector(selectProfile);

  useEffect(() => {
    const fetchBootLoader = async () => {
      await dispatch(fetchAsyncGetProfile());
    };
    fetchBootLoader();
  }, [dispatch]);

  const Logout = () => {
    localStorage.removeItem("token");
    history.push("/");
  };

  return (
    <div className={styles.mainPage__root}>
      <Grid container>
        <Grid item xs>
          {profile.username}
        </Grid>
        <Grid item xs>
          <span data-testid="span-title" className={styles.mainPage__title}>
            Vehicle register system
          </span>
        </Grid>
        <Grid item xs>
          <button data-testid="btn-logout" onClick={Logout}>
            Logout
          </button>
        </Grid>
      </Grid>
      <Grid container>
        <Grid item xs={3}>
          <Segment />
        </Grid>
        <Grid item xs={3}>
          <Brand />
        </Grid>
        <Grid item xs={6}>
          <Vehicle />
        </Grid>
      </Grid>
    </div>
  );
};

export default MainPage;
```
- `useEffect`の第二引数に`空の配列`ではなく、`[dispatch]`を指定しているのは、ESLintが空の配列だと普遍であることに気付けないので明示的にdispatchを渡している

```app/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/authSlice";
import vehicleReducer from "../features/vehicleSlice";
jjj 
export const store = configureStore({
  reducer: {
    auth: authReducer,
    vehicle: vehicleReducer,
  },
});
```
#Authコンポーネントのテスト
srcディレクトリ直下にtestsディレクトリとtestファイルを作成する

```tests/Auth.test.js
import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/authSlice";
import Auth from "../components/Auth";

const mockHistoryPush = jest.fn();

jest.mock("react-router-dom", () => ({
  useHistory: () => ({
    push: mockHistoryPush,
  }),
}));

const handlers = [
  rest.post("http://localhost:8000/api/auth/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ token: "abc123" }));
  }),
  rest.post("http://localhost:8000/api/create/", (req, res, ctx) => {
    return res(ctx.status(201));
  }),
];

const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});
afterEach(() => {
  server.resetHandlers();
  cleanup();
});
afterAll(() => {
  server.close();
});

describe("Auth Component Test Cases", () => {
  let store;
  beforeEach(() => {
    store = configureStore({
      reducer: {
        auth: authReducer,
      },
    });
  });
  it("1 :Should render all the elements correctly", async () => {
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    // screen.debug();
    expect(screen.getByTestId("label-username")).toBeTruthy();
    expect(screen.getByTestId("label-password")).toBeTruthy();
    expect(screen.getByTestId("input-username")).toBeTruthy();
    expect(screen.getByTestId("input-password")).toBeTruthy();
    expect(screen.getByRole("button")).toBeTruthy();
    expect(screen.getByTestId("toggle-icon")).toBeTruthy();
  });
  it("2 :Should change button name by icon click", async () => {
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    expect(screen.getByRole("button")).toHaveTextContent("Login");
    userEvent.click(screen.getByTestId("toggle-icon"));
    expect(screen.getByRole("button")).toHaveTextContent("Register");
  });
  it("3 :Should route to MainPage when login is successful", async () => {
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );

    userEvent.click(screen.getByText("Login"));
    expect(
      await screen.findByText("Successfully logged in!")
    ).toBeInTheDocument();
    expect(mockHistoryPush).toBeCalledWith("/vehicle");
    expect(mockHistoryPush).toHaveBeenCalledTimes(1);
  });
  it("4 :Should not route to MainPage when login is falled", async () => {
    server.use(
      rest.post("http://localhost:8000/api/auth/", (req, res, ctx) => {
        return res(ctx.status(400));
      })
    );
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    userEvent.click(screen.getByText("Login"));
    expect(await screen.findByText("Login error!")).toBeInTheDocument();
    expect(mockHistoryPush).toHaveBeenCalledTimes(0);
  });
  it("5 :Should output success msg when registration succeded", async () => {
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    userEvent.click(screen.getByTestId("toggle-icon"));
    expect(screen.getByRole("button")).toHaveTextContent("Register");
    userEvent.click(screen.getByText("Register"));
    expect(
      await screen.findByText("Successfully logged in!")
    ).toBeInTheDocument();
    expect(mockHistoryPush).toBeCalledWith("/vehicle");
    expect(mockHistoryPush).toHaveBeenCalledTimes(1);
  });
  it("6 :Should output error msg when registration failed", async () => {
    server.use(
      rest.post("http://localhost:8000/api/create/", (req, res, ctx) => {
        return res(ctx.status(400));
      })
    );
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    userEvent.click(screen.getByTestId("toggle-icon"));
    expect(screen.getByRole("button")).toHaveTextContent("Register");
    userEvent.click(screen.getByText("Register"));
    expect(await screen.findByText("Registration error!")).toBeInTheDocument();
    expect(mockHistoryPush).toHaveBeenCalledTimes(0);
  });
  it("7 : Should output login error msg when registration success but login failed", async () => {
    server.use(
      rest.post("http://localhost:8000/api/auth/", (req, res, ctx) => {
        return res(ctx.status(400));
      })
    );
    render(
      <Provider store={store}>
        <Auth />
      </Provider>
    );
    userEvent.click(screen.getByTestId("toggle-icon"));
    expect(screen.getByRole("button")).toHaveTextContent("Register");
    userEvent.click(screen.getByText("Register"));
    expect(await screen.findByText("Login error!")).toBeInTheDocument();
    expect(mockHistoryPush).toHaveBeenCalledTimes(0);
  });
});
```

- `userEvent`はボタンが押された時や、入力されるアクションをシミュレーションできるライブラリ
- api(django)をモッキングするためにmswのrestをimportする
- `const mockHistoryPush = jest.fn();`はuseHistoryのモック関数を作っている
- `jest.mock("react-router-dom", () => ({
  useHistory: () => ({
    push: mockHistoryPush,
  }),
}));`はreactのrouter-domのpushメソッドをjestのダミーの関数に上書きしている
- `rest.post("http://localhost:8000/api/auth/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ token: "abc123" }));
  }),`はhttp://localhost:8000/api/auth/にアクセスした際に指定したstatusとjsonを返すモックapiを作成している
- `const server = setupServer(...handlers);`でモックサーバーウォーカを作っている
- `expect(mockHistoryPush).toBeCalledWith("/vehicle");`はダミーの関数(mockHistoryPush)が引数として`/vehicle`を受け取ったかどうかをテストしている
- `server.user(
      rest.post("http://localhost:8000/api/auth/", (req, res, ctx) => {
        return res(ctx.status(400));
      })
    );`はserverを書き換えている

#MainPageコンポーネントのテスト
```tests/MainPage.test.js
import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/authSlice";
import vehicleReducer from "../features/vehicleSlice";
import MainPage from "../components/MainPage";

const mockHistoryPush = jest.fn();
jest.mock("react-router-dom", () => ({
  useHistory: () => ({
    push: mockHistoryPush,
  }),
}));
const handlers = [
  rest.get("http://localhost:8000/api/profile/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ id: 1, username: "test user" }));
  }),
  rest.get("http://localhost:8000/api/segments/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json([]));
  }),
  rest.get("http://localhost:8000/api/brands/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json([]));
  }),
  rest.get("http://localhost:8000/api/vehicles/", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json([]));
  }),
];

const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});
afterEach(() => {
  server.resetHandlers();
  cleanup();
});
afterAll(() => {
  server.close();
});

describe("MainPage Component Test Cases", () => {
  let store;
  beforeEach(() => {
    store = configureStore({
      reducer: {
        auth: authReducer,
        vehicle: vehicleReducer,
      },
    });
  });
  it("1 :Should render all the elements correctly", async () => {
    render(
      <Provider store={store}>
        <MainPage />
      </Provider>
    );
    expect(screen.getByTestId("span-title")).toBeTruthy();
    expect(screen.getByTestId("btn-logout")).toBeTruthy();
  });
  it("2 :Should route to Auth page when logout button pressed", async () => {
    render(
      <Provider store={store}>
        <MainPage />
      </Provider>
    );
    userEvent.click(screen.getByTestId("btn-logout"));
    expect(mockHistoryPush).toBeCalledWith("/");
    expect(mockHistoryPush).toHaveBeenCalledTimes(1);
  });
  it("3 :Should render logged in user name", async () => {
    render(
      <Provider store={store}>
        <MainPage />
      </Provider>
    );
    expect(screen.queryByText("test user")).toBeNull();
    expect(await screen.findByText("test user")).toBeInTheDocument();
  });
});
```
```package.json
#省略
 "test": "react-scripts test --env=jsdom --verbose",
#省略
```
- テスト一つひとつに対してのpassを確認できるように設定を変更する

#Vehicleコンポーネントのテスト
```tests/Vehicle.test.js
import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import vehicleReducer from "../features/vehicleSlice";
import Vehicle from "../components/Vehicle";
import Brand from "../components/Brand";
import Segment from "../components/Segment";

const handlers = [
  rest.get("http://localhost:8000/api/segments/", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, segment_name: "SUV" },
        { id: 2, segment_name: "EV" },
      ])
    );
  }),
  rest.get("http://localhost:8000/api/brands/", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, brand_name: "Audi" },
        { id: 2, brand_name: "Tesla" },
      ])
    );
  }),
  rest.delete("http://localhost:8000/api/segments/1/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
  rest.delete("http://localhost:8000/api/segments/2/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
  rest.delete("http://localhost:8000/api/brands/1/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
  rest.delete("http://localhost:8000/api/brands/2/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
  rest.get("http://localhost:8000/api/vehicles/", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        {
          id: 1,
          vehicle_name: "SQ7",
          release_year: 2019,
          price: 300.12,
          segment: 1,
          brand: 1,
          segment_name: "SUV",
          brand_name: "Audi",
        },
        {
          id: 2,
          vehicle_name: "MODEL S",
          release_year: 2020,
          price: 400.12,
          segment: 2,
          brand: 2,
          segment_name: "EV",
          brand_name: "Tesla",
        },
      ])
    );
  }),
  rest.post("http://localhost:8000/api/vehicles/", (req, res, ctx) => {
    return res(
      ctx.status(201),
      ctx.json({
        id: 3,
        vehicle_name: "MODEL X",
        release_year: 2019,
        price: 350.12,
        segment: 2,
        brand: 2,
        segment_name: "EV",
        brand_name: "Tesla",
      })
    );
  }),
  rest.put("http://localhost:8000/api/vehicles/1/", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        id: 1,
        vehicle_name: "new SQ7",
        release_year: 2019,
        price: 300.12,
        segment: 1,
        brand: 1,
        segment_name: "SUV",
        brand_name: "Audi",
      })
    );
  }),
  rest.put("http://localhost:8000/api/vehicles/2/", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        id: 2,
        vehicle_name: "new MODEL S",
        release_year: 2020,
        price: 400.12,
        segment: 2,
        brand: 2,
        segment_name: "EV",
        brand_name: "Tesla",
      })
    );
  }),
  rest.delete("http://localhost:8000/api/vehicles/1/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
  rest.delete("http://localhost:8000/api/vehicles/2/", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
];
const server = setupServer(...handlers);

beforeAll(() => {
  server.listen();
});
afterEach(() => {
  server.resetHandlers();
  cleanup();
});
afterAll(() => {
  server.close();
});

describe("Vehicle Component Test Cases", () => {
  let store;
  beforeEach(() => {
    store = configureStore({
      reducer: {
        vehicle: vehicleReducer,
      },
    });
  });
  it("1 :Should render all the elements correctly", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.getByTestId("h3-vehicle")).toBeTruthy();
    expect(screen.getByPlaceholderText("new vehicle name")).toBeTruthy();
    expect(screen.getByPlaceholderText("year of release")).toBeTruthy();
    expect(screen.getByPlaceholderText("price")).toBeTruthy();
    expect(screen.getByTestId("select-segment")).toBeTruthy();
    expect(screen.getByTestId("select-brand")).toBeTruthy();
    expect(screen.getByTestId("btn-vehicle-post")).toBeTruthy();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(await screen.findByText("MODEL S")).toBeInTheDocument();
    expect(screen.getAllByRole("listitem")[0]).toBeTruthy();
    expect(screen.getAllByRole("listitem")[1]).toBeTruthy();
    expect(screen.getByTestId("delete-veh-1")).toBeTruthy();
    expect(screen.getByTestId("delete-veh-2")).toBeTruthy();
    expect(screen.getByTestId("edit-veh-1")).toBeTruthy();
    expect(screen.getByTestId("edit-veh-2")).toBeTruthy();
  });
  it("2 :Should render list of vehicles from REST API", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
  });
  it("3 :Should not render list of vehicles from REST API when rejected", async () => {
    server.use(
      rest.get("http://localhost:8000/api/vehicles/", (req, res, ctx) => {
        return res(ctx.status(400));
      })
    );
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("Get error!")).toBeInTheDocument();
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
  });
  it("4 :Should add new vehicle and also to the list", async () => {
    render(
      <Provider store={store}>
        <Brand />
        <Segment />
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("MODEL X")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    userEvent.type(screen.getByPlaceholderText("new vehicle name"), "MODEL X");
    userEvent.selectOptions(screen.getByTestId("select-segment"), "2");
    userEvent.selectOptions(screen.getByTestId("select-brand"), "2");
    userEvent.click(screen.getByTestId("btn-vehicle-post"));
    expect(await screen.findByText("MODEL X")).toBeInTheDocument();
  });
  it("5 :Should delete segment (id 1) and also from list", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-veh-1"));
    expect(await screen.findByText("Deleted in vehicle!")).toBeInTheDocument();
    expect(screen.queryByText("SQ7")).toBeNull();
  });
  it("6 :Should delete segment(id 2) and also from list", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-veh-2"));
    expect(await screen.findByText("Deleted in vehicle!")).toBeInTheDocument();
    expect(screen.queryByText("MODEL S")).toBeNull();
  });
  it("7 :Should update segment (id 1) and also from list", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("edit-veh-1"));
    const inputValue = screen.getByPlaceholderText("new vehicle name");
    userEvent.type(inputValue, "new SQ7");
    userEvent.click(screen.getByTestId("btn-vehicle-post"));
    expect(await screen.findByText("Updated in vehicle!")).toBeInTheDocument();
    expect(screen.getByTestId("name-1").textContent).toBe("new SQ7");
  });
  it("8 :Should update segment (id 2) and also from list", async () => {
    render(
      <Provider store={store}>
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("edit-veh-2"));
    const inputValue = screen.getByPlaceholderText("new vehicle name");
    userEvent.type(inputValue, "new MODEL S");
    userEvent.click(screen.getByTestId("btn-vehicle-post"));
    expect(await screen.findByText("Updated in vehicle!")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("new MODEL S");
  });
  it("9 :Should MODEL S(id 2) cascade deleted when EV(id 2) seg deleted", async () => {
    render(
      <Provider store={store}>
        <Segment />
        <Brand />
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-seg-2"));
    expect(await screen.findByText("Deleted in segment!")).toBeInTheDocument();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(screen.getByTestId("name-1").textContent).toBe("SQ7");
  });
  it("10 :Should MODEL S(id 2) cascade deleted when Tesla(id 2) brand deleted", async () => {
    render(
      <Provider store={store}>
        <Segment />
        <Brand />
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-brand-2"));
    expect(await screen.findByText("Deleted in brand!")).toBeInTheDocument();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(screen.getByTestId("name-1").textContent).toBe("SQ7");
  });
  it("11 :Should SQ7(id 1) cascade deleted when SUV(id 2) seg deleted", async () => {
    render(
      <Provider store={store}>
        <Segment />
        <Brand />
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-seg-1"));
    // expect(await screen.findByText("Deleted in segment!")).toBeInTheDocument();
    // expect(screen.queryByText("SQ7")).toBeNull();
    // expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
  });
  it("12 :Should SQ7(id 1) cascade deleted when Audi(id 1) brand deleted", async () => {
    render(
      <Provider store={store}>
        <Segment />
        <Brand />
        <Vehicle />
      </Provider>
    );
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.queryByText("MODEL S")).toBeNull();
    expect(await screen.findByText("SQ7")).toBeInTheDocument();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
    userEvent.click(screen.getByTestId("delete-brand-1"));
    expect(await screen.findByText("Deleted in brand!")).toBeInTheDocument();
    expect(screen.queryByText("SQ7")).toBeNull();
    expect(screen.getByTestId("name-2").textContent).toBe("MODEL S");
  });
});
```

#tips
- getByは要素が取得できなかった時はエラーをスローし、文字列の引数が完全一致であるのに対し、正規表現は部分一致となる
- 存在しない要素のアサーションを行うためには、getByではなくqueryByを使用する
- まだ存在しないものの最終的に存在する要素については、getByやqueryByではなくfindByを使用する

- [RTLについて](https://qiita.com/ossan-engineer/items/4757d7457fafd44d2d2f)
- [テストコードを書く前にfactory_botを学ぶべき](https://qiita.com/AJIKING/items/ba53d215014989be311c)
- [Rest Framework Djangoのテストコードを作成する](https://qiita.com/shinno21/items/a42eea9b6ce959e106c0)
- [pytestへの第一歩](https://www.m3tech.blog/entry/pytest-summary)