#開発環境
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を編集
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へのアクセスを制限するためのもの)
##モデルの作成
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ダッシュボードの作成
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を作成
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の作成
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を作成
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メソッドを送った際にユーザーのトークンを返してくれるエンドポイント
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
上記はテストの実行コマンド
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)
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()
で指定したモデルに対するデータベースを更新している
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)
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をモックするためのもの
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;
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;
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
を使用している
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;
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;
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を渡している
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ファイルを作成する
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コンポーネントのテスト
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();
});
});
#省略
"test": "react-scripts test --env=jsdom --verbose",
#省略
- テスト一つひとつに対してのpassを確認できるように設定を変更する
#Vehicleコンポーネントのテスト
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を使用する