iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0

在上一篇我們簡單的介紹了怎麼測試 Effect 的程式,不過好像跟測試一般的程式沒有太大的差別,但別忘了,我們之前還有提到 DI 可能幫助我們更好的寫測試,那就一起來看 DI 可以怎麼樣的幫助到我們

DI 與 Service 如何幫助我們測試?

還記得之前提到的 DI 與 service 嗎?以我們之前的爬蟲為例,假設我們想要測試我們的 parser function 是否正常運作,但又不希望實際送 request 出去,以免 iThelp 的網頁改變影響到測試結果,那我們可以怎麼做

  1. msw mock 所有的 http request
  2. 用 vitest 的 mock 來 mock fetch 或是 ofetch

但這邊我們還有一個方法,那就是 mock cache service

// 我們要測試的 function
export function getArticleLinks() {
  return pipe(
    $fetchTextWithCache("https://ithelp.ithome.com.tw/articles?tab=tech"),
    Effect.map((html) => parseArticleLinks(html))
  );
}

it.effect("can fetch article list", () =>
  Effect.gen(function* () {
    // 只要像這樣提供一個簡單的實作就可以達到 mock 的效果
    const cache = Cache.make({
     // html 是列表頁的內容
      getItem: () => Effect.succeed(html),
      setItem: () => Effect.void,
    });

    const articles = yield* pipe(
      getArticleLinks(),
      // 提供 service
      Effect.provideService(Cache, cache)
    );

    expect(articles).toMatchSnapshot();
  })
);

像這樣我們就可以簡單的 "mock" service ,另外如果你忘了提供 service ,你也會看到 TypeScript 的 error

這個方法其實有點小問題,因為這已經在讓我們的測試在依賴實作細節,這個細節指的是我們會「cache 抓到的 html」的這件事,如果哪天我們業務邏輯改變而不做 cache 了,那這個方法就會失效,比較好的應該是將 fetch 變成一個 service ,或是將 fetch + cache 變成一個 service ,再去 mock 它,這樣就偏向是在 mock 對外溝通的 service 了

更簡單的 mock service: Layer.mock

另外在 mock service 時還有個方法,那就是使用 Layer.mock ,我們直接看 code 吧

// 會回傳一個 Layer
const mockCache = Layer.mock(Cache, {
  // service 中,所有非 Effect 的東西一定要填
  _tag: Cache.key,
  getItem: () => Effect.succeed(html),
  // 可以省略掉 setItem
});

it.effect("can fetch article list", () =>
  Effect.gen(function* () {
    const articles = yield* pipe(getArticleLinks(), Effect.provide(mockCache));

    expect(articles).toMatchSnapshot();
  })
);

像這樣, Layer.mock 會自動的幫我們把未實作的部份變成使用了就會拋出錯誤的 Effect ,這讓我們可以只實作必要的介面即可

內建 service

我們知道了如何透過將程式對外互動的部份,或是一些共用元件拆出來變成 service 可以更好的讓我們在 Effect 的程式中透過 mock service 的方式來更好的測試,那如果一些基礎的東西也變成 service 是不是就更好做測試了呢?比如時間,或是 random ,像是 random ,如果是平常我們可能要像這樣去 mock

const spy = vi.spyOn(Math, 'random').mockImplementation(() => 42)

你需要去知道你的程式實際用到的是哪些 API ,並且使用 vi.spyOn 去 mock ,如果是時間相關的操作就又更複雜了,若有更容易讓我們 mock 的 service 就好了,然而實際上 Effect 已經幫你想好了,Effect 實際上就有一些內建的 service

  • Clock: 時間相關的
  • ConfigProvider: 讓我們可以提供設定到 Effect 的程式中
  • Console: 就是 console.log
  • Random: 亂數產生器
  • Tracer: observability 的那個 trace ,之後會再提到

你可以在這邊看到相關的介紹

我們先來聊兩個比較簡單的 RandomConsole ,如果你在你的程式中使用它們

import { Console, Random } from 'effect'

const logRandom = Effect.gen(function* () {
  yield* Console.log(yield* Random.nextInt)
})

那你就可以在測試中 mock 它們

it.effect('can mock random and inspect log', () =>
  Effect.gen(function* () {
    // 記得讓你的 mock function 回傳一個 effect
    const log = vi.fn().mockImplementation(() => Effect.void)
    yield* pipe(
      logRandom,
      Effect.withRandom(Random.fixed([42])),
      Effect.withConsole({
        log,
      } as unknown as Console.Console),
    )
    expect(log).toBeCalledWith(42)
  }),
)

原本我們可能還要用 vi.spyOn 來去攔截這些 function 的,這樣感覺是不是有變的好用一點

TestClock

上面還有提到一個 Clock 的內建 service ,其實這個 Clock 的 service 可不只是用來取時間而已,實際上 Effect 內建的 Schedule 等等也是依賴在這個 Clock 上,若是 mock 了這個 Clock 就連 Effect 中與時間還有 schedule 有關的 code 都能測試

有興趣可以看到這邊有 schedule 內部的 code ,確實有用 Clock 取得時間來算要暫停多久

不過我們還沒有提到怎麼讓 Effect 在背景執行,我們之後再來講怎麼用它來測試 Effect.sleep 等 code 吧,這次先來看時間跟 TestClock 吧,在 Effect 中,除了直接用原生的 Date 拿取現在的時間外,你也可以用 DateTime 拿取時間,這是 Effect 中提供的一個稍微簡單了一點,但通常而言非常夠用的時間操作的函式庫了,裡面有計算與時區的支援

而你用 DateTime 拿時間只需要透過 DateTime.now 就可以拿到當下的時間,但這時你也會發現,它回傳是一個 Effect

const getCurrentTime = pipe(
 DateTime.now,
 Effect.tap((now) => {
   console.log(now)
 })
)

如果你去 playground 上執行,那你拿到的應該是當下的 UTC 時間

但如果你在 vitest 中使用 it.effect 執行,你會發現你拿到的是 1970-01-01T00:00:00.000Z ,這個其實是 unix timestamp 開始的時間, it.effect 已經預設幫你把 Clock service 換成測試用的 TestClock 了,之後我們再來講跟多關於 TestClock 的用法

像這樣使用 service ,並且 mock service 就可以更容易的進行測試,下一篇我們又來要分享實際的經驗了,這次分享的內容是資料遷移,還請期待

Reference


上一篇
13. 如何測試 Effect 的程式
系列文
Effect 魔法:打造堅不可摧的應用程式15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言