非同步系統的兩種測試方法

有贊技術發表於2018-04-08

網際網路軟體系統一直隨著需求、使用者量上升等等的原因在演進,以求適應更復雜的業務場景,更高的效能要求等等。軟體演進方式各種各樣,系統非同步化即為其中一種。

一般的,對於那些實時性要求不高,但卻計算密集或者需要處理大資料量的耗時較長的任務,或是有較慢 I/O 的任務,選擇非同步化是一個不錯的選擇。在系統層面,像引入訊息中介軟體來解耦系統,將耗時長的任務放在中介軟體後非同步執行。在方法層面,像把耗時較長的任務放到其他執行緒中去非同步執行。

與測試同步系統或方法不同,當我們測試非同步系統(端到端測試、整合測試)或非同步方法的時候(單元測試),由於測試執行緒不會被非同步任務執行緒阻塞而讓測試變得不可控,概率性失敗,以單元測試為例,這樣寫非同步測試是不穩定的:

@Test
public void testAsynchronousMethod() {
    callAsynchronousMethod();
    assertXXX(...);  //非同步任務可能仍未完成,這時assert可能會失敗
}
複製程式碼

###非同步任務的兩種型別:

  • 非同步任務執行後對任務發起方或呼叫方有感知,比如發出一個事件或通知
  • 非同步任務執行後對任務發起方或呼叫方沒有感知,只是改變了系統中的某些狀態

對非同步任務的測試也分以上兩種型別討論。對於第一種,我們可以採用監聽方式測試:

import org.junit.Before;
import org.junit.Test;

public class ExampleTest {
    private final Object lock = new Object();

    @Before
    public void init() {
        new Thread(new Runnable() {
            public void run() {
                synchronized (lock) {  //獲得鎖
                    monitorEvent();    //監聽非同步事件的到來
                    lock.notifyAll();  //事件到達,釋放鎖
                }
            }
        }).start();
    }

    @Test
    public void testAsynchronousMethod() {
        callAsynchronousMethod();  //呼叫非同步方法,需要較長一段時間才能執行完,並觸發事件通知

        /**
         * 事件未到達時由於init已經獲得了鎖而阻塞,事件到達後因init中的鎖釋放而獲得鎖,
         * 此時非同步任務已執行完成,可以放心的執行斷言驗證結果了
         */
        synchronized (lock) {
            assertTestResult();
        }
    }
}
複製程式碼

這裡的前提是事件通知會到來並被監聽到,可要是不來呢(比如異常任務執行失敗了)?我們就乾等嗎,其實我們還可以在測試中引入超時機制,這也引出了第二種型別的異常測試(可以稱之為輪詢方式),假設我們有如下一個非同步系統,應用發訊息到 NSQ 訊息中介軟體,一個待測試的 Job 監聽這個訊息並在訊息到達後處理訊息:

非同步系統的兩種測試方法

那我們怎麼測試呢,站在端到端測試的角度,可以測試從應用到 Job 的鏈路,訊息是應用直接構造的 NSQ 訊息,也可以是 Mysql binlog 經轉化後構造的 NSQ 訊息;站在整合測試的角度,我們可以縮小測試範圍,直接在測試中構造 NSQ 訊息,測試從訊息中介軟體到 Job 的鏈路。長鏈路測試耗時長,且寫測試前需要了解具體應用的訊息觸發邏輯,寫測試也比較慢,無形中增加了很多測試成本。所以對於這樣的系統,我們可以採用整合測試方法來測。

@Test
public void testAsynchronousJob() throws Exception {
    String msg = buildNsqMsg();    //構造NSQ訊息
    nsqClient.send(TOPIC, msg, false);	//傳送Nsq訊息

    with().pollInterval(ONE_HUNDRED_MILLISECONDS).  //100ms後開始檢查
            and().with().pollDelay(10, MILLISECONDS).  //此後每隔10ms檢查一次
            await("description").  //描述資訊
            atMost(1L, SECONDS).   //1s超時時間
            until(() -> xxxService.getState() == "changed");  //業務相關的斷言邏輯
}
複製程式碼

上述測試我們引入了 awaitility 工具類來做輪詢操作,一個靠譜的輪詢至少包含以下特性:

  • 超時機制,不可能一直輪詢
  • 首次延遲輪詢
  • 輪詢頻率

最後,我們來討論下測試結果可靠性問題。

假設一個非同步系統採用輪詢方式測試,觸發非同步任務後,當在兩次輪詢中間系統狀態因為某些原因出現了抖動,下一次輪詢時輪詢方式可能會誤以為非同步操作還未完成或出現了異常,從而導致測試結果誤判:

非同步系統的兩種測試方法

相對的,監聽方式是不存在這樣的問題的,只要系統狀態改變,監聽中的測試能立馬感知到,並作出可靠的測試結果:

非同步系統的兩種測試方法

很多非同步系統對外是沒有回撥的,這時候只能使用輪詢方式測試非同步任務,而輪詢測試的可靠性取決於待測系統的可靠性。

可是,一個週期性執行的可靠系統同樣會遇到上述問題,測試會因為程式碼週期性執行,系統狀態週期性改變而變得不可靠。對於這樣的系統,我們可以做一些可測性改造。將業務邏輯和週期執行邏輯剝離,並增加一個可以呼叫業務邏輯的入口,比如一個 restful 介面,這樣測試時可以準確控制業務邏輯的執行時機和頻率,也就可以可靠的測試了。

有贊已經在一些非同步 Job 中採用上述輪詢方式測試,我們在測試中主要碰到了兩類 Job,一類是會和 Elasticsearch 搜尋引擎互動的,由於 Elasticsearch 的重新整理機制(有贊出於效能原因設定為 5 秒重新整理一次資料),輪詢方式因為測試時間太長而很侷限,除非提高 Elasticsearch 的重新整理頻率;另一類則是跟 Mysql、Redis 互動的 Job,這類 Job 的測試可以工作的很好,測試基本可以在 150 毫秒內完成,也就意味著可以像普通測試一樣置入持續整合的構建中了。

非同步系統的兩種測試方法

相關文章