在接觸開源社群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 TestCaseclass 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).setUptest_demotearDown.setUptest_demo2tearDown.----------------------------------------------------------------------Ran 2 tests in 0.001sOKDestroying 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 APIViewfrom rest_framework.response import Responseclass 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 pathfrom development_of_test_habits import viewsurlpatterns = [ path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'),]複製程式碼
接下來我們編寫一個HelloTestCase
的單元測試類來測試我們的測試用例。
from django.urls import resolve, reversefrom django.test import TestCaseclass 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 modelsclass 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 ReadOnlyModelViewSetfrom rest_framework.permissions import IsAuthenticatedfrom development_of_test_habits.models import HomeWorkfrom development_of_test_habits.serializers import HomeWorkSerializerclass HomeWorkViewSet(ReadOnlyModelViewSet): queryset = HomeWork.objects.all() serializer_class = HomeWorkSerializer permission_classes = (IsAuthenticated, )複製程式碼
from rest_framework import serializersfrom development_of_test_habits.models import HomeWorkclass 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 TestCasefrom django.urls import reversefrom django.contrib.auth.models import Userfrom mixer.backend.django import mixerfrom development_of_test_habits import modelsclass 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.cn/2018/12/07/…