[英]How to unit test abstract classes: extend with stubs?
我想知道如何對抽象類和擴展抽象類的類進行單元測試。
我應該通過擴展抽象類來測試抽象類,剔除抽象方法,然后測試所有具體方法嗎? 然后只測試我覆蓋的方法,並在單元測試中測試擴展我的抽象類的對象的抽象方法?
我是否應該有一個抽象測試用例來測試抽象類的方法,並在我的測試用例中為擴展抽象類的對象擴展這個類?
請注意,我的抽象類有一些具體的方法。
有兩種使用抽象基類的方法。
您正在專門化您的抽象對象,但所有客戶端都將通過其基接口使用派生類。
您正在使用抽象基類來排除設計中對象內的重復,而客戶通過他們自己的接口使用具體實現。!
解決方案 1 - 策略模式
如果您有第一種情況,那么您實際上有一個由派生類正在實現的抽象類中的虛擬方法定義的接口。
您應該考慮使其成為一個真正的接口,將您的抽象類更改為具體的,並在其構造函數中獲取此接口的實例。 然后您的派生類成為這個新接口的實現。
這意味着您現在可以使用新接口的模擬實例以及通過現在公共接口的每個新實現來測試您以前的抽象類。 一切都很簡單且可測試。
解決方案 2
如果您有第二種情況,那么您的抽象類就是作為輔助類工作的。
看看它包含的功能。 查看是否可以將其中的任何內容推送到正在操作的對象上,以最大程度地減少這種重復。 如果您還剩下任何東西,請考慮使其成為您的具體實現在其構造函數中接受並刪除其基類的輔助類。
這再次導致了簡單且易於測試的具體類。
作為一項規則
比復雜對象的簡單網絡更喜歡簡單對象的復雜網絡。
可擴展的可測試代碼的關鍵是小的構建塊和獨立的布線。
更新:如何處理兩者的混合物?
有一個基類可以同時執行這兩個角色......即:它有一個公共接口,並有受保護的輔助方法。 如果是這種情況,那么您可以將輔助方法分解為一個類(方案 2)並將繼承樹轉換為策略模式。
如果你發現你的基類有一些方法是直接實現的,而另一些是虛擬的,那么你仍然可以將繼承樹轉換成策略模式,但我也認為它是職責沒有正確對齊的一個很好的指標,並且可能需要重構。
更新 2:抽象類作為墊腳石 (2014/06/12)
前幾天我遇到了使用抽象的情況,所以我想探索一下原因。
我們的配置文件有一個標准格式。 這個特殊的工具有 3 個都是這種格式的配置文件。 我希望每個設置文件都有一個強類型類,這樣,通過依賴注入,一個類可以詢問它關心的設置。
我通過擁有一個抽象基類來實現這一點,該類知道如何解析設置文件格式和暴露這些相同方法的派生類,但封裝了設置文件的位置。
我可以編寫一個“SettingsFileParser”,將 3 個類包裝起來,然后委托給基類以公開數據訪問方法。 我選擇不這樣做又因為這將導致3派生類與他們更代表團代碼比什么都重要。
但是……隨着代碼的發展和每個設置類的使用者變得更加清晰。 每個設置用戶都會要求一些設置並以某種方式轉換它們(因為設置是文本,他們可能會將它們包裝在將它們轉換為數字等的對象中)。 發生這種情況時,我將開始將此邏輯提取到數據操作方法中,並將它們推回到強類型設置類中。 這將導致每組設置的更高級別的界面,即最終不再意識到它正在處理“設置”。
此時,強類型設置類將不再需要暴露底層“設置”實現的“getter”方法。
那時我不再希望他們的公共接口包含設置訪問器方法; 所以我將改變這個類來封裝一個設置解析器類,而不是從它派生。
因此,Abstract 類是:我目前避免委托代碼的一種方式,也是代碼中的一個標記,以提醒我以后更改設計。 我可能永遠不會得到它,所以它可能會存活一段時間......只有代碼可以說明。
我發現這適用於任何規則……比如“沒有靜態方法”或“沒有私有方法”。 它們在代碼中表明了一種氣味……這很好。 它讓您不斷尋找您錯過的抽象概念……同時讓您繼續為您的客戶提供價值。
我想象這樣的規則定義了一個景觀,可維護的代碼存在於山谷中。 當您添加新行為時,就像雨落在您的代碼上一樣。 最初你把它放在任何地方......然后你重構以允許良好設計的力量推動行為直到它全部結束。
編寫一個 Mock 對象並將它們用於測試。 它們通常非常非常小(從抽象類繼承)而不是更多。然后,在您的單元測試中,您可以調用要測試的抽象方法。
您應該像您擁有的所有其他類一樣測試包含某些邏輯的抽象類。
我對抽象類和接口所做的如下:我編寫了一個測試,它使用具體的對象。 但是測試中沒有設置X類型的變量(X是抽象類)。 這個測試類沒有添加到測試套件中,而是它的子類,它們有一個設置方法,可以將變量設置為 X 的具體實現。這樣我就不會復制測試代碼。 如果需要,未使用測試的子類可以添加更多測試方法。
要專門對抽象類進行單元測試,您應該派生它用於測試目的,在繼承時測試 base.method() 結果和預期行為。
你通過調用一個方法來測試它,所以通過實現它來測試一個抽象類......
如果您的抽象類包含具有商業價值的具體功能,那么我通常會通過創建一個測試替身來直接測試它,以提取抽象數據,或者使用模擬框架為我執行此操作。 我選擇哪一個很大程度上取決於我是否需要編寫抽象方法的特定於測試的實現。
我需要這樣做的最常見場景是當我使用模板方法模式時,例如當我構建某種將由 3rd 方使用的可擴展框架時。 在這種情況下,抽象類定義了我要測試的算法,因此測試抽象基比測試特定實現更有意義。
但是,我認為這些測試應該只關注真實業務邏輯的具體實現,這一點很重要; 你不應該對抽象類的實現細節進行單元測試,因為你最終會得到脆弱的測試。
一種方法是編寫一個與您的抽象類相對應的抽象測試用例,然后編寫具體的測試用例來繼承您的抽象測試用例。 對原始抽象類的每個具體子類執行此操作(即,您的測試用例層次結構反映了您的類層次結構)。 請參閱在 junit 食譜書中測試接口: http ://safari.informit.com/9781932394238/ch02lev1sec6。 如果您沒有 safari 帳戶,請訪問 https://www.manning.com/books/junit-recipes或https://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230 。
另請參閱 xUnit 模式中的測試用例超類: http : //xunitpatterns.com/Testcase%20Superclass.html
我會反對“抽象”測試。 我認為測試是一個具體的想法,沒有抽象。 如果你有共同的元素,把它們放在輔助方法或類中供每個人使用。
至於測試抽象測試類,一定要問自己要測試的是什么。 有幾種方法,您應該找出在您的場景中有效的方法。 您是否正在嘗試在您的子類中測試新方法? 然后讓您的測試只與該方法交互。 您是否正在測試基類中的方法? 然后可能只有該類的單獨夾具,並根據需要使用盡可能多的測試單獨測試每個方法。
這是我在設置用於測試抽象類的工具時通常遵循的模式:
public abstract class MyBase{
/*...*/
public abstract void VoidMethod(object param1);
public abstract object MethodWithReturn(object param1);
/*,,,*/
}
我在測試中使用的版本:
public class MyBaseHarness : MyBase{
/*...*/
public Action<object> VoidMethodFunction;
public override void VoidMethod(object param1){
VoidMethodFunction(param1);
}
public Func<object, object> MethodWithReturnFunction;
public override object MethodWithReturn(object param1){
return MethodWihtReturnFunction(param1);
}
/*,,,*/
}
如果抽象方法在我不期望的時候被調用,測試就會失敗。 在安排測試時,我可以很容易地用 lambda 來刪除抽象方法,這些方法執行斷言、拋出異常、返回不同的值等。
如果具體方法調用任何抽象方法,該策略將不起作用,並且您希望分別測試每個子類的行為。 否則,擴展它並存根您所描述的抽象方法應該沒問題,再次提供抽象類的具體方法與子類分離。
我想你可能想要測試一個抽象類的基本功能......但是你最好通過擴展類而不覆蓋任何方法,並對抽象方法進行最小的模擬。
使用抽象類的主要動機之一是在您的應用程序中啟用多態性——即:您可以在運行時替換不同的版本。 事實上,這與使用接口非常相似,只是抽象類提供了一些常見的管道,通常稱為模板模式。
從單元測試的角度來看,有兩件事需要考慮:
抽象類與其相關類的交互。 使用模擬測試框架非常適合這種情況,因為它表明您的抽象類與其他類兼容良好。
派生類的功能。 如果您為派生類編寫了自定義邏輯,則應單獨測試這些類。
編輯:RhinoMocks 是一個很棒的模擬測試框架,它可以通過從您的類動態派生在運行時生成模擬對象。 這種方法可以為您節省無數小時的手工編碼派生類。
首先,如果抽象類包含一些具體的方法,我認為你應該考慮這個例子
public abstract class A
{
public boolean method 1
{
// concrete method which we have to test.
}
}
class B extends class A
{
@override
public boolean method 1
{
// override same method as above.
}
}
class Test_A
{
private static B b; // reference object of the class B
@Before
public void init()
{
b = new B ();
}
@Test
public void Test_method 1
{
b.method 1; // use some assertion statements.
}
}
按照@patrick-desjardins 的回答,我實現了抽象及其實現類以及@Test
,如下所示:
抽象類 - ABC.java
import java.util.ArrayList;
import java.util.List;
public abstract class ABC {
abstract String sayHello();
public List<String> getList() {
final List<String> defaultList = new ArrayList<>();
defaultList.add("abstract class");
return defaultList;
}
}
由於抽象類不能被實例化,但它們可以被子類化,具體類DEF.java ,如下:
public class DEF extends ABC {
@Override
public String sayHello() {
return "Hello!";
}
}
@Test類來測試抽象和非抽象方法:
import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import org.junit.Test;
public class DEFTest {
private DEF def;
@Before
public void setup() {
def = new DEF();
}
@Test
public void add(){
String result = def.sayHello();
assertThat(result, is(equalTo("Hello!")));
}
@Test
public void getList(){
List<String> result = def.getList();
assertThat((Collection<String>) result, is(not(empty())));
assertThat(result, contains("abstract class"));
}
}
如果抽象類適合您的實現,請測試(如上所述)派生的具體類。 你的假設是正確的。
為避免將來混淆,請注意這個具體的測試類不是模擬,而是偽造的.
嚴格來說,模擬由以下特征定義:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.