国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

設(shè)計模式之美——單元測試和代碼可測性

這篇具有很好參考價值的文章主要介紹了設(shè)計模式之美——單元測試和代碼可測性。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

最可落地執(zhí)行、最有效的保證重構(gòu)不出錯的手段應(yīng)該就是單元測試(Unit Testing)。

什么是單元測試?

單元測試由研發(fā)工程師自己來編寫,用來測試自己寫的代碼的正確性。我們常常將它跟集成測試放到一塊來對比。單元測試相對于集成測試(Integration Testing)來說,測試的粒度更小一些。集成測試的測試對象是整個系統(tǒng)或者某個功能模塊,比如測試用戶注冊、登錄功能是否正常,是一種端到端(end to end)的測試。而單元測試的測試對象是類或者函數(shù),用來測試一個類和函數(shù)是否都按照預(yù)期的邏輯執(zhí)行。這是代碼層級的測試。

單元測試除了能有效地為重構(gòu)保駕護航之外,也是保證代碼質(zhì)量最有效的兩個手段之一(另一個是 Code Review)。

寫單元測試就是針對代碼設(shè)計覆蓋各種輸入、異常、邊界條件的測試用例,并將這些測試用例翻譯成代碼的過程。

在把測試用例翻譯成代碼的時候,我們可以利用單元測試框架,來簡化測試代碼的編寫。比如,Java 中比較出名的單元測試框架有 Junit、TestNG、Spring Test 等。這些框架提供了通用的執(zhí)行流程(比如執(zhí)行測試用例的 TestCaseRunner)和工具類庫(比如各種 Assert 判斷函數(shù))等。借助它們,我們在編寫測試代碼的時候,只需要關(guān)注測試用例本身的編寫即可。設(shè)計模式之美代碼,設(shè)計模式,單元測試,設(shè)計模式,junit
設(shè)計模式之美代碼,設(shè)計模式,單元測試,設(shè)計模式,junit

編寫可測試代碼案例實戰(zhàn)

其中,Transaction 是經(jīng)過我抽象簡化之后的一個電商系統(tǒng)的交易類,用來記錄每筆訂單交易的情況。Transaction 類中的 execute() 函數(shù)負責(zé)執(zhí)行轉(zhuǎn)賬操作,將錢從買家的錢包轉(zhuǎn)到賣家的錢包中。真正的轉(zhuǎn)賬操作是通過調(diào)用 WalletRpcService RPC 服務(wù)來完成的。除此之外,代碼中還涉及一個分布式鎖 DistributedLock 單例類,用來避免 Transaction 并發(fā)執(zhí)行,導(dǎo)致用戶的錢被重復(fù)轉(zhuǎn)出。


public class Transaction {
  private String id;
  private Long buyerId;
  private Long sellerId;
  private Long productId;
  private String orderId;
  private Long createTimestamp;
  private Double amount;
  private STATUS status;
  private String walletTransactionId;
  
  // ...get() methods...
  
  public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
    if (preAssignedId != null && !preAssignedId.isEmpty()) {
      this.id = preAssignedId;
    } else {
      this.id = IdGenerator.generateTransactionId();
    }
    if (!this.id.startWith("t_")) {
      this.id = "t_" + preAssignedId;
    }
    this.buyerId = buyerId;
    this.sellerId = sellerId;
    this.productId = productId;
    this.orderId = orderId;
    this.status = STATUS.TO_BE_EXECUTD;
    this.createTimestamp = System.currentTimestamp();
  }
  
  public boolean execute() throws InvalidTransactionException {
    if ((buyerId == null || (sellerId == null || amount < 0.0) {
      throw new InvalidTransactionException(...);
    }
    if (status == STATUS.EXECUTED) return true;
    boolean isLocked = false;
    try {
      isLocked = RedisDistributedLock.getSingletonIntance().lockTransction(id);
      if (!isLocked) {
        return false; // 鎖定未成功,返回false,job兜底執(zhí)行
      }
      if (status == STATUS.EXECUTED) return true; // double check
      long executionInvokedTimestamp = System.currentTimestamp();
      if (executionInvokedTimestamp - createdTimestap > 14days) {
        this.status = STATUS.EXPIRED;
        return false;
      }
      WalletRpcService walletRpcService = new WalletRpcService();
      String walletTransactionId = walletRpcService.moveMoney(id, buyerId, sellerId, amount);
      if (walletTransactionId != null) {
        this.walletTransactionId = walletTransactionId;
        this.status = STATUS.EXECUTED;
        return true;
      } else {
        this.status = STATUS.FAILED;
        return false;
      }
    } finally {
      if (isLocked) {
       RedisDistributedLock.getSingletonIntance().unlockTransction(id);
      }
    }
  }
}

設(shè)計模式之美代碼,設(shè)計模式,單元測試,設(shè)計模式,junit
測試用例 1 的代碼實現(xiàn):


public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
}

設(shè)計模式之美代碼,設(shè)計模式,單元測試,設(shè)計模式,junit
我們通過繼承 WalletRpcService 類,并且重寫其中的 moveMoney() 函數(shù)的方式來實現(xiàn) mock。具體的代碼實現(xiàn)如下所示。通過 mock 的方式,我們可以讓 moveMoney() 返回任意我們想要的數(shù)據(jù),完全在我們的控制范圍內(nèi),并且不需要真正進行網(wǎng)絡(luò)通信。


public class MockWalletRpcServiceOne extends WalletRpcService {
  public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) {
    return "123bac";
  } 
}

public class MockWalletRpcServiceTwo extends WalletRpcService {
  public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) {
    return null;
  } 
}

現(xiàn)在我們再來看,如何用 MockWalletRpcServiceOne、MockWalletRpcServiceTwo 來替換代碼中的真正的 WalletRpcService 呢?因為 WalletRpcService 是在 execute() 函數(shù)中通過 new 的方式創(chuàng)建的,我們無法動態(tài)地對其進行替換。也就是說,Transaction 類中的 execute() 方法的可測試性很差,需要通過重構(gòu)來讓其變得更容易測試。

該如何重構(gòu)這段代碼呢?在依賴反轉(zhuǎn)中,我們講到,依賴注入是實現(xiàn)代碼可測試性的最有效的手段。我們可以應(yīng)用依賴注入,將 WalletRpcService 對象的創(chuàng)建反轉(zhuǎn)給上層邏輯,在外部創(chuàng)建好之后,再注入到 Transaction 類中。

依賴注入是實現(xiàn)代碼可測試性的最有效的手段:通過DI實現(xiàn)反轉(zhuǎn),將對象的創(chuàng)建交給業(yè)務(wù)調(diào)用方,這樣就可以隨意控制輸出的結(jié)果,從而達到"mock"數(shù)據(jù)的目的,這樣的思路太贊了。。。(補充下:不存在外部依賴的類對象可以直接通過new來創(chuàng)建)

重構(gòu)之后的 Transaction 類的代碼如下所示:


public class Transaction {
  //...
  // 添加一個成員變量及其set方法
  private WalletRpcService walletRpcService;
  
  public void setWalletRpcService(WalletRpcService walletRpcService) {
    this.walletRpcService = walletRpcService;
  }
  // ...
  public boolean execute() {
    // ...
    // 刪除下面這一行代碼
    // WalletRpcService walletRpcService = new WalletRpcService();
    // ...
  }
}

現(xiàn)在,我們就可以在單元測試中,非常容易地將 WalletRpcService 替換成 MockWalletRpcServiceOne 或 WalletRpcServiceTwo 了。重構(gòu)之后的代碼對應(yīng)的單元測試如下所示:


public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  // 使用mock對象來替代真正的RPC服務(wù)
  transaction.setWalletRpcService(new MockWalletRpcServiceOne()):
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
  assertEquals(STATUS.EXECUTED, transaction.getStatus());
}

WalletRpcService 的 mock 和替換問題解決了,我們再來看 RedisDistributedLock。它的 mock 和替換要復(fù)雜一些,主要是因為 RedisDistributedLock 是一個單例類。單例相當于一個全局變量,我們無法 mock(無法繼承和重寫方法),也無法通過依賴注入的方式來替換。

如果 RedisDistributedLock 是我們自己維護的,可以自由修改、重構(gòu),那我們可以將其改為非單例的模式,或者定義一個接口,比如 IDistributedLock,讓 RedisDistributedLock 實現(xiàn)這個接口。這樣我們就可以像前面 WalletRpcService 的替換方式那樣,替換 RedisDistributedLock 為 MockRedisDistributedLock 了。但如果 RedisDistributedLock 不是我們維護的,我們無權(quán)去修改這部分代碼,這個時候該怎么辦呢?

我們可以對 transaction 上鎖這部分邏輯重新封裝一下。具體代碼實現(xiàn)如下所示:


public class TransactionLock {
  public boolean lock(String id) {
    return RedisDistributedLock.getSingletonIntance().lockTransction(id);
  }
  
  public void unlock() {
    RedisDistributedLock.getSingletonIntance().unlockTransction(id);
  }
}

public class Transaction {
  //...
  private TransactionLock lock;
  
  public void setTransactionLock(TransactionLock lock) {
    this.lock = lock;
  }
 
  public boolean execute() {
    //...
    try {
      isLocked = lock.lock();
      //...
    } finally {
      if (isLocked) {
        lock.unlock();
      }
    }
    //...
  }
}

這樣,我們就能在單元測試代碼中隔離真正的 RedisDistributedLock 分布式鎖這部分邏輯了。


public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  
  TransactionLock mockLock = new TransactionLock() {
    public boolean lock(String id) {
      return true;
    }
  
    public void unlock() {}
  };
  
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  transaction.setWalletRpcService(new MockWalletRpcServiceOne());
  transaction.setTransactionLock(mockLock);
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
  assertEquals(STATUS.EXECUTED, transaction.getStatus());
}

現(xiàn)在,我們再來看測試用例 3:交易已過期(createTimestamp 超過 14 天),交易狀態(tài)設(shè)置為 EXPIRED,返回 false。


public void testExecute_with_TransactionIsExpired() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  transaction.setCreatedTimestamp(System.currentTimestamp() - 14days);
  boolean actualResult = transaction.execute();
  assertFalse(actualResult);
  assertEquals(STATUS.EXPIRED, transaction.getStatus());
}

上面的代碼看似沒有任何問題。我們將 transaction 的創(chuàng)建時間 createdTimestamp 設(shè)置為 14 天前,也就是說,當單元測試代碼運行的時候,transaction 一定是處于過期狀態(tài)。但是,如果在 Transaction 類中,并沒有暴露修改 createdTimestamp 成員變量的 set 方法(也就是沒有定義 setCreatedTimestamp() 函數(shù))呢?

你可能會說,如果沒有 createTimestamp 的 set 方法,我就重新添加一個唄!實際上,這違反了類的封裝特性。在 Transaction 類的設(shè)計中,createTimestamp 是在交易生成時(也就是構(gòu)造函數(shù)中)自動獲取的系統(tǒng)時間,本來就不應(yīng)該人為地輕易修改,所以,暴露 createTimestamp 的 set 方法,雖然帶來了靈活性,但也帶來了不可控性。因為,我們無法控制使用者是否會調(diào)用 set 方法重設(shè)createTimestamp,而重設(shè) createTimestamp 并非我們的預(yù)期行為。

那如果沒有針對 createTimestamp 的 set 方法,那測試用例 3 又該如何實現(xiàn)呢?實際上,這是一類比較常見的問題,就是代碼中包含跟“時間”有關(guān)的“未決行為”邏輯。我們一般的處理方式是將這種未決行為邏輯重新封裝。針對 Transaction 類,我們只需要將交易是否過期的邏輯,封裝到 isExpired() 函數(shù)中即可,具體的代碼實現(xiàn)如下所示:


public class Transaction {

  protected boolean isExpired() {
    long executionInvokedTimestamp = System.currentTimestamp();
    return executionInvokedTimestamp - createdTimestamp > 14days;
  }
  
  public boolean execute() throws InvalidTransactionException {
    //...
      if (isExpired()) {
        this.status = STATUS.EXPIRED;
        return false;
      }
    //...
  }
}

針對重構(gòu)之后的代碼,測試用例 3 的代碼實現(xiàn)如下所示:


public void testExecute_with_TransactionIsExpired() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId) {
    protected boolean isExpired() {
      return true;
    }
  };
  boolean actualResult = transaction.execute();
  assertFalse(actualResult);
  assertEquals(STATUS.EXPIRED, transaction.getStatus());
}

通過重構(gòu),Transaction 代碼的可測試性提高了。之前羅列的所有測試用例,現(xiàn)在我們都順利實現(xiàn)了。不過,Transaction 類的構(gòu)造函數(shù)的設(shè)計還有點不妥。


  public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
    if (preAssignedId != null && !preAssignedId.isEmpty()) {
      this.id = preAssignedId;
    } else {
      this.id = IdGenerator.generateTransactionId();
    }
    if (!this.id.startWith("t_")) {
      this.id = "t_" + preAssignedId;
    }
    this.buyerId = buyerId;
    this.sellerId = sellerId;
    this.productId = productId;
    this.orderId = orderId;
    this.status = STATUS.TO_BE_EXECUTD;
    this.createTimestamp = System.currentTimestamp();
  }

我們發(fā)現(xiàn),構(gòu)造函數(shù)中并非只包含簡單賦值操作。交易 id 的賦值邏輯稍微復(fù)雜。我們最好也要測試一下,以保證這部分邏輯的正確性。為了方便測試,我們可以把 id 賦值這部分邏輯單獨抽象到一個函數(shù)中,具體的代碼實現(xiàn)如下所示:


  public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
    //...
    fillTransactionId(preAssignId);
    //...
  }
  
  protected void fillTransactionId(String preAssignedId) {
    if (preAssignedId != null && !preAssignedId.isEmpty()) {
      this.id = preAssignedId;
    } else {
      this.id = IdGenerator.generateTransactionId();
    }
    if (!this.id.startWith("t_")) {
      this.id = "t_" + preAssignedId;
    }
  }

Transaction 類中 isExpired() 函數(shù)就不用測試了嗎?對于 isExpired() 函數(shù),邏輯非常簡單,肉眼就能判定是否有 bug,是可以不用寫單元測試的。

其他常見的 Anti-Patterns

上面通過一個實戰(zhàn)案例,講解了如何利用依賴注入來提高代碼的可測試性,以及編寫單元測試中最復(fù)雜的一部分內(nèi)容:如何通過 mock、二次封裝等方式解依賴外部服務(wù)?,F(xiàn)在,我們再來總結(jié)一下,有哪些典型的、常見的測試性不好的代碼,也就是我們常說的 Anti-Patterns。

1. 未決行為

代碼的輸出是隨機或者說不確定的,比如,跟時間、隨機數(shù)有關(guān)的代碼。

2. 全局變量

全局變量是一種面向過程的編程風(fēng)格,有種種弊端。實際上,濫用全局變量也讓編寫單元測試變得困難。

RangeLimiter 表示一個[-5, 5]的區(qū)間,position 初始在 0 位置,move() 函數(shù)負責(zé)移動 position。其中,position 是一個靜態(tài)全局變量。RangeLimiterTest 類是為其設(shè)計的單元測試。


public class RangeLimiter {
  private static AtomicInteger position = new AtomicInteger(0);
  public static final int MAX_LIMIT = 5;
  public static final int MIN_LIMIT = -5;

  public boolean move(int delta) {
    int currentPos = position.addAndGet(delta);
    boolean betweenRange = (currentPos <= MAX_LIMIT) && (currentPos >= MIN_LIMIT);
    return betweenRange;
  }
}

public class RangeLimiterTest {
  public void testMove_betweenRange() {
    RangeLimiter rangeLimiter = new RangeLimiter();
    assertTrue(rangeLimiter.move(1));
    assertTrue(rangeLimiter.move(3));
    assertTrue(rangeLimiter.move(-5));
  }

  public void testMove_exceedRange() {
    RangeLimiter rangeLimiter = new RangeLimiter();
    assertFalse(rangeLimiter.move(6));
  }
}

上面的單元測試有可能會運行失敗。假設(shè)單元測試框架順序依次執(zhí)行 testMove_betweenRange() 和 testMove_exceedRange() 兩個測試用例。在第一個測試用例執(zhí)行完成之后,position 的值變成了 -1;再執(zhí)行第二個測試用例的時候,position 變成了 5,move() 函數(shù)返回 true,assertFalse 語句判定失敗。所以,第二個測試用例運行失敗。

當然,如果 RangeLimiter 類有暴露重設(shè)(reset)position 值的函數(shù),我們可以在每次執(zhí)行單元測試用例之前,把 position 重設(shè)為 0,這樣就能解決剛剛的問題。

不過,每個單元測試框架執(zhí)行單元測試用例的方式可能是不同的。有的是順序執(zhí)行,有的是并發(fā)執(zhí)行。對于并發(fā)執(zhí)行的情況,即便我們每次都把 position 重設(shè)為 0,也并不奏效。如果兩個測試用例并發(fā)執(zhí)行,第 16、17、18、23 這四行代碼可能會交叉執(zhí)行,影響到 move() 函數(shù)的執(zhí)行結(jié)果。設(shè)計模式之美代碼,設(shè)計模式,單元測試,設(shè)計模式,junit文章來源地址http://www.zghlxwxcb.cn/news/detail-530754.html

到了這里,關(guān)于設(shè)計模式之美——單元測試和代碼可測性的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包