在資料密集型應用盛行的今日,高品質測試資料對軟體開發至關重要。手動建立測試資料既費時又難以滿足大規模測試需求。本文介紹根據結構描述的自動化資料生成技術,解決測試資料生成效率與品質問題。此方法能根據結構描述自動生成符合預期格式和型別的測試資料,提升測試可靠性。同時,文章也探討如何從現有資料樣本中提取結構描述,並結合 Faker 函式庫生成更貼近真實場景的測試資料。
自動化測試資料生成:根據結構描述的技術
在軟體開發和測試過程中,產生高品質的測試資料是一項重要的任務。傳統的手動資料建立方法不僅耗時耗力,而且難以滿足大規模和多樣化的測試需求。本篇文章將介紹如何利用結構描述(Schema)驅動的自動化資料生成技術,來提高測試資料的品質和效率。
結構描述的重要性
結構描述是描述資料結構和型別的定義,它在資料驗證和測試資料生成中扮演著至關重要的角色。透過使用結構描述,我們可以確保生成的測試資料符合預期的格式和型別,從而提高測試的可靠性和有效性。
從資料樣本中提取結構描述
在某些情況下,我們可能沒有明確的結構描述可供使用。這時,我們可以從資料樣本中提取結構描述。例如,在Spark中,我們可以使用df.schema來取得DataFrame的結構描述。
def create_schema_and_mock_spark(filename, length=1):
with open(filename) as f:
sample_data = json.load(f)
df = spark.sparkContext.parallelize(sample_data).toDF()
return generate_data(df.schema, length)
內容解密:
這段程式碼展示瞭如何從JSON檔案中載入樣本資料,並建立一個Spark DataFrame。然後,透過df.schema取得DataFrame的結構描述,並將其傳遞給generate_data函式來生成測試資料。
將資料生成對映到結構描述
要實作根據結構描述的資料生成,我們需要將結構描述中的列和資料型別對映到相應的資料生成器(如Faker提供者)。這樣,我們就可以根據結構描述自動生成符合預期格式和型別的測試資料。
DYNAMIC_GENERAL_FIELDS = {
StringType(): lambda: fake.word(),
LongType(): lambda: fake.random.randint(),
ArrayType(StringType()): lambda: [fake.word() for i in range(fake.random.randint(0,5))]
}
DYNAMIC_NAMED_FIELDS = {
("count", LongType()): lambda: fake.random.randint(0, 20),
("description", StringType()): lambda: fake.description_only(),
("user", StringType()): lambda: fake.email(),
}
內容解密:
這段程式碼定義了兩個對映字典:DYNAMIC_GENERAL_FIELDS和DYNAMIC_NAMED_FIELDS。前者根據資料型別對映到相應的資料生成器,而後者則根據列名和資料型別的組合進行對映。這樣,我們就可以根據結構描述中的列資訊生成相應的測試資料。
實作根據結構描述的資料生成
透過結合結構描述和資料生成器,我們可以實作自動化的測試資料生成。下面是一個示例實作:
def get_value(name, datatype):
if name in [t[0] for t in DYNAMIC_NAMED_FIELDS]:
value = DYNAMIC_NAMED_FIELDS[(name, datatype)]()
return value
if datatype in DYNAMIC_GENERAL_FIELDS:
return DYNAMIC_GENERAL_FIELDS.get(datatype)()
raise DataGenerationError(f"No match for {name}, {datatype}")
def generate_data(schema, length=1):
gen_samples = []
for _ in range(length):
gen_samples.append(tuple(map(lambda field: get_value(field.name, field.dataType), schema.fields)))
return spark.createDataFrame(gen_samples, schema)
內容解密:
這段程式碼實作了兩個關鍵函式:get_value和generate_data。get_value函式根據列名和資料型別傳回相應的測試資料值,而generate_data函式則根據結構描述和指定的長度生成測試資料DataFrame。
自動化資料生成與測試的進階應用
在資料工程領域中,確保資料品質與測試的有效性是至關重要的。隨著資料處理流程的日益複雜,如何生成符合預期格式的測試資料成為一大挑戰。本章將探討根據結構描述(schema)的模擬資料生成技術,以及屬性基礎測試(Property-Based Testing)在資料驗證中的應用。
根據結構描述的模擬資料生成
在進行資料處理與分析時,測試資料的品質直接影響到測試結果的準確性。傳統的手動資料生成方式不僅耗時耗力,還難以保證資料的多樣性和覆寫率。為此,我們可以採用根據結構描述的模擬資料生成技術。
實作根據結構描述的資料生成
要實作根據結構描述的資料生成,首先需要定義資料的結構描述。以下是一個範例,展示如何使用結構描述來生成模擬資料:
from pyspark.sql import types as T
# 定義結構描述
schema = T.StructType([
T.StructField("description", T.StringType(), True),
T.StructField("user", T.StringType(), True)
])
# 生成模擬資料
def generate_data(schema):
# 根據結構描述生成模擬資料的邏輯
pass
結合 Faker 函式庫生成模擬資料
在前面的章節中,我們已經介紹瞭如何使用 Faker 函式庫生成模擬資料。現在,我們將結合結構描述和 Faker 來生成更為真實和多樣化的測試資料。
from faker import Faker
fake = Faker()
def generate_data(schema):
data = []
for field in schema.fields:
if field.dataType == T.StringType():
data.append(fake.text())
# 根據不同的資料型別生成對應的模擬資料
return data
內容解密:
- 結構描述定義:首先定義資料的結構描述,包括欄位名稱和資料型別。
- 模擬資料生成:根據結構描述,使用 Faker 函式庫生成符合預期格式的模擬資料。
- 擴充套件性:這種方法具有良好的擴充套件性,可以輕鬆適配不同的資料結構和格式。
處理結構描述變更
在實際應用中,資料結構變更是常見的情況。如何及時捕捉這些變更,並更新測試資料,是確保測試有效性的關鍵。
範例:捕捉結構描述變更
假設 description 欄位從 StringType 變更為 ArrayType(StringType()):
schema = T.StructType([
T.StructField("description", T.ArrayType(T.StringType(), True), True),
T.StructField("user", T.StringType(), True)
])
當結構描述變更時,持續整合(CI)測試應該能夠捕捉到這些變更,並及時報告錯誤。
內容解密:
- 監控結構描述變更:透過 CI 測試監控結構描述的變更。
- 錯誤報告:當結構描述變更導致測試失敗時,及時報告錯誤。
- 更新測試資料:根據變更後的結構描述,更新測試資料以確保測試的有效性。
屬性基礎測試
屬性基礎測試是一種從測試斷言出發,反向生成測試資料的方法。這種方法可以有效地找出程式碼中的邊界情況和潛在錯誤。
使用 Hypothesis 進行屬性基礎測試
Hypothesis 是 Python 中一個強大的屬性基礎測試框架,可以自動生成測試資料並檢查程式碼的正確性。
from hypothesis import given
import hypothesis.strategies as st
@given(st.emails())
def test_is_valid_email(email):
assert is_valid_email(email)
內容解密:
- 定義測試屬性:根據程式碼的邏輯,定義測試屬性和斷言。
- 使用 Hypothesis 生成測試資料:利用 Hypothesis 自動生成多樣化的測試資料。
- 檢查測試結果:根據生成的測試資料,檢查程式碼是否符合預期的行為。
寫好日誌:資料管道測試與成本控制的關鍵
在測試資料管道時,使用合成資料有許多好處,包括降低成本和測試環境的複雜度、啟用 CI 測試、提高團隊速度等。然而,合成資料並非總是最佳選擇,尤其是當資料複雜或變數眾多時。此時,考慮使用讀取副本或快照來減少對分享資源的負載也是一個不錯的方法。
手動資料生成:建立合成資料的基礎
手動資料生成是建立合成資料的起點,即使你計畫使用自動化方法也是如此。手工製作幾行資料來測試邏輯和資料轉換,將幫助你快速迭代以最佳化程式碼,並提供對資料輪廓和測試需求的深入瞭解。這將有助於你在建立自動化資料生成流程時提出好的模型。
程式碼範例:手動生成測試資料
import random
# 手動生成測試資料
def generate_test_data(num_rows):
test_data = []
for _ in range(num_rows):
data = {
'id': random.randint(1, 100),
'name': f'Name {random.randint(1, 100)}',
'age': random.randint(18, 65)
}
test_data.append(data)
return test_data
# 生成10行測試資料
test_data = generate_test_data(10)
for data in test_data:
print(data)
內容解密:
此範例程式碼展示如何手動生成測試資料。generate_test_data 函式接受一個引數 num_rows,表示要生成的資料行數。函式內部使用迴圈生成指定數量的資料,每一行資料包含 id、name 和 age 三個欄位,分別賦予隨機值。最後,函式傳回包含所有測試資料的列表。
自動化資料生成:擴充套件測試資料的利器
當需要大規模的合成資料時,自動化技術可以生成任意數量的行或列的資料,同時透過資料生成器最小化維護負擔。Python 的標準函式庫,如 random 和 strings,提供了建立合成資料模型的工具。此外,像 Faker 這樣的資料生成函式庫提供了大量的現成資料提供者。
程式碼範例:使用Faker生成合成資料
from faker import Faker
fake = Faker()
def generate_synthetic_data(num_rows):
synthetic_data = []
for _ in range(num_rows):
data = {
'name': fake.name(),
'address': fake.address(),
'text': fake.text()
}
synthetic_data.append(data)
return synthetic_data
# 生成10行合成資料
synthetic_data = generate_synthetic_data(10)
for data in synthetic_data:
print(data)
內容解密:
此範例程式碼展示如何使用Faker函式庫生成合成資料。generate_synthetic_data 函式使用Faker例項生成假資料,包括姓名、地址和文字內容。這些資料可用於測試和開發環境中模擬真實資料。
日誌記錄:可觀察性的重要組成部分
日誌記錄是可觀察性的重要組成部分,它能夠提供有關系統狀態和操作的寶貴資訊。良好的日誌記錄實踐能夠幫助除錯問題、觀察執行情況,並將日誌資料匯出到查詢工具中進行進一步分析。
有效日誌記錄的原則
- 適度記錄:記錄足夠但不過多的資訊。
- 避免敏感資訊:絕不記錄敏感資訊,如密碼或個人識別資訊。
- 考慮規模:設計能夠適應大規模環境的日誌記錄策略。
程式碼範例:適度日誌記錄
import logging
# 設定日誌記錄級別
logging.basicConfig(level=logging.INFO)
def process_data(data):
try:
# 處理資料的邏輯
logging.info('Data processed successfully')
except Exception as e:
logging.error(f'Error processing data: {e}')
# 處理資料
process_data({'key': 'value'})
內容解密:
此範例程式碼展示如何進行適度日誌記錄。透過設定日誌記錄級別為INFO,並在適當的位置記錄日誌訊息,能夠幫助開發者瞭解程式的執行狀態。同時,避免記錄敏感資訊以確保安全。
圖表翻譯:日誌資料分析的重要性
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 結構描述驅動自動化測試資料生成
package "軟體測試架構" {
package "測試層級" {
component [單元測試
Unit Test] as unit
component [整合測試
Integration Test] as integration
component [端對端測試
E2E Test] as e2e
}
package "測試類型" {
component [功能測試] as functional
component [效能測試] as performance
component [安全測試] as security
}
package "工具框架" {
component [pytest] as pytest
component [unittest] as unittest
component [Selenium] as selenium
component [JMeter] as jmeter
}
}
unit --> pytest : 撰寫測試
unit --> integration : 組合模組
integration --> e2e : 完整流程
functional --> selenium : UI 自動化
performance --> jmeter : 負載測試
note right of unit
測試金字塔基礎
快速回饋
高覆蓋率
end note
@enduml
圖表翻譯: 此圖表示展示了資料處理流程。首先,系統開始處理資料,接著檢查資料格式是否正確。如果格式正確,則繼續處理資料;如果格式錯誤,則記錄錯誤日誌。不論是成功處理還是記錄錯誤,最終都會完成處理流程。
雲端儲存彈性對成本的影響
在處理具有有限磁碟空間的筆記型電腦或容器時,過度熱衷的日誌記錄可能會導致某種形式的磁碟空間不足錯誤。幸運的是,這種情況下你會收到警告。然而,在使用雲端日誌記錄時,這種情況並不會發生。
在雲端工作時,日誌通常會被轉發到雲端儲存。正如你在第三章所看到的,除非你自己設定限制,否則傳送資料到雲端儲存通常是沒有限制的。有時日誌轉發是自動進行的,例如AWS EMR會自動將日誌轉發到S3。其他時候,你可能明確要求日誌轉發,就像為Google Kubernetes Engine(GKE)設定雲端日誌記錄一樣。雲端日誌記錄是一件好事,它使你能夠在pod或叢集關閉後回溯和檢查日誌。但缺點是,在雲端帳單出來之前,你不太可能意識到產生了大量的日誌資料。
FinOps Foundation在其「Managing Retention in CloudWatch」報告中提到,每月超過1,400美元的費用歸因於不必要的除錯日誌。有趣的是,在雲端帳單中,這些費用被列為特定物件操作的成本,即PutLogEvents。回想第三章,當物件被儲存到雲端儲存時,你會產生多種費用:儲存資料的費用和對物件執行的操作的費用。使用雲端日誌記錄時,除了儲存日誌之外,還可能產生接收日誌的費用,如Google Cloud Logging的價格摘要所示。
另一個值得注意的是,日誌訊息包含了幾百KB的資料。回想前一節關於規模影響的內容,這在數百萬個事件的規模下會變成數百GB。
降低日誌記錄成本
你可以採取多種措施來享受雲端日誌記錄的好處,同時保持成本在可控範圍內,從調整雲端服務設定到使用日誌記錄技術和工具來處理大規模的日誌。
首先,從雲端服務方面開始。當你使用雲端日誌記錄時,通常日誌會被永久儲存,除非你設定了日誌保留期限,例如AWS文章「Altering CloudWatch log retention」中所述的那樣。建立保留期限將導致日誌在特定時間後被刪除,確保你不會保留不再有用的日誌。
與第三章中看到的生命週期策略類別似,保留期限將根據服務和環境的不同而有所不同。例如,保留生產日誌一週對於除錯和監控可能是值得的。在測試環境中,你可能會在較小的時間視窗內除錯失敗,因此使用較短的保留期限更具成本效益。
除了保留期限外,你還可以設定哪些日誌要轉發到日誌記錄服務,哪些要忽略。這可以透過專注於感興趣的日誌來進一步減少你的日誌足跡。例如,Google Cloud Logging提供了設定排除和包含過濾器的功能,以達到此目的。
在日誌記錄技術方面,使用不同的日誌級別有助於調整日誌冗餘度。Python日誌記錄檔案包含了何時使用不同日誌級別的概述,具體取決於你要傳達的訊息型別。然後,你可以在不同的環境中選擇日誌記錄級別。在生產環境中,你可能會使用預設的WARNING級別來顯示資訊訊息、警告和錯誤,同時抑制除錯訊息。在開發或測試環境中,你可以將日誌級別設定為DEBUG以取得更多資訊。
對於特別冗餘的模組,請考慮對日誌記錄進行更精細的控制。例如,在具有DEBUG日誌級別的測試環境中,你可以引入一個額外的標記來設定具有冗餘DEBUG日誌記錄的模組的日誌級別。預設情況下,你可以將模組日誌級別標記設定為INFO以減少日誌訊息。如果情況需要額外的日誌記錄,你可以更改級別,而不是不斷處理可能無用的大量日誌訊息。
使用組態可以讓你在不更改程式碼的情況下更改日誌級別。這種方法還可以在不同的環境中使用不同的日誌級別。我曾經使用環境變數來實作這一點,其中DEBUG標記在測試環境中被開啟,但在生產環境中被停用。如果生產環境中出現問題,我們的團隊只需切換一個環境變數就能啟用除錯日誌。
對於Python來說,特別是透過延遲建立日誌訊息,可以減少日誌記錄的效能影響。你可以在Python日誌記錄流程中看到,只有當記錄器為日誌呼叫的日誌級別啟用時,才會建立日誌記錄。如果沒有,日誌記錄就不會被建立。
例如,以下是兩種不同的使用變數記錄訊息的方法,一種使用%格式化,另一種使用f-字串:
# 使用%格式化的例子
logger.debug("This is a debug message with variable %s", variable)
# 使用f-字串的例子
logger.debug(f"This is a debug message with variable {variable}")
內容解密:
上述程式碼展示了兩種不同的日誌記錄方法。第一種方法使用%格式化,第二種方法使用f-字串。使用%格式化的優點在於,只有當日誌級別被啟用時,才會建立日誌訊息,從而避免了不必要的效能開銷。相反,使用f-字串會立即建立字串,無論日誌級別是否啟用,這可能會導致不必要的效能損失。因此,在需要高效能的應用程式中,建議使用%格式化或其他延遲字串建立的方法。