[英]What parts of analytical code to write unit tests for?
最后,當我編寫分析型Python代碼時,當用戶通過基於隊列的批處理與前端工具進行交互時,該代碼將按需運行。
通常,用戶在前端工具中設置一些值,這些值作為參數傳遞給分析代碼,他們要么提供數據集,要么從公司提供的整體數據源中選擇數據的子集。
通常,每個分析模型在其他分析模型中都位於較大的存儲庫中,因此每個模型通常都位於其自己的模塊中,並且該模塊將導出一個功能,該功能是該模型的切入點。 這些模型的范圍從數分鍾的簡單模型到非常復雜的基於靜態或機器學習的模型,並可能使用數小時的numpy / Pandas / Numba或Dask數據幀的組合。
現在我的問題是,我一直在重新思考我應該集中在哪種類型的代碼上的測試工作。 在我職業生涯的早期,我天真地認為每個函數都應該有一個單元測試,因此我的代碼將具有一組全面的測試。 我很快意識到這是適得其反的,因為即使很小的性能重構也可能導致撕裂,甚至可能丟棄很多單元測試。 如此明顯地感覺到,我應該只為每個模型的主要公共功能編寫測試,但是,這通常意味着相反的情況,對於某些更復雜的模型,很難深入控制流的邊緣案例去測試。
那么我的問題是我應該如何正確測試這些分析模型? 有人可能會說:“僅測試面向公眾的功能,如果您無法通過面向公眾的功能來測試邊緣案例,那么從技術上講它們應該是不可用的,因此不需要在那里”。 但是,我發現,實際上這並不是很有效。
為了提供一個簡單的例子,假設特定模型是為出租車數據集的起飛/起飛點計算頻率矩陣。
import pandas as pd
def _cat(col1, col2):
cat_col = col1.astype(str).str.cat(col2.astype(str), ', ')
return cat_col
def _make_points_df(taxi_df):
pickup_points = _cat(taxi_df["pickup_longitude"], taxi_df["pickup_latitude"])
dropoff_points = _cat(taxi_df["dropoff_longitude"], taxi_df["dropoff_latitude"])
points_df = pd.DataFrame({"pickup": pickup_points, "dropoff": dropoff_points})
return points_df
def _points_df_to_freq_mat(points_df):
mat_df = points_df.groupby(['pickup', 'dropoff']).size().unstack(fill_value=0)
return mat_df
def _validate_taxi_df(taxi_df):
if type(taxi_df) is not pd.DataFrame:
raise TypeError(f"taxi_df param must be a pandas dataframe, got: {type(taxi_df)}")
expected_cols = {
"pickup_longitude",
"pickup_latitude",
"dropoff_longitude",
"dropoff_latitude",
}
if set(taxi_df) != expected_cols:
raise RuntimeError(
f"Expected the following columns for taxi_df param: {expected_cols}."
f"Got: {set(taxi_df)}"
)
def calculate_frequency_matrix(taxi_df, long_round=1, lat_round=1):
"""Calculate a dropoff/pickup frequency matrix which tells you the number of times
passengers have been picked up and dropped from a given discrete point. The
resolution of these points is controlled by using the long_round and lat_round params
Paramaters
----------
taxi_df : pandas.DataFrame
Dataframe specifying dropoff and pickup long/lat coordinates
long_round : int
Number of decimal places to round the dropoff and pickup longitude values to
lat_round : int
Number of decimal places to round the dropoff and pickup latitude values to
Returns
-------
pandas.DataFrame
Dataframe in matrix format of frequency of dropoff/pickup points
Raises
------
TypeError : If taxi_df is not a pandas DataFrame
RuntimeError : If taxi_df does not contain correct columns
"""
_validate_taxi_df(taxi_df)
taxi_df = taxi_df.copy()
taxi_df["pickup_longitude"] = taxi_df["pickup_longitude"].round(long_round)
taxi_df["dropoff_longitude"] = taxi_df["dropoff_longitude"].round(long_round)
taxi_df["pickup_latitude"] = taxi_df["pickup_latitude"].round(lat_round)
taxi_df["dropoff_latitude"] = taxi_df["dropoff_latitude"].round(lat_round)
points_df = _make_points_df(taxi_df)
mat_df = _points_df_to_freq_mat(points_df)
return mat_df
接受一個像
pickup_longitude pickup_latitude dropoff_longitude dropoff_latitude
0 -73.988129 40.732029 -73.990173 40.756680
1 -73.964203 40.679993 -73.959808 40.655403
2 -73.997437 40.737583 -73.986160 40.729523
3 -73.956070 40.771900 -73.986427 40.730469
4 -73.970215 40.761475 -73.961510 40.755890
5 -73.991302 40.749798 -73.980515 40.786549
6 -73.978310 40.741550 -73.952072 40.717003
7 -74.012711 40.701527 -73.986481 40.719509
就文件夾結構而言,此代碼位於analytics/models/taxi_freq/taxi_freq.py
,而analytics/models/taxi_freq/__init__.py
文件看起來像
from taxi_freq import calculate_frequency_matrix
顯然,以上代碼中的私有功能可以拆分為analytics/models/taxi_freq/
多個實用文件。
共識是只測試calculate_frequency_matrix
函數,還是應該測試taxi_freq
模塊中的“私有”輔助方法和其他實用程序文件/函數?
與一般的軟件開發一樣,在測試中,您始終必須尋找能夠代表相互競爭的目標之間(最理想的)折衷的解決方案。 總體測試以及單元測試的主要目標之一是發現錯誤(請參閱Myers,Badgett,Sandler:軟件測試的藝術,或Beizer:軟件測試技術,以及許多其他方法)。
在您的項目中,您對此可能會比較放松,但是,如果實施級別的錯誤逃到以后的開發階段甚至到現場,則在許多軟件項目中可能會造成嚴重后果。 有人說,您的目標應該是增加對代碼的信心-這也是對的,但是信心只能是正確進行測試的結果。 如果您不進行測試以查找錯誤,那么在您完成測試后,我將對您的代碼完全沒有信心。
當發現錯誤是單元測試的主要目標時,那么試圖使單元測試套件完全獨立於實現細節的嘗試可能會導致效率低下的測試套件-也就是說,不適用於查找所有可能的錯誤的測試套件。找到了。 不同的實現有不同的潛在錯誤。 如果您不使用單元測試來查找這些錯誤,那么任何其他測試級別(集成,子系統,系統)絕對不適合系統地查找它們。
例如,考慮實現Fibonacci函數的不同方法:作為迭代或遞歸函數,作為閉合形式表達式(Moivre / Binet)或作為查找表:接口始終相同,可能的錯誤差異很大,單元測試策略也是如此。 將會有一組有用的獨立於實現的測試用例,但是僅憑這些就不足以找到特定實現可能的所有錯誤。
因此,擁有一個有效的測試套件的目標是與另一個目標競爭,即擁有一個易於維護的測試套件。 但是,此目標以不同的形式出現,並帶來不同的后果:您可以要求在實現細節更改時,單元測試套件不受影響。 這非常困難,並且IMO將維護友好的測試代碼的次要目標置於發現錯誤的主要目標之上。
Meszaros有一個更為平衡的表述,即“更改代碼庫的工作應與維護測試套件的工作相稱。” (請參閱Meszaros: 測試自動化原理:確保相應的努力 )。 也就是說,對SUT的少量更改只需要對測試套件進行很小的更改,對於SUT的較大更改,可以認為測試套件也需要同樣大的更改。 (但是,對我個人而言,“保持測試代碼的工作量應該少”的表述就足夠了。)
結論:
對我來說,因為我將發現錯誤作為主要目標並將將測試套件的可維護性作為次要目標,這導致了以下后果:我同意我還必須測試實現細節才能發現更多的錯誤。 但是,盡管有這個事實,我仍然盡力降低維護工作量:我主要通過應用以下機制來做到這一點,目的是在更改SUT的情況下簡化測試套件的調整:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.