Django REST Framework หรือที่เรียกสั้น ๆ ว่า DRF เป็นชุดเครื่องมือมาตรฐานสำหรับการสร้าง API บน Django มันมอบ serializers, viewsets, routers และชั้นสำหรับการยืนยันตัวตนให้คุณ สิ่งที่มันมอบให้คุณด้วยและนักพัฒนาหลายคนใช้ประโยชน์จากมันน้อยเกินไปคือชั้นการทดสอบที่แข็งแกร่งซึ่งสร้างขึ้นบน test runner ของ Django
คู่มือนี้จะแสดงวิธีทดสอบ DRF API สองวิธี อย่างแรกคือการทดสอบอัตโนมัติที่เขียนด้วย Python โดยใช้ APITestCase และ APIClient ของ DRF ซึ่งทำงานได้โดยไม่ต้องมีเซิร์ฟเวอร์ที่ทำงานจริง และสามารถตรวจจับข้อบกพร่องได้ทุกครั้งที่มีการคอมมิต อย่างที่สองคือการทดสอบ endpoints ที่ทำงานอยู่จริงด้วย API client ซึ่งเป็นวิธีที่คุณสำรวจพฤติกรรมและยืนยันบริการจริง ทั้งสองวิธีมีความสำคัญและสามารถตรวจจับปัญหาที่แตกต่างกันได้
ตั้งค่าโปรเจกต์และสภาพแวดล้อมการทดสอบ
เริ่มต้นด้วยสภาพแวดล้อมที่แยกออกมาเพื่อให้การพึ่งพาการทดสอบแยกต่างหากจากส่วนอื่น ๆ ของระบบของคุณ:
python -m venv venv
source venv/bin/activate
pip install django djangorestframework coverage
โปรเจกต์ DRF ทดสอบได้เหมือนกับโปรเจกต์ Django ทั่วไป ตามธรรมเนียม การทดสอบจะอยู่ในไฟล์ tests.py ภายในแต่ละแอป หรือในแพ็กเกจ tests/ หากคุณมีหลายแอป ตัวรันการทดสอบของ Django จะค้นหาคลาสใด ๆ ที่สืบทอดมาจาก TestCase และเมธอดใด ๆ ที่ชื่อขึ้นต้นด้วย test_
ทางเลือกสำคัญคือจะใช้คลาสพื้นฐานใด unittest.TestCase แบบธรรมดาจะไม่มีฐานข้อมูล TestCase ของ Django จะห่อหุ้มการทดสอบแต่ละครั้งด้วย transaction และ rollback มัน ทำให้การทดสอบไม่ส่งผลกระทบต่อกัน APITestCase ของ DRF ขยาย TestCase ของ Django และสลับมาใช้ APIClient ของ DRF ซึ่งเข้าใจการยืนยันตัวตนและประเภทเนื้อหาของ DRF สำหรับงาน API ให้ใช้ APITestCase
ยังมีอีกคลาสหนึ่งที่น่ารู้คือ TransactionTestCase ไม่ได้ห่อหุ้มการทดสอบด้วย transaction ซึ่งคุณจำเป็นต้องใช้เมื่อโค้ดที่กำลังทดสอบจัดการ transaction ด้วยตัวเอง หรือพึ่งพาคุณสมบัติของฐานข้อมูลที่ transaction แบบห่อหุ้มจะซ่อนไว้ มันช้ากว่าเนื่องจากมันตัดตารางระหว่างการทดสอบแทนที่จะ rollback ดังนั้นจึงควรใช้มันเมื่อ TestCase ไม่สามารถจำลองพฤติกรรมได้อย่างแท้จริง สำหรับการทดสอบ endpoint ส่วนใหญ่ APITestCase เป็นตัวเลือกที่เหมาะสมและรวดเร็วที่สุด
การคิดถึงข้อมูลทดสอบตั้งแต่เนิ่นๆ ก็ช่วยได้เช่นกัน ใช้เมธอด setUp เพื่อสร้างแถวที่แต่ละการทดสอบในคลาสต้องการ หรือ setUpTestData หากข้อมูลเป็นแบบอ่านอย่างเดียวและสามารถแชร์ข้ามคลาสเพื่อความเร็วได้ สำหรับโปรเจกต์ที่ใหญ่ขึ้น ไลบรารี factory เช่น factory_boy จะสร้างอินสแตนซ์โมเดลที่ถูกต้องโดยที่คุณไม่ต้องระบุทุกฟิลด์ ซึ่งช่วยให้การทดสอบสั้นและยืดหยุ่นเมื่อโมเดลมีฟิลด์ที่จำเป็นใหม่เพิ่มเข้ามา
ทดสอบ serializers เป็นหน่วยย่อย
Serializers ทำการตรวจสอบข้อมูลและแปลงระหว่างอินสแตนซ์โมเดลและ JSON พวกมันมีขนาดเล็กและบริสุทธิ์ ทำให้เหมาะสำหรับการทดสอบหน่วยที่รวดเร็วโดยไม่ต้องสัมผัส endpoints
สมมติว่าคุณมีโมเดล Article และ ArticleSerializer การทดสอบหน่วยจะตรวจสอบว่าข้อมูลที่ถูกต้องผ่านและข้อมูลที่ไม่ถูกต้องไม่ผ่าน:
from django.test import TestCase
from articles.serializers import ArticleSerializer
class ArticleSerializerTests(TestCase):
def test_valid_data_passes(self):
data = {"title": "Caching strategies", "body": "Use ETags."}
serializer = ArticleSerializer(data=data)
self.assertTrue(serializer.is_valid())
def test_missing_title_fails(self):
data = {"body": "No title here."}
serializer = ArticleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn("title", serializer.errors)
การทดสอบเหล่านี้ใช้เวลาเพียงไม่กี่มิลลิวินาทีเนื่องจากไม่มี HTTP และไม่มี view พวกมันบอกคุณว่ากฎการตรวจสอบข้อมูลถูกต้องก่อนที่คุณจะพิจารณาถึง endpoint ด้วยซ้ำ การครอบคลุมระดับหน่วยนี้เป็นพื้นฐานของ Testing Pyramid ที่อธิบายไว้ใน การทดสอบอัตโนมัติคืออะไร
ทดสอบ endpoints ด้วย APITestCase และ APIClient
การทดสอบ Endpoint ตรวจสอบเส้นทางทั้งหมด: routing, view, serializer และการตอบกลับ APIClient ของ DRF ส่งคำขอแบบ in-process ดังนั้นจึงไม่มีเซิร์ฟเวอร์ทำงานและการทดสอบยังคงรวดเร็ว
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from articles.models import Article
class ArticleEndpointTests(APITestCase):
def setUp(self):
Article.objects.create(title="First post", body="Hello world")
def test_list_articles_returns_200(self):
url = reverse("article-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_create_article(self):
url = reverse("article-list")
payload = {"title": "Second post", "body": "More content"}
response = self.client.post(url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 2)
รายละเอียดบางอย่างที่ควรทราบ self.client คืออินสแตนซ์ APIClient ที่จัดเตรียมโดย APITestCase ใช้ reverse() กับชื่อเส้นทางแทนการฮาร์ดโค้ด URL เพื่อให้การเปลี่ยนแปลงเส้นทางไม่ทำให้การทดสอบทั้งหมดพัง ระบุ format="json" ในการเขียนเพื่อให้ client แปลง payload ได้อย่างถูกต้อง ยืนยันรหัสสถานะด้วยค่าคงที่ที่มีชื่อจาก rest_framework.status เนื่องจากพวกมันอ่านได้ชัดเจนกว่าตัวเลขดิบ รหัสสถานะเหล่านี้ครอบคลุมอยู่ในคู่มือ รหัสสถานะ HTTP ที่ REST API ควรใช้
ทดสอบการยืนยันตัวตนและการอนุญาต
API จริงส่วนใหญ่จะปกป้อง endpoints ของพวกเขา คุณต้องมีการทดสอบที่พิสูจน์ว่าคำขอที่ไม่ได้ยืนยันตัวตนจะถูกปฏิเสธและคำขอที่ยืนยันตัวตนแล้วและมีสิทธิ์ที่ถูกต้องจะสำเร็จ
APIClient มีสองวิธีในการยืนยันตัวตนในการทดสอบ force_authenticate() ข้ามการตรวจสอบข้อมูลประจำตัวและแนบผู้ใช้โดยตรง ซึ่งเหมาะสำหรับการทดสอบตรรกะของ view เอง login() หรือ credentials() จะทดสอบเส้นทางการยืนยันตัวตนจริง นี่คือการทดสอบสิทธิ์โดยใช้ทั้งสองทิศทาง:
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse
class ArticlePermissionTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username="editor", password="testpass123"
)
self.url = reverse("article-list")
def test_anonymous_cannot_create(self):
payload = {"title": "Blocked", "body": "Should fail"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_403_FORBIDDEN
)
def test_authenticated_user_can_create(self):
self.client.force_authenticate(user=self.user)
payload = {"title": "Allowed", "body": "Should pass"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_201_CREATED
)
การทดสอบแรกยืนยันว่าคลาสสิทธิ์บล็อกการเขียนที่ไม่ระบุตัวตน การทดสอบที่สองยืนยันว่าผู้ใช้ที่ยืนยันตัวตนแล้วสามารถดำเนินการได้ ร่วมกันแล้วพวกมันจะระบุขอบเขตของสิทธิ์ ซึ่งเป็นพฤติกรรมประเภทหนึ่งที่มักจะเสียหายโดยไม่รู้ตัวระหว่างการปรับโครงสร้างโค้ด หากคุณกำลังทดสอบ Token Auth โดยเฉพาะ ให้ใช้ self.client.credentials(HTTP_AUTHORIZATION="Token " + token) แทน force_authenticate เพื่อทดสอบเส้นทาง header จริง
ควรไตร่ตรองให้ดีว่าจะใช้อันไหน force_authenticate เร็วกว่าและแยกตรรกะของ view ออกมา จึงเหมาะสำหรับการทดสอบสิทธิ์ส่วนใหญ่ที่คุณสนใจเพียงแค่ว่าบทบาทสามารถหรือไม่สามารถทำบางสิ่งได้ เส้นทางการยืนยันตัวตนจริงคือสิ่งที่คุณต้องการสำหรับการทดสอบชุดเล็กๆ ที่พิสูจน์กลไกการยืนยันตัวตนว่าทำงานได้: โทเค็นที่ไม่ถูกต้องถูกปฏิเสธ โทเค็นที่หมดอายุใช้งานไม่ได้ และ endpoint การเข้าสู่ระบบออกโทเค็นที่ใช้งานได้ การผสมผสานทั้งสองอย่างจะให้การครอบคลุมที่กว้างขวางในราคาถูกและการครอบคลุมเชิงลึกในจุดที่สำคัญ
อย่าลืมสิทธิ์ระดับอ็อบเจกต์ (object-level permissions) DRF APIs จำนวนมากอนุญาตให้ผู้ใช้แก้ไขเรคคอร์ดของตนเองแต่ไม่อนุญาตให้แก้ไขของผู้อื่น ทดสอบสิ่งนี้อย่างชัดเจน: สร้างผู้ใช้สองคน ให้คนหนึ่งสร้างเรคคอร์ด จากนั้นยืนยันว่าผู้ใช้อีกคนได้รับรหัส 403 หรือ 404 เมื่อพยายามแก้ไขมัน Endpoint รายการก็ควรได้รับการตรวจสอบอย่างละเอียดเช่นกัน เนื่องจาก queryset ที่รั่วไหลสามารถส่งคืนแถวที่ผู้ใช้ไม่ควรเห็นได้แม้ว่า endpoint รายละเอียดจะถูกล็อกไว้ก็ตาม
รันชุดทดสอบและวัดความครอบคลุม
รันการทดสอบทั้งหมดด้วย runner ของ Django:
python manage.py test
runner จะค้นหาคลาสทดสอบของคุณ ตั้งค่าฐานข้อมูลทดสอบชั่วคราว รันการทดสอบแต่ละครั้งภายใน transaction และ rollback หลังจากนั้น หากรันได้สะอาดจะแสดงผล OK หากมีข้อผิดพลาด จะแสดง assertion ที่เสียและตำแหน่งที่เกิด
การรู้ว่าชุดทดสอบผ่านไม่ได้หมายความว่าครอบคลุมเพียงพอ เครื่องมือ coverage วัดว่าบรรทัดใดทำงานจริง:
coverage run --source='.' manage.py test
coverage report
coverage html
รายงานจะแสดงไฟล์แต่ละไฟล์พร้อมเปอร์เซ็นต์ของบรรทัดที่ทำงานเอาต์พุต HTML จะไฮไลต์บรรทัดที่ยังไม่ได้ทดสอบเป็นสีแดง ซึ่งจะนำคุณไปยังช่องว่างโดยตรง ตั้งเป้าหมายความครอบคลุมที่มีความหมายของ views, serializers และ permissions แทนที่จะเป็นเพียงตัวเลขหลักเดียว บทความเกี่ยวกับ วิธีเขียนสคริปต์ทดสอบอัตโนมัติ ครอบคลุมสิ่งที่ทำให้การทดสอบคุ้มค่าที่จะเก็บไว้ เนื่องจากความครอบคลุมของการทดสอบที่อ่อนแอไม่ใช่ความปลอดภัยที่แท้จริง
ทดสอบ API ที่ทำงานอยู่จริงด้วย client
การทดสอบ Python อัตโนมัติรวดเร็วและทำงานใน CI แต่พวกมันทดสอบแอปแบบ in-process พวกมันไม่ได้ตรวจจับปัญหาที่ปรากฏขึ้นเฉพาะกับบริการที่กำลังทำงานอยู่เท่านั้น เช่น CORS header ที่ตั้งค่าผิด, reverse proxy ที่ตัดบางอย่างออกไป, ฐานข้อมูลที่ทำงานช้าภายใต้โหลดจริง หรือความแตกต่างระหว่างฐานข้อมูลทดสอบของคุณกับ production สำหรับสิ่งนั้น คุณต้องส่งคำขอจริงไปยัง endpoints ที่ใช้งานจริง
Apidog เป็นตัวเลือกที่เหมาะสมที่นี่ มันเป็นแพลตฟอร์ม API แบบครบวงจร คุณสามารถนำเข้า OpenAPI schema ของ DRF API ของคุณ ส่งคำขอสดไปยังเซิร์ฟเวอร์ที่กำลังทำงานอยู่ และสร้าง assertions ด้วยภาพโดยไม่ต้องเขียน Python เพิ่มเติม DRF สามารถสร้าง OpenAPI schema ได้ และ Apidog สามารถนำเข้าได้โดยตรง ซึ่งช่วยให้ client ของคุณซิงค์กับสัญญาจริง คุณยังสามารถสร้างสถานการณ์การทดสอบแบบหลายขั้นตอนได้ เช่น เข้าสู่ระบบ สร้างบทความ ดึงข้อมูล ลบ และรันตามกำหนดเวลาหรือใน CI สิ่งนี้ช่วยเสริมชุด APITestCase ของคุณมากกว่าที่จะแทนที่: การทดสอบหน่วยและ endpoint จะป้องกันโค้ด API client จะป้องกันบริการที่ใช้งานจริง คุณสามารถ ดาวน์โหลด Apidog เพื่อนำเข้า DRF schema และทดลองใช้ได้ สำหรับทีมที่ต้องการอยู่ใน Python ตั้งแต่ต้นจนจบ คู่มือ pytest API automated testing framework จะแสดงวิธีรันการทดสอบสไตล์ DRF ภายใต้ pytest
คำถามที่พบบ่อย
ความแตกต่างระหว่าง TestCase และ APITestCase คืออะไร?
TestCase ของ Django จะห่อหุ้มการทดสอบแต่ละครั้งด้วย transaction ฐานข้อมูลและให้ Django test client มาตรฐานแก่คุณ APITestCase ของ DRF สืบทอดมาจากมันและสลับ client เป็น APIClient ซึ่งเข้าใจกลไกการยืนยันตัวตนของ DRF, content negotiation และ argument format ในคำขอ ใช้ APITestCase สำหรับการทดสอบ DRF endpoints
ฉันควรใช้ force_authenticate แทน login เมื่อใด?
ใช้ force_authenticate() เมื่อคุณต้องการทดสอบตรรกะของ view และสิทธิ์โดยไม่มีค่าใช้จ่ายและความซับซ้อนของขั้นตอนการยืนยันตัวตนจริง มันจะแนบผู้ใช้กับคำขอโดยตรง ใช้ login() หรือ credentials() เมื่อกลไกการยืนยันตัวตนเอง เช่น session หรือ token auth เป็นสิ่งที่คุณต้องการยืนยัน
การทดสอบ DRF จำเป็นต้องมีเซิร์ฟเวอร์ที่ทำงานอยู่หรือไม่?
ไม่ APIClient จะส่งคำขอแบบ in-process ไปยัง views ของคุณโดยตรง ดังนั้นชุดทดสอบจึงทำงานได้โดยไม่ต้องเริ่มเซิร์ฟเวอร์ นั่นคือสิ่งที่ทำให้มันรวดเร็ว หากต้องการทดสอบบริการที่ใช้งานจริง รวมถึงโครงสร้างพื้นฐานเช่น proxies และ CORS คุณต้องส่งคำขอ HTTP จริงด้วย API client เช่น Apidog
ฉันจะตรวจสอบ test coverage สำหรับโปรเจกต์ DRF ได้อย่างไร?
ติดตั้งแพ็กเกจ coverage จากนั้นรัน coverage run --source='.' manage.py test ตามด้วย coverage report สำหรับสรุป หรือ coverage html สำหรับมุมมองแบบบรรทัดต่อบรรทัด รายงาน HTML จะไฮไลต์บรรทัดที่ยังไม่ได้ทดสอบเพื่อให้คุณเห็นได้อย่างชัดเจนว่า views หรือ serializers ใดขาดการทดสอบ
ฉันควรทดสอบ serializers และ endpoints แยกกันหรือไม่?
ใช่ การทดสอบหน่วยของ Serializer นั้นรวดเร็วและระบุข้อบกพร่องในการตรวจสอบข้อมูลได้โดยไม่มี overhead ของ HTTP การทดสอบ Endpoint ด้วย APITestCase จะตรวจสอบ routing, สิทธิ์ และวงจรคำขอทั้งหมด การเก็บทั้งสองอย่างจะช่วยให้คุณได้รับข้อเสนอแนะอย่างรวดเร็วในระดับหน่วยและความมั่นใจว่าการเชื่อมต่อทำงานได้ในระดับ integration
