iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0
Mobile Development

30 天輕鬆學會 Flutter 測試系列 第 10

Day 10 測試每執行五秒,開發者就少了五秒

  • 分享至 

  • xImage
  •  

在開發的時候,有時候會需要 Scheduler 執行一些定期任務,例如:撈資料到本地端存放、檢查並繼續未完成任務 …等。今天主要討論如何測試 Schedler,這個主題也與時間有點關係,但是跟昨天的主題不太一樣,話不多說,我們直接看一下測試 Scheduler 會碰到什麼問題吧。

假設有個 Scheduler

我們常會用 Scheduler 來執行定期更新資料的任務,像下面這個例子中,我們每五秒會的更新使用者的錢包,讓使用者的錢包常常維持最新的狀態。(P.S. 其實更新資料用 Push 策略是比較效率的做法,而不是使用 Pull 策略,但這邊請先忽略這件事 )

class UpdateWalletScheduler {
  final WalletRepository walletRepository;

  UpdateWalletScheduler(this.walletRepository);

  void start() {
    Timer.periodic(const Duration(seconds: 5), (timer) {
      walletRepository.update();
    });
  }
}

讓我們為這個 Scheduler 寫這個測試,如果之前的所有測試儀樣,準備測試替身,呼叫 SUT 方法,驗證結果。

@GenerateNiceMocks([MockSpec<WalletRepository>()])
main() {
  test("update wallet after 5 seconds", () {
    var mockWalletRepository = MockWalletRepository();

    UpdateWalletScheduler(mockWalletRepository).start();

    verify(mockWalletRepository.update()).called(1);
  });
}

很快地就會發現測試失敗了,mockWalletRepository.update() 並沒有成功被呼叫到。[範例連結]

1.png

檢查一下後會發現,程式執行完 start 後,要等五秒之後才會執行 mockWalletRepository.update(),但是測試卻是在 start 之後就馬上驗證,那當然會驗證失敗。那我們應該怎麼修改測試呢?依照最直覺的方式,那我們就老老實實等五秒吧。

@GenerateNiceMocks([MockSpec<WalletRepository>()])
main() {
  test("update wallet after 5 seconds", () async {
      var mockWalletRepository = MockWalletRepository();

      UpdateWalletScheduler(mockWalletRepository).start();

      await Future.delayed(const Duration(seconds: 5));

      verify(mockWalletRepository.update()).called(1);
  });
}

在執行 start 之後等待五秒再驗證,測試確實成功了。[範例連結]

2.png

但是這個測試執行花費時間很長,開發人員得真的等五秒才會通過,如果我們有很多需要等待一定時間的測試,整體執行時間會變得很長。一旦測試時間花得越久,開發人員就會越來越不願意頻繁執行。

單元測試的特性之一:執行快速

除了之前講到的可重複性之外,單元測試還必須執行快速,為什麼需要執行快速呢?當我們每修改一小段程式碼,我們就可以執行單元測試來確認,確認我們這次修改有沒有弄壞東西,快速執行,快速驗證。當測試錯誤的時候,因為我們只有改一小段程式碼,所以我們可以很快發現哪邊改壞了。執行快速的單元測試,可以提供開發人員即時的回饋,縮短開發回饋循環,可以讓我們每一個修改都更有信心。

想像一下如果測試執行時間很長,我們肯定會懶得頻繁執行,想改多一點程式碼後,再來一次執行,結果測試錯了還要回頭找到底是哪裡改壞了,最初是想節省時間,最後反倒是花更多時間。還記得 UserRepository 在測試中直接呼叫遠端 Server 的例子嗎?如果遠端伺服器正在忙,沒空回應,也會卡著我我們的測試,讓測試時間執行很久。

使用 fake_async 套件

回到我們剛剛的範例,我們應該如何修改呢?與時間流逝有關的測試,我們可以使用 fake_async 套件,這個套件是由官方維護的套件,可以用於 Future、Stream、Timer 等非同步操作,讓我們用它來修改一下原本的測試。

@GenerateNiceMocks([MockSpec<WalletRepository>()])
main() {
  test("update wallet after 5 seconds", () async {
    fakeAsync((async) {
      var mockWalletRepository = MockWalletRepository();

      UpdateWalletScheduler(mockWalletRepository).start();

      async.elapse(const Duration(seconds: 5));

      verify(mockWalletRepository.update()).called(1);
    });
  });
}

而修改方法也很簡單,與 clock 的使用方式有點像,只要把測試包在 fakeAsync 方法中,然後當測試執行 start 之後,呼叫 async.elapse 假裝時間經過 5 秒,最後測試通過得到綠燈。[範例連結]

用套件雖然可以很好的解決我們的問題,那我們有沒有其他方式呢?

讓測試避開框架

讓我們想一下,為什麼這個測試會這麼不好測試?因為我們用到了 Timer 這個框架提供的物件,當我們使用框架或套件的東西時,有可能會變得不好測試,因為這些東西在設計之初可能沒有考慮測試場景。在寫單元測試中,我想要知道的是我們的邏輯是否正確,而不是去測試第三方套件的程式碼邏輯是否正確,在上面使用 fake_async 的測試中,在測試我們自己的邏輯過程中,也同時測試了 Timer 是不是經過五秒之後就會來呼叫我們的方法。

所以我們也可以考慮直接測試任務內容即可。以下面的例子來說,我們可以透過抽取方法的方式,將主要任務抽出成獨立一個方法,並在測試中直接測試這個方法。

class Scheduler {
  final WalletRepository walletRepository;

  Scheduler(this.walletRepository);

  void start() {
    Timer.periodic(
      const Duration(seconds: 5),
      (timer) => execute(),
    );
  }

  void execute() {
    walletRepository.update();
  }
}

修改之後,就像下面這個測試一樣,直接測試 execute 方法的正確性。[範例連結]

@GenerateNiceMocks([MockSpec<WalletRepository>()])
main() {
  test("should update wallet", () {
    var mockWalletRepository = MockWalletRepository();

    UpdateWalletScheduler(mockWalletRepository).execute();

    verify(mockWalletRepository.update()).called(1);
  });
}

讓我們比較一下上面介紹這兩個方法

使用 fake_async

  • 優點:可以完整測試 Scheduler 的行為
  • 缺點:我們要寫的程式碼比較多,如果不熟套件也還要花時間研究

只測主要任務

  • 優點:測試簡潔易懂
  • 缺點:少測試了設定五秒的部分

如何取捨使用 fake_async 模擬時間流逝,或者直接主要任務的部分,則由觀眾朋友的信心而定,未來的文章中應該會再次談論到這個問題,這邊就先不多說。

小結

當我們寫單元測試時,需要注意測試執行是否有過慢的問題,當測試執行太久,就應該思考是否有方法可以加速它,避免測試越跑越慢,使得開發人員不想執行。在時間流逝有關的測試當中,我們可以選擇使用 fake_async 來解決,也可以選擇測試主要任務的部分就好,端看開發者當下的狀況決定。


上一篇
Day 9 如何在 Dart 中輕鬆測試時間
下一篇
Day 11 我們會重構程式碼,那測試呢?
系列文
30 天輕鬆學會 Flutter 測試30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言