LoginSignup
1
0

More than 1 year has passed since last update.

備忘録:DRF×Reactのテスト

Last updated at Posted at 2021-08-06

開発環境

Python 3.8

Anaconda Navigatarにて仮想環境を作成

  • Anaconda Navigatarのターミナルにて以下3つのライブラリーの導入
$ pip install Django==3.1.3
$ pip install djangorestframework==3.12.2
$ pip install django-cors-headers==3.5.0
  • PyCharmにてプロジェクトの作成

Djangoアプリの作成

$ 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'
    #以下省略
]
#以下の4行を追加
CORS_ORIGIN_WHITELIST = [
    "http://localhost:3000"
]
#以下の8行を追加
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は小数点以下の桁数も含めた最大桁数
$ 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)
$ 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_tokenauth/に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と表記する必要がある
- カスタムのテストを作成するのでデフォルトのテストは削除する

$ 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/モデル名/1などの詳細ページの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側のテスト

$ 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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0