簡體   English   中英

分析代碼的哪些部分要編寫單元測試?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM