整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          單元測試實(shí)戰(zhàn)-四種覆蓋詳解、測試實(shí)例

          單元測試實(shí)戰(zhàn)-四種覆蓋詳解、測試實(shí)例

          擊上方 "程序員小樂"關(guān)注公眾號, 星標(biāo)或置頂一起成長



          者:HelloGitHub-追夢人物

          一個完整的項(xiàng)目,無論是個人的還是公司的,自動化的單元測試是必不可少,否則以后任何的功能改動將成為你的災(zāi)難。

          假設(shè)你正在維護(hù)公司的一個項(xiàng)目,這個項(xiàng)目已經(jīng)開發(fā)了幾十個 API 接口,但是沒有任何的單元測試。現(xiàn)在你的 leader 讓你去修改幾個接口并實(shí)現(xiàn)一些新的功能,你接到需求后高效地完成了開發(fā)任務(wù),然后手動測試了一遍改動的接口和新實(shí)現(xiàn)的功能,確保沒有任何問題后,滿心歡喜地提交了代碼。

          代碼上線后出了 BUG,分析原因發(fā)現(xiàn)原來是新的改動導(dǎo)致某個舊 API 接口出了問題,因?yàn)樯暇€前只對改動的接口做了測試,所以未能發(fā)現(xiàn)這個問題。你的 leader 批評了你,你因?yàn)槭鹿视浟诉^,年終只能拿個 3.25,非常凄慘。

          但是如果我們有全面的單元測試,上述情況就有很大概率避免。只需要在代碼發(fā)布前運(yùn)行一遍單元測試,受影響的功能立即就會報錯,這樣就能在代碼部署前發(fā)現(xiàn)問題,從而避免線上事故。

          當(dāng)然以上故事純屬虛構(gòu),說這么多只是希望大家在開發(fā)時養(yǎng)成良好的習(xí)慣,一是寫優(yōu)雅的代碼,二是一定要測試自己寫的代碼

          單元測試回顧

          在上一部教程 Django博客教程(第二版)單元測試:測試 blog 應(yīng)用單元測試:測試評論應(yīng)用Coverage.py 統(tǒng)計測試覆蓋率 中,我們詳細(xì)講解了 django 單元測試框架的使用方式。這里我們再對 djnago 的測試框架做一個回顧整體回顧,至于如何編寫和運(yùn)行測試,后面將會進(jìn)行詳細(xì)的講解,如果想對 django 的單元測試做更基礎(chǔ)的了解,推薦回去看看關(guān)于測試的 3 篇教程以及 django 的官方文檔。

          下面是 djnago 單元測試框架的一些要點(diǎn):

          • django 的單元測試框架基于 Python 的 unittest 測試框架。
          • django 提供了多個 XXTestCase 類,這些類均直接或者間接繼承自 unittest.TestCase 類,因?yàn)?django 的單元測試框架是基于 unittest 的,所以編寫的測試用例類也都需要直接或者間接繼承 unittest.TestCase。通常情況我們都是繼承 django 提供的 XXTestCase,因?yàn)檫@些類針對 django 定制了更多的功能特性。
          • 默認(rèn)情況下,測試代碼需要放在 django 應(yīng)用的下的 tests.py 文件或者 tests 包里,django 會自動發(fā)現(xiàn) tests 包中以 test 開頭的模塊(例如 test_models.py、test_views.py),然后執(zhí)行測試用例類中命名以 test 開頭的方法。
          • python manage.py test 命令可以運(yùn)行單元測試。

          梳理需要測試的接口

          接下來我們就為博客的 API 接口來編寫單元測試。對 API 接口來說,我們主要關(guān)心的就是:對特定的請求返回正確的響應(yīng)。我們先來梳理一下需要測試的接口和功能點(diǎn)。

          博客主要的接口都集中在 PostViewSet 和 CommentViewSet 兩個視圖集中。

          • CommentViewSet 視圖集的接口比較簡單,就是創(chuàng)建評論。
          • PostViewSet 視圖集的接口則包含了文章列表、文章詳情、評論列表、歸檔日期列表等。對于文章列表接口,還可以通過查詢參數(shù)對請求的文章列表資源進(jìn)行過濾,獲取全部文章的一個子集。

          測試 CommentViewSet

          CommentViewSet 只有一個接口,功能比較簡單,我們首先以它為例來講解單元測試的編寫方式。

          測試接口的一般步驟:

          1. 獲得接口的 URL。
          2. 向接口發(fā)送請求。
          3. 檢查響應(yīng)的 HTTP 狀態(tài)碼、返回的數(shù)據(jù)等是否符合預(yù)期。

          我們以測試創(chuàng)建評論的代碼 test_create_valid_comment 為例:

          # filename="comments/tests/test_api.py
          from django.apps import apps
          from django.contrib.auth.models import User
          from rest_framework import status
          from rest_framework.reverse import reverse
          from rest_framework.test import APITestCase
          
          from blog.models import Category, Post
          from comments.models import Comment
          
          
          class CommentViewSetTestCase(APITestCase):
              def setUp(self):
                  self.url = reverse("v1:comment-list")
                  # 斷開 haystack 的 signal,測試生成的文章無需生成索引
                  apps.get_app_config("haystack").signal_processor.teardown()
                  user = User.objects.create_superuser(
                      username="admin", email="admin@hellogithub.com", password="admin"
                  )
                  cate = Category.objects.create(name="測試")
                  self.post = Post.objects.create(
                      title="測試標(biāo)題", body="測試內(nèi)容", category=cate, author=user,
                  )
          
              def test_create_valid_comment(self):
                  data = {
                      "name": "user",
                      "email": "user@example.com",
                      "text": "test comment text",
                      "post": self.post.pk,
                  }
                  response = self.client.post(self.url, data)
                  self.assertEqual(response.status_code, status.HTTP_201_CREATED)
          
                  comment = Comment.objects.first()
                  self.assertEqual(comment.name, data["name"])
                  self.assertEqual(comment.email, data["email"])
                  self.assertEqual(comment.text, data["text"])
                  self.assertEqual(comment.post, self.post)

          首先,接口的 URL 地址為:reverse("v1:comment-list")。reverse 函數(shù)通過視圖函數(shù)名來解析對應(yīng)的 URL,視圖函數(shù)名的格式為:"<namespace>:<basename>-<action name>"。

          其中 namespace 是 include 函數(shù)指定的 namespace 參數(shù)值,例如:

          path("api/v1/", include((router.urls, "api"), namespace="v1"))

          basename 是 router 在 register 視圖集時指定的參數(shù) basename 的值,例如:

          router.register(r"posts", blog.views.PostViewSet, basename="post")

          action name 是 action 裝飾器指定的 url_name 參數(shù)的值,或者默認(rèn)的 list、retrieve、create、update、delete 標(biāo)準(zhǔn) action 名,例如:

          # filename="blog/views.py
          @action(
           methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
          )
          def list_archive_dates(self, request, *args, **kwargs):
           pass

          因此,reverse("v1:comment-list") 將被解析為 /api/v1/comments/。

          接著我們向這個 URL 發(fā)送 POST 請求:response=self.client.post(self.url, data),因?yàn)槔^承自 django-reset-framework 提供的測試類 APITestCase,因此可以直接通過 self.client 來發(fā)送請求,其中 self.client 是 django-rest-framework 提供的 APIClient 的一個實(shí)例,專門用來發(fā)送 HTTP 測試請求。

          最后就是對請求的響應(yīng)結(jié)果 response 做檢查。創(chuàng)建評論成功后返回的狀態(tài)碼應(yīng)該是 201,接口返回的數(shù)據(jù)在 response.data 屬性中,我們對接口返回的狀態(tài)碼和部分?jǐn)?shù)據(jù)進(jìn)行了斷言,確保符合預(yù)期的結(jié)果。

          當(dāng)然以上是評論創(chuàng)建成功的情況,我們測試時不能只測試正常情況,更要關(guān)注邊界情況和異常情況,我們再來增加一個評論數(shù)據(jù)格式不正確導(dǎo)致創(chuàng)建失敗的測試案例:

          # filename="comments/tests/test_api.py
          def test_create_invalid_comment(self):
              invalid_data = {
                  "name": "user",
                  "email": "user@example.com",
                  "text": "test comment text",
                  "post": 999,
              }
              response = self.client.post(self.url, invalid_data)
              self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
              self.assertEqual(Comment.objects.count(), 0)

          套路還是一樣的,第一步向接口發(fā)請求,然后對預(yù)期返回的響應(yīng)結(jié)果進(jìn)行斷言。這里由于評論數(shù)據(jù)不正確(關(guān)聯(lián)的 id 為 999 的 post 不存在),因此預(yù)期返回的狀態(tài)碼是 400,同時數(shù)據(jù)庫中不應(yīng)該有創(chuàng)建的評論。

          測試 PostViewSet

          盡管 PostViewSet 包含的接口比較多,但是每個接口測試的套路和上面講的是一樣的,依葫蘆畫瓢就行了。因?yàn)?PostViewSet 測試代碼較多,這里僅把各個測試案例對應(yīng)的方法列出來,具體的測試邏輯省略掉。如需了解詳細(xì)可查看 GitHub 上項(xiàng)目的源碼:

          # filename="blog/tests/test_api.py
          from datetime import datetime
          
          from django.apps import apps
          from django.contrib.auth.models import User
          from django.core.cache import cache
          from django.urls import reverse
          from django.utils.timezone import utc
          from rest_framework import status
          from rest_framework.test import APITestCase
          
          from blog.models import Category, Post, Tag
          from blog.serializers import PostListSerializer, PostRetrieveSerializer
          from comments.models import Comment
          from comments.serializers import CommentSerializer
          
          
          class PostViewSetTestCase(APITestCase):
              def setUp(self):
                  # 斷開 haystack 的 signal,測試生成的文章無需生成索引
                  apps.get_app_config("haystack").signal_processor.teardown()
                  # 清除緩存,防止限流
                  cache.clear()
          
                  # 設(shè)置博客數(shù)據(jù)
                  # post3 category2 tag2 2020-08-01 comment1 comment2
                  # post2 category1 tag1 2020-07-31
                  # post1 category1 tag1 2020-07-10
          
              def test_list_post(self):
                  """
                  這個方法測試文章列表接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為文章列表序列化后的結(jié)果
                  """
                  url = reverse("v1:post-list")
          
              def test_list_post_filter_by_category(self):
                  """
                  這個方法測試獲取某個分類下的文章列表接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為文章列表序列化后的結(jié)果
                  """
                  url = reverse("v1:post-list")
                  
          
              def test_list_post_filter_by_tag(self):
                  """
                  這個方法測試獲取某個標(biāo)簽下的文章列表接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為文章列表序列化后的結(jié)果
                  """
                  url = reverse("v1:post-list")
                  
          
              def test_list_post_filter_by_archive_date(self):
                  """
                  這個方法測試獲取歸檔日期下的文章列表接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為文章列表序列化后的結(jié)果
                  """
                  url = reverse("v1:post-list")
                  
          
              def test_retrieve_post(self):
                  """
                  這個方法測試獲取單篇文章接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為單篇文章序列化后的結(jié)果
                  """
                  url = reverse("v1:post-detail", kwargs={"pk": self.post1.pk})
                  
          
              def test_retrieve_nonexistent_post(self):
                  """
                  這個方法測試獲取一篇不存在的文章,預(yù)期的響應(yīng)狀態(tài)碼為 404
                  """
                  url = reverse("v1:post-detail", kwargs={"pk": 9999})
                  
          
              def test_list_archive_dates(self):
                  """
                  這個方法測試獲取文章的歸檔日期列表接口
                  """
                  url = reverse("v1:post-archive-date")
                  
          
              def test_list_comments(self):
                  """
                  這個方法測試獲取某篇文章的評論列表接口,預(yù)期的響應(yīng)狀態(tài)碼為 200,數(shù)據(jù)為評論列表序列化后的結(jié)果
                  """
                  url = reverse("v1:post-comment", kwargs={"pk": self.post3.pk})
                  
          
              def test_list_nonexistent_post_comments(self):
                  """
                  這個方法測試獲取一篇不存在的文章的評論列表,預(yù)期的響應(yīng)狀態(tài)碼為 404
                  """
                  url = reverse("v1:post-comment", kwargs={"pk": 9999})

          我們以 test_list_post_filter_by_archive_date 為例做一個講解,其它的測試案例代碼邏輯大同小異。

          # filename="blog/tests/test_api.py
          def test_list_post_filter_by_archive_date(self):
              # 解析文章列表接口的 URL
              url = reverse("v1:post-list")
              
              # 發(fā)送請求,我們這里給 get 方法的第二個參數(shù)傳入了一個字典,這個字典代表了 get 請求的查詢參數(shù)。
              # 例如最終的請求的 URL 會被編碼成:/posts/?created_year=2020&created_month=7
              response = self.client.get(url, {"created_year": 2020, "created_month": 7})
              self.assertEqual(response.status_code, status.HTTP_200_OK)
              
              # 如何檢查返回的數(shù)據(jù)是否正確呢?對這個接口的請求,
              # 我們預(yù)期返回的結(jié)果是 post2 和 post1 這兩篇發(fā)布于2020年7月的文章序列化后的數(shù)據(jù)。
              # 因此,我們使用 PostListSerializer 對這兩篇文章進(jìn)行了序列化,
              # 然后和返回的結(jié)果 response.data["results"] 進(jìn)行比較。
              serializer = PostListSerializer(instance=[self.post2, self.post1], many=True)
              self.assertEqual(response.data["results"], serializer.data)

          運(yùn)行測試

          接下來運(yùn)行測試:

          "Linux/macOS"
          $ pipenv run coverage run manage.py test
          
          "Windows"
          ...\> pipenv run coverage run manage.py test

          大部分測試都通過了,但是也有一個測試失敗了,也就是說我們通過測試發(fā)現(xiàn)了一個 BUG:

          ======================================================================FAIL: test_list_archive_dates (blog.tests.test_api.PostViewSetTestCase)
          ----------------------------------------------------------------------
          Traceback (most recent call last):
            File "C:\Users\user\SpaceLocal\Workspace\G_Courses\HelloDjango\HelloDjango-rest-framework-tutorial\blog\tests\test_api.py", line 123, in test_list_archive_dates
              self.assertEqual(response.data, ["2020-08", "2020-07"])
          AssertionError: Lists differ: ['2020-08-01', '2020-07-01'] != ['2020-08', '2020-07']

          失敗的是 test_list_archive_dates 這個測試案例,文章歸檔日期接口返回的數(shù)據(jù)不符合我們的預(yù)期,我們預(yù)期得到 yyyy-mm 格式的日期列表,但接口返回的是 yyyy-mm-dd,這是我們之前開發(fā)時沒有發(fā)現(xiàn)的,通過測試將問題暴露了,這也從一定程度上印證了我們之前強(qiáng)調(diào)的測試的作用。

          既然已經(jīng)發(fā)現(xiàn)了問題,就來修復(fù)它。我相信修復(fù)這個 bug 對你來說應(yīng)該已經(jīng)是輕而易舉的事了,因此留作練習(xí)吧,這里不再講解。

          重新運(yùn)行一遍測試,得到 ok 的狀態(tài)。

          Ran 55 tests in 8.997s

          OK

          說明全部測試通過。

          檢查測試覆蓋率

          以上測試充分了嗎?單憑肉眼自然很難發(fā)現(xiàn),Coverage.py 統(tǒng)計測試覆蓋率 中我們配置了 Coverage.py 并介紹了它的用法,直接運(yùn)行下面的命令就可以查看代碼的測試覆蓋程度:

          "Linux/macOS"
          $ pipenv run coverage report
          
          "Windows"
          ...\> pipenv run coverage report

          覆蓋結(jié)果如下:

          Name                  Stmts   Miss Branch BrPart  Cover   Missing
          -----------------------------------------------------------------
          blog\serializers.py      46      5      0      0    89%   82-86
          blog\utils.py            21      2      4      1    88%   29->30, 30-31
          blog\views.py           119      5      4      0    94%   191, 200, 218-225
          comments\views.py        25      1      2      0    96%   59
          -----------------------------------------------------------------
          TOTAL                  1009     13     34      1    98%

          可以看到測試覆蓋率整體達(dá)到了 98%,但是仍有 4 個文件部分代碼未被測試,命令行中只給出了未被測試覆蓋的代碼行號(Missing 列),不是很直觀,運(yùn)行下面的命令可以生成一個 HTML 報告,可視化地查看未被測試覆蓋的代碼片段:

          "Linux/macOS"
          $ pipenv run coverage html
          
          "Windows"
          ...\> pipenv run coverage html

          命令執(zhí)行后會在項(xiàng)目根目錄生成一個 htmlcov 文件夾,用瀏覽器打開里面的 index.html 頁面就可以查看測試覆蓋情況的詳細(xì)報告了。

          HTML 報告頁面示例:



          未覆蓋的代碼通過紅色高亮背景標(biāo)出,非常直觀。可以看到 blog/views.py 中 CategoryViewSet 和 TagViewSet 未進(jìn)行測試,按照上面介紹的測試方法補(bǔ)充測試就可以啦。這兩個視圖集都非常的簡單,測試的任務(wù)就留作練習(xí)了。

          補(bǔ)充測試

          blog/serializers.py 中的 HighlightedCharField 未測試,還有 blog/utils.py 中新增的 UpdatedAtKeyBit 未測試,我們編寫相應(yīng)的測試案例。

          測試 UpdatedAtKeyBit

          UpdatedAtKeyBit 就只有一個 get_data 方法,這個方法預(yù)期的邏輯是:從緩存中取得以 self.key 為鍵的緩存值(緩存被設(shè)置時的時間),如果緩存未命中,就取當(dāng)前時間,并將這個時間寫入緩存。

          將預(yù)期的邏輯寫成測試代碼如下,需要注意的一點(diǎn)是因?yàn)檫@個輔助類不涉及 django 數(shù)據(jù)庫方面的操作,因此我們直接繼承自更為簡單的 unittest.TestCase,這可以提升測試速度:

          # filename="blog/tests/test_utils.py
          import unittest
          from datetime import datetime
          
          from django.core.cache import cache
          
          from ..utils import Highlighter, UpdatedAtKeyBit
          
          class UpdatedAtKeyBitTestCase(unittest.TestCase):
              def test_get_data(self):
                  # 未緩存的情況
                  key_bit = UpdatedAtKeyBit()
                  data = key_bit.get_data()
                  self.assertEqual(data, str(cache.get(key_bit.key)))
          
                  # 已緩存的情況
                  cache.clear()
                  now = datetime.utcnow()
                  now_str = str(now)
                  cache.set(key_bit.key, now)
                  self.assertEqual(key_bit.get_data(), now_str)

          測試 HighlightedCharField

          我們在講解自定義系列化字段的時候講過,序列化字段通過調(diào)用 to_representation 方法,將傳入的值進(jìn)行序列化。HighlightedCharField 的預(yù)期邏輯就是調(diào)用 to_representation 方法后將傳入的值進(jìn)行高亮處理。

          HighlightedCharField 涉及到一些高級操作,主要是因?yàn)?to_representation 方法中涉及到對 HTTP 請求request 的操作。正常的視圖函數(shù)調(diào)用時,視圖函數(shù)會接收到傳入的 request 參數(shù),然后 django-rest-framework 會將 request 傳給序列化器(Serializer)的 _context 屬性,序列化器中的任何序列化字段均可以通過直接訪問 context 屬性而間接訪問到 _context 屬性,從而拿到 request 對象。

          但是在單元測試中,可能沒有這樣的視圖函數(shù)調(diào)用,因此 _context 的設(shè)置并不會自動進(jìn)行,需要我們模擬視圖函數(shù)調(diào)用時的行為,手動進(jìn)行設(shè)置。主要包括 2 點(diǎn):

          1. 構(gòu)造 HTTP 請求對象 request。
          2. 設(shè)置 _context 屬性的值。

          具體的代碼如下,詳細(xì)講解請看相關(guān)代碼行的注釋:

          # filename="blog/tests/test_serializer.py
          import unittest
          
          from blog.serializers import HighlightedCharField
          from django.test import RequestFactory
          from rest_framework.request import Request
          
          
          class HighlightedCharFieldTestCase(unittest.TestCase):
              def test_to_representation(self):
                  field = HighlightedCharField()
                  # RequestFactory 專門用來構(gòu)造 request 對象。
                  # 這個 RequestFactory 生成的 request 代表了一個對 URL / 訪問的 get 請求,
                  # 并包含 URL 參數(shù) text=關(guān)鍵詞。
                  # 請求訪問的完整 URL 就是 /?text=關(guān)鍵詞
                  request = RequestFactory().get("/", {"text": "關(guān)鍵詞"})
                  
                  # django-rest-framework 對 django 內(nèi)置的 request 進(jìn)行了包裝,
                  # 因此這里要手動使用 drf 提供的 Request 類對 django 的 request 進(jìn)行一層包裝。
                  drf_request = Request(request=request)
                  
                  # 設(shè)置 HighlightedCharField 實(shí)例 _context 屬性的值,這樣在其內(nèi)部就可以通過
                  # self.context["request"] 拿到請求對象 request
                  setattr(field, "_context", {"request": drf_request})
                  document = "無關(guān)文本關(guān)鍵詞無關(guān)文本,其他別的關(guān)鍵詞別的無關(guān)的詞。"
                  result = field.to_representation(document)
                  expected = (
                      '無關(guān)文本<span class="highlighted">關(guān)鍵詞</span>無關(guān)文本,'
                      '其他別的<span class="highlighted">關(guān)鍵詞</span>別的無關(guān)的詞。'
                  )
                  self.assertEqual(result, expected)

          再次運(yùn)行一遍測試覆蓋率的檢查命令,這次得到的測試覆蓋率就是 100% 了:

          Name    Stmts   Miss Branch BrPart  Cover   Missing
          ---------------------------------------------------
          ---------------------------------------------------
          TOTAL    1047      0     32      0   100%

          當(dāng)然,需要提醒一點(diǎn)的是,測試覆蓋率 100% 并不能說明程序就沒有 BUG 了。線上可能出現(xiàn)各種奇奇怪怪的問題,這些問題可能并沒有寫成測試案例,所以也就沒有測試到。但無論如何,目前我們已經(jīng)進(jìn)行了較為充分的測試,就可以考慮發(fā)布一個版本了。如果以后再線上遇到什么問題,或者想到了新的測試案例,可以隨時補(bǔ)充進(jìn)單元測試,以后程序出 BUG 的幾率就會越來越低了。

          在我的日常工作中,我是一名專業(yè)程序員。我使用c++、c#和Javascript。我是一個開發(fā)團(tuán)隊(duì)的一員,他們使用單元測試來驗(yàn)證我們的代碼是否按照它應(yīng)該的方式工作。

          在本文中,我將通過討論以下主題來研究如何使用Python創(chuàng)建單元測試。

          • 單元測試基礎(chǔ)
          • 可用的Python測試框架
          • 測試設(shè)計原則
          • 代碼覆蓋率

          單元測試基礎(chǔ)

          我使用FizzBuzz編碼方式創(chuàng)建了單元測試示例。編碼類型是程序員的練習(xí)。在這個練習(xí)中,程序員試圖解決一個特定的問題。但主要目標(biāo)不是解決問題,而是練習(xí)編程。FizzBuz是一個簡單的代碼類型,非常適合解釋和展示Python中的單元測試。

          單元測試

          單元測試是程序員為測試程序的一小部分而編寫的自動化測試。單元測試應(yīng)該運(yùn)行得很快。與文件系統(tǒng)、數(shù)據(jù)庫或網(wǎng)絡(luò)交互的測試不是單元測試。

          為了在Python中創(chuàng)建第一個FizzBuzz單元測試,我定義了一個繼承自unittest.TestCase的類。這個unittest模塊可以在Python的標(biāo)準(zhǔn)安裝中獲得。

          import unittest
          class FizzBuzzTest(unittest.TestCase):
              def test_one_should_return_one(self):
                  fizzbuzz=FizzBuzz()
                  result=fizzbuzz.filter(1)
                  self.assertEqual('1', result)
          
          
              def test_two_should_return_two(self):
                  fizzbuzz=FizzBuzz()
                  result=fizzbuzz.filter(2)
                  self.assertEqual('2', result)


          第一個測試用例驗(yàn)證數(shù)字1是否通過了FizzBuzz過濾器,它將返回字符串' 1 '。使用self驗(yàn)證結(jié)果。assertEqual方法。方法的第一個參數(shù)是預(yù)期的結(jié)果,第二個參數(shù)是實(shí)際的結(jié)果。

          測試用例

          我們在測試用例FizzBuzzTest類中調(diào)用test_one_should_return_one()方法。測試用例是測試程序特定部分的實(shí)際測試代碼。

          第一個測試用例驗(yàn)證數(shù)字1是否通過了FizzBuzz過濾器,它將返回字符串' 1 '。使用self驗(yàn)證結(jié)果。assertEqual方法。方法的第一個參數(shù)是預(yù)期的結(jié)果,第二個參數(shù)是實(shí)際的結(jié)果。

          如果您查看這兩個測試用例,您會看到它們都創(chuàng)建了FizzBuzz類的一個實(shí)例。第一個在第6行,另一個在第11行。

          我們可以從這兩個方法中重構(gòu)FizzBuzz實(shí)例的創(chuàng)建,從而改進(jìn)代碼。

          import unittest
          class FizzBuzzTest(unittest.TestCase):
              def setUp(self):
                  self.fizzbuzz=FizzBuzz()
          
          
              def tearDown(self):
                  pass
          
          
              def test_one_should_return_one(self):
                  result=self.fizzbuzz.filter(1)
                  self.assertEqual('1', result)
          
          
              def test_two_should_return_two(self):
                  result=self.fizzbuzz.filter(2)
                  self.assertEqual('2', result)

          我們使用setUp方法創(chuàng)建FizzBuzz類的實(shí)例。TestCase基類的設(shè)置在每個測試用例之前執(zhí)行。

          另一個方法tearDown是在每個單元測試執(zhí)行之后調(diào)用的。你可以用它來清理或關(guān)閉資源。

          測試夾具

          方法的設(shè)置和拆卸是測試夾具的一部分。測試夾具用于配置和構(gòu)建被測試單元。每個測試用例都可以使用這些通用條件。在本例中,我使用它創(chuàng)建FizzBuzz類的實(shí)例。

          要運(yùn)行單元測試,我們需要一個測試運(yùn)行器。

          測試運(yùn)行器

          測試運(yùn)行程序是執(zhí)行所有單元測試并報告結(jié)果的程序。Python的標(biāo)準(zhǔn)測試運(yùn)行器可以使用以下命令在終端上運(yùn)行。

          python -m unittest test_fizzbuzz.py


          測試套件

          單元測試詞匯表的最后一個術(shù)語是測試套件。測試套件是測試用例或測試套件的集合。通常一個測試套件包含應(yīng)該一起運(yùn)行的測試用例。

          單元測試設(shè)計

          測試用例應(yīng)該被很好地設(shè)計。考試的名稱和結(jié)構(gòu)是最重要的。

          測試用例名稱

          測試的名稱非常重要。它就像一個總結(jié)考試內(nèi)容的標(biāo)題。如果測試失敗,你首先看到的就是它。因此,名稱應(yīng)該清楚地表明哪些功能不起作用。

          測試用例名稱的列表應(yīng)該讀起來像摘要或場景列表。這有助于讀者理解被測單元的行為。


          構(gòu)造測試用例方法體

          一個設(shè)計良好的測試用例由三部分組成。第一部分,安排、設(shè)置要測試的對象。第二部分,Act,練習(xí)被測單元。最后,第三部分,斷言,對應(yīng)該發(fā)生的事情提出主張。

          有時,我在單元測試中添加這三個部分作為注釋,以使其更清楚。

          import unittest
          
          class FizzBuzzTest(unittest.TestCase):
          
              def test_one_should_return_one(self):
                  # Arrange
                  fizzbuzz=FizzBuzz()
                  # Act
                  result=fizzbuzz.filter(1)
                  # Assert
                  self.assertEqual('1', result)


          每個測試用例的單個斷言

          盡管在一個測試用例中可能有很多斷言。我總是嘗試使用單個斷言。

          原因是,當(dāng)斷言失敗時,測試用例的執(zhí)行就會停止。因此,您永遠(yuǎn)不會知道測試用例中的下一個斷言是否成功。

          使用pytest進(jìn)行單元測試

          在上一節(jié)中,我們使用了unittest模塊。Python的默認(rèn)安裝安裝這個模塊。unittest模塊于2001年首次引入。基于Kent Beck和Eric Gamma開發(fā)的流行的Java單元測試框架JUnit。

          另一個模塊pytest是目前最流行的Python單元測試框架。與unittest框架相比,它更具有python風(fēng)格。您可以將測試用例定義為函數(shù),而不是從基類派生。

          因?yàn)閜ytest不在默認(rèn)的Python安裝中,所以我們使用Python的包安裝程序PIP來安裝它。通過在終端中執(zhí)行以下命令,可以安裝pytest。

          pip install pytest

          下面我將第一個FizzBuzz測試用例轉(zhuǎn)換為pytest。

          def test_one_should_return_one():
              fizzbuzz=FizzBuzz()
              result=fizzbuzz.filter(1)
              assert '1'==result

          有三個不同點(diǎn)。首先,您不需要導(dǎo)入任何模塊。其次,您不需要實(shí)現(xiàn)一個類并從基類派生。最后,您可以使用標(biāo)準(zhǔn)的Python assert方法來代替自定義的方法。


          測試裝置

          您還記得,單元測試模塊使用setUp和tearDown來配置和構(gòu)建測試中的單元。相反,pytest使用@pytest.fixture屬性。在您的測試用例中,您可以使用用該屬性裝飾的方法的名稱作為參數(shù)。

          pytest框架在運(yùn)行時將它們連接起來,并將fizzBuzz實(shí)例注入測試用例中。

          @pytest.fixture
          def fizzBuzz():
              return FizzBuzz()
          
          
          def test_one_should_return_one(fizzBuzz):
              result=fizzBuzz.filter(1)
              assert result=='1'
          
          
          def test_two_should_return_two(fizzBuzz):
              result=fizzBuzz.filter(2)
              assert result=='2'

          如果您想要模擬單元測試tearDown()方法的行為,可以使用相同的方法來實(shí)現(xiàn)。不使用return,而是使用yield關(guān)鍵字。然后,您可以將清理代碼放在yield之后。

          @pytest.fixture
          def fizzBuzz():
              yield FizzBuzz()
              # put your clean up code here


          pytest標(biāo)記

          標(biāo)記是可以在測試各種函數(shù)時使用的屬性。例如,如果您將跳過標(biāo)記添加到您的測試用例中,測試運(yùn)行器將跳過測試。

          @pytest.mark.skip(reason="WIP")
          def test_three_should_return_fizz(fizzBuzz):
              result=fizzBuzz.filter(3)
              assert result=='Fizz'


          pytest插件生態(tài)系統(tǒng)

          pytest有很多插件可以添加額外的功能。到我寫這篇文章的時候,已經(jīng)有將近900個插件了。例如,pytest-html和pytest-sugar。

          pytest-html

          pytest- HTML是pytest的插件,它為測試結(jié)果生成HTML報告。當(dāng)您在構(gòu)建服務(wù)器上運(yùn)行單元測試時,這非常有用。

          pytest-sugar

          pytest-sugar改變pytest的默認(rèn)外觀和感覺。它會添加一個進(jìn)度條,并立即顯示失敗的測試。

          創(chuàng)建代碼覆蓋率報告

          有一些工具可以創(chuàng)建代碼覆蓋率報告。這個代碼覆蓋率報告顯示了您的單元測試執(zhí)行了哪些代碼。

          我使用Coverage和pytest-cov來創(chuàng)建代碼覆蓋率報告。覆蓋率是度量代碼覆蓋率的通用包。模塊pytest-cov是pytest的一個插件,用于連接到Coverage。

          都可以使用pip安裝。

          pip install coverage

          pip install pytest-cov

          在您安裝了這兩個命令之后,您可以使用這兩個命令生成覆蓋率報告。在終端或命令中運(yùn)行它們。

          coverage run -m pytest

          coverage html

          第一個生成覆蓋率數(shù)據(jù)。第二個命令將數(shù)據(jù)轉(zhuǎn)換為HTML報告。Coverage將報告存儲在文件系統(tǒng)的htmlcov文件夾中。

          如果你在瀏覽器中打開index.html,它會顯示每個文件覆蓋率的概覽。

          如果您選擇一個文件,它將顯示下面的屏幕。覆蓋率向源代碼添加了一個指示,顯示單元測試覆蓋了哪一行。

          下面我們看到我們的單元測試并沒有涵蓋第12行和第16行。

          分支覆蓋度量

          覆蓋率還支持分支覆蓋率度量。有了分支覆蓋率,如果您的程序中有一行可以跳轉(zhuǎn)到下一行以上,覆蓋率跟蹤是否訪問了這些目的地。

          您可以通過執(zhí)行以下命令來創(chuàng)建帶有分支覆蓋率的覆蓋率報告。

          pytest——cov-report html:htmlcov——cov-branch——cov=alarm

          我指示pytest生成一個帶有分支覆蓋的HTML覆蓋報告。它應(yīng)該將結(jié)果存儲在htmlcov中。而不是為所有文件生成覆蓋率報告,我告訴覆蓋率只使用alarm.py。


          主站蜘蛛池模板: 日本韩国黄色一区二区三区| 亚洲sm另类一区二区三区| 在线精品动漫一区二区无广告| 国产精品熟女视频一区二区| 日本福利一区二区| 青娱乐国产官网极品一区| 国模无码人体一区二区| 亚洲爆乳精品无码一区二区| 国产日韩一区二区三免费高清| 国产一区二区电影| 无码中文人妻在线一区| 精品久久一区二区三区| 天堂国产一区二区三区| 在线观看国产一区亚洲bd| 国产av熟女一区二区三区| 激情亚洲一区国产精品| 久久中文字幕一区二区| 国产在线无码视频一区二区三区 | 国产成人无码一区二区三区| 国产精品一区电影| 午夜视频在线观看一区| 文中字幕一区二区三区视频播放| 日韩视频免费一区二区三区| 精彩视频一区二区| 无码av免费一区二区三区试看 | 精品国产日产一区二区三区 | 无码人妻精品一区二区在线视频| 国产伦精品一区二区三区精品| 中文字幕乱码一区久久麻豆樱花| 午夜精品一区二区三区免费视频| 奇米精品一区二区三区在| 一区二区不卡久久精品| 亚洲一区二区三区在线观看精品中文| 美女视频免费看一区二区| 亚洲高清成人一区二区三区| 国产一区二区三区高清视频| 国产精品区一区二区三在线播放| 日韩人妻无码一区二区三区久久99 | 日韩国产一区二区| 日本一区二区不卡视频| 成人区人妻精品一区二区不卡|