在接觸開源社區Github之后,發現特別多的開源項目都會有單元測試TestCase。但是在步入工作后,從業了兩個創業公司,發現大多數程序員都沒有養成寫單元測試的習慣。
在目前的公司面試了一些程序員,他們的工作經驗平均都有三年以上,但是都沒有編寫單元測試的習慣。 問到"為什么不去編寫單元測試呢?",無非就是回答"沒有時間"、"寫的都是接口,直接用客戶端工具測試一下就可以了"。
在筆者使用了Django框架自帶的TestCase之后,發現用TestCase測試接口不僅比一些客戶端工具方便,而且還能降低在對代碼進行修改之后出現BUG的幾率, 特別是一些對代碼有嚴重的潔癖喜歡優化代碼的程序員來說真的非常有用。
而且運用框架的TestCase編寫單元測試,還能結合一些CI工具來實現自動化測試,這個我也會專門寫一篇文章來介紹我利用Gitlab CI結合Django的TestCase實現自動化測試的一些心得。
TestCase 類的結構
為了方便沒用用過TestCase的讀者,先簡單介紹一下TestCase的類結構。
常見的TestCase由setUp函數、tearDown函數和test_func組成。
這里test_func是指你編寫了測試邏輯的函數,而setUp函數則是在test_func函數之前執行的函數,tearDown函數則是在test_func執行之后執行的函數。
from django.test import TestCase
class Demo(TestCase):
def setUp(self): print('setUp') def tearDown(self): print('tearDown') def test_demo(self): print('test_demo') def test_demo_2(self): print('test_demo2')復制代碼
我們可以通過在Django項目的根目錄運行以下命令來運行這個單元測試
python manage.py test development_of_test_habits.tests.test_demo.Demo 復制代碼
如果使用Pycharm來運行的話可以直接點擊類左側的運行箭頭,更加方便地運行或者Debug這個單元測試。
可以從運行后的結果清晰的看到這個單元測試的執行順序。
Creating test database for alias 'default'... System check identified no issues (0 silenced). setUp test_demo tearDown .setUp test_demo2 tearDown . ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK Destroying test database for alias 'default'... 復制代碼
此外還可以從運行結果看到,在測試之前單元測試創建了一個測試數據庫。
Creating test database for alias ‘default’…
然后在測試結束將數據庫摧毀。
Destroying test database for alias ‘default’…
這個也就是在繼承了Django框架中的TestCase,它已經幫你實現的一些邏輯方便用於測試,所以我們不需要在setUp和tearDown函數中實現這些邏輯。
利用TestCase測試接口
接下來講一下我們如何使用TestCase來測試接口的,首先我們編寫一個簡單的接口,這里筆者是用Django Rest Framework的APIView來編寫的,讀者也可以使用自己管用的方法來編寫。
from rest_framework.views import APIView
from rest_framework.response import Response
class HelloTestCase(APIView):
def get(self, request, *args, **kwargs):
return Response({ 'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',') })復制代碼
然后這個接口類加到我們的路由中。
from django.urls import path
from development_of_test_habits import views
urlpatterns = [
path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'), ]復制代碼
接下來我們編寫一個HelloTestCase的單元測試類來測試我們的測試用例。
from django.urls import resolve, reverse
from django.test import TestCase
class HelloTestCase(TestCase):
def setUp(self): self.name = 'Django' def test_hello_test_case(self): url = '/test_case/hello_test_case' # url = reverse('hello_test_case') # Input: print(resolve(url)) # Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[]) response = self.client.get(url) self.assertEqual(response.status_code, 200) # 期望的Http相應碼為200 data = response.json() self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回結果為'Hello , I am a test Case' response = self.client.get(url, {'name': self.name}) self.assertEqual(response.status_code, 200) # 期望的Http相應碼為200 data = response.json() self.assertEqual(data['msg'], 'Hello Django I am a test Case') # 期望的msg返回結果為'Hello Django I am a test Case'復制代碼
在setUp函數中,我定義了一個name屬性並且賦值為Django便於后面使用。
單元測試測試接口主要分為下面幾個重要的內容。
請求的路由地址
在測試接口時無非就是發起請求,檢查返回的狀態嘛和響應內容是否正確。發請求肯定少不了url地址,這里有兩種方式來配置請求地址。
1.直接設置請求地址
url = '/test_case/hello_test_case' 復制代碼
2.透過django.urls.reverse函數和在路由設置的name來得到請求的地址
url = reverse('hello_test_case') 復制代碼
這里在介紹以下我們還可以通過django.urls.resolve和url得到對應的接口類或者接口函數`。
請求的客戶端
發起請求我們除了需要路由外,我們還需要一個發起請求的客戶端。python的requests庫就是很好的客戶端工具,只不過Django在它的TestCase類 中已經集成了一個客戶端工具,我們只需要調用TestCase的client屬性就可以得到一個客戶端。
client = self.client
復制代碼
發起請求
發起請求非常簡單只需要一行代碼,我們就可以通過請求得到它的響應體。
response = self.client.get(url)
復制代碼
如果需要攜帶參數只需要傳入data參數。
response = self.client.get(url, {'name': self.name}) 復制代碼
驗證響應體
在單元測試中,TestCase的assertEqual有點類似python的assert函數,除了assertEqual外還有assertNotEqual、assertGreater、assertIn等等。 這里筆者主要做了兩個檢查,一個是檢查status_code是否等於200。
self.assertEqual(response.status_code, 200) # 期望的Http相應碼為200 復制代碼
另一個是檢查響應內容是否正確。
data = response.json()
self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回結果為'Hello , I am a test Case' 復制代碼
這個就是最簡單的測試請求的單元測試,但是在實際的接口中,我們是需要數據的,所以我們還需要生成測試數據。
這里介紹一個非常方便的庫mixer,可以方便在我們的單元測試中生成測試數據。
利用mixer在TestCase中生成測試數據
首先我們定一個場景,比如說我們記錄了學校班級的學生的作業,需要一個接口來返回學生的作業列表,並且這個接口是需要用戶登陸后才可以請求的,定義的models和接口類如下。
from django.db import models
class School(models.Model):
name = models.CharField(max_length=32)
class Class(models.Model):
school_id = models.ForeignKey(to=School, on_delete=models.PROTECT)
name = models.CharField(max_length=32)
class Student(models.Model):
class_id = models.ForeignKey(to=Class, on_delete=models.PROTECT)
name = models.CharField(max_length=32)
class HomeWork(models.Model):
student_id = models.ForeignKey(to=Student, on_delete=Student)
name = models.CharField(max_length=32)復制代碼
接口筆者用的是Django rest framework的ReadOnlyModelViewSet視圖類實現的,實現的功能就是返回一個json結果集, 並且json中有HomeWork的School Name、Class Name和Student Name,視圖類代碼和序列化代碼如下。
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated
from development_of_test_habits.models import HomeWork
from development_of_test_habits.serializers import HomeWorkSerializer
class HomeWorkViewSet(ReadOnlyModelViewSet):
queryset = HomeWork.objects.all()
serializer_class = HomeWorkSerializer
permission_classes = (IsAuthenticated, )復制代碼
from rest_framework import serializers
from development_of_test_habits.models import HomeWork
class HomeWorkSerializer(serializers.ModelSerializer):
class Meta:
model = HomeWork
fields = ('school_name', 'class_name', 'student_name', 'name') school_name = serializers.CharField(source='student_id.class_id.school_id.name', read_only=True) class_name = serializers.CharField(source='student_id.class_id.name', read_only=True) student_name = serializers.CharField(source='student_id.name', read_only=True)復制代碼
最后把我們的接口類添加到路由中。
urlpatterns = [
path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'), path('api/home_works', views.HomeWorkViewSet.as_view({'get': 'list'}), name='home_works_list') ]復制代碼
完成接口的編寫,可以開始寫單元測試了,定義HomeWorkAPITestCase測試類並且在setUp中生成測試數據。
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from mixer.backend.django import mixer
from development_of_test_habits import models
class HomeWorkAPITestCase(TestCase):
def setUp(self): self.user = mixer.blend(User) self.random_home_works = [ mixer.blend(models.HomeWork) for _ in range(11) ]復制代碼
這里介紹一下mixer這個模塊,這個模塊會根據你定義的模型和模型的字段來隨機生成測試數據,包括這個數據的外鍵數據。 這樣在我們這種層級非常多的關系型數據就非常的方便,否則需要一層一層的去生成數據。 代碼中就利用mixer生成了一個隨機的用戶和11個隨機的HomeWork數據。
接下來編寫測試的邏輯代碼。
class HomeWorkAPITestCase(TestCase):
def setUp(self): self.user = mixer.blend(User) self.random_home_works = [ mixer.blend(models.HomeWork) for _ in range(11) ] def test_home_works_list_api(self): url = reverse('home_works_list') response = self.client.get(url) self.assertEqual(response.status_code, 403) self.client.force_login(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(len(data), len(self.random_home_works)) data_fields = [key for key in data[0].keys()] self.assertIn('school_name', data_fields) self.assertIn('class_name', data_fields) self.assertIn('student_name', data_fields) self.assertIn('name', data_fields)復制代碼
首先通過django.urls.reverse函數和接口的路由名稱獲得url,第一步先測試用戶在沒有登陸的情況下請求接口,這里期望的請求響應碼為403。
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
復制代碼
我們通過client的一個登陸函數force_login來登陸我們隨機生成的用戶,再次請求接口,這次的期望的請求相應碼就為200。
self.client.force_login(self.user)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
復制代碼
最后驗證返回的結果數量是和結果中定義的字段是否正確。
data = response.json()
self.assertEqual(len(data), len(self.random_home_works))
data_fields = [key for key in data[0].keys()] self.assertIn('school_name', data_fields) self.assertIn('class_name', data_fields) self.assertIn('student_name', data_fields) self.assertIn('name', data_fields) 復制代碼
以上就是在項目中測試接口的最常見的流程。
TestCase在使用中需要注意的一些問題
假設我們要在接口中增加請求頭,以HelloTestCase接口為例,我們要增加一個TEST_HEADER的請求頭,則在接口的邏輯處理中,就需要給這個請求頭 加上HTTP_前綴。
class HelloTestCase(APIView):
def get(self, request, *args, **kwargs):
data = {
'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',') } test_header = request.META.get('HTTP_TEST_HEADER') if test_header: data['test_header'] = test_header return Response(data)復制代碼
如果我們用客戶端工具類似Post Man、RestFul Client等等,請求時只要在請求頭中加上TEST_HEADER即可。 但是在單元測試中,我們也需要把HTTP_這個前綴加上,否則接口邏輯是無法獲取的。
def test_hello_test_case(self):
url = '/test_case/hello_test_case' # url = reverse('hello_test_case') # Input: print(resolve(url)) # Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[]) response = self.client.get(url) self.assertEqual(response.status_code, 200) # 期望的Http相應碼為200 data = response.json() self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回結果為'Hello , I am a test Case' response = self.client.get(url, {'name': self.name}) self.assertEqual(response.status_code, 200) # 期望的Http相應碼為200 data = response.json() self.assertEqual(data['msg'], 'Hello Django I am a test Case') # 期望的msg返回結果為'Hello Django I am a test Case' # 假設我們要在接口中增加請求頭'TEST_HEADER' # 則在測試時需要加上前綴'HTTP_'最終的結果為'HTTP_TEST_HEADER' response = self.client.get(url, **{'HTTP_TEST_HEADER': 'This is a test header.'}) data = response.json() self.assertEqual(data['test_header'], 'This is a test header.')復制代碼
總結
在用測試用例來測試接口后,筆者已經開始養成寫完接口直接用單元測試來測試的習慣,這樣不單是在給別人說明自己的接口的功能,還是減少線上環境的BUG都有明顯的幫助, 希望讀者也能用這種方式不斷的養成寫單元測試的好習慣。
作者:elfgzp
鏈接:https://juejin.im/post/6844903742274273287
