異步系統的兩種測試方法

互聯網軟件系統一直隨着需求、用戶量上升等等的緣由在演進,以求適應更復雜的業務場景,更高的性能要求等等。軟件演進方式各類各樣,系統異步化即爲其中一種。java

通常的,對於那些實時性要求不高,但卻計算密集或者須要處理大數據量的耗時較長的任務,或是有較慢 I/O 的任務,選擇異步化是一個不錯的選擇。在系統層面,像引入消息中間件來解耦系統,將耗時長的任務放在中間件後異步執行。在方法層面,像把耗時較長的任務放到其餘線程中去異步執行。sql

與測試同步系統或方法不一樣,當咱們測試異步系統(端到端測試、集成測試)或異步方法的時候(單元測試),因爲測試線程不會被異步任務線程阻塞而讓測試變得不可控,機率性失敗,以單元測試爲例,這樣寫異步測試是不穩定的:restful

@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 工具類來作輪詢操做,一個靠譜的輪詢至少包含如下特性:測試

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

最後,咱們來討論下測試結果可靠性問題。大數據

假設一個異步系統採用輪詢方式測試,觸發異步任務後,當在兩次輪詢中間系統狀態由於某些緣由出現了抖動,下一次輪詢時輪詢方式可能會誤覺得異步操做還未完成或出現了異常,從而致使測試結果誤判:ui

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

不少異步系統對外是沒有回調的,這時候只能使用輪詢方式測試異步任務,而輪詢測試的可靠性取決於待測系統的可靠性。

但是,一個週期性執行的可靠系統一樣會遇到上述問題,測試會由於代碼週期性執行,系統狀態週期性改變而變得不可靠。對於這樣的系統,咱們能夠作一些可測性改造。將業務邏輯和週期執行邏輯剝離,並增長一個能夠調用業務邏輯的入口,好比一個 restful 接口,這樣測試時能夠準確控制業務邏輯的執行時機和頻率,也就能夠可靠的測試了。

有贊已經在一些異步 Job 中採用上述輪詢方式測試,咱們在測試中主要碰到了兩類 Job,一類是會和 Elasticsearch 搜索引擎交互的,因爲 Elasticsearch 的刷新機制(有贊出於性能緣由設置爲 5 秒刷新一次數據),輪詢方式由於測試時間太長而很侷限,除非提升 Elasticsearch 的刷新頻率;另外一類則是跟 Mysql、Redis 交互的 Job,這類 Job 的測試能夠工做的很好,測試基本能夠在 150 毫秒內完成,也就意味着能夠像普通測試同樣置入持續集成的構建中了。

相關文章
相關標籤/搜索