Skip to content
Vitest 2
Main Navigation 指南API配置瀏覽器模式高級
2.1.9
1.6.1
0.34.6

繁體中文

English
简体中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

繁體中文

English
简体中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

外觀

Sidebar Navigation

為什麼使用 Vitest

開始使用

功能特性

工作區

命令列界面

測試過濾器

報告器

覆蓋率

快照

模擬(Mocking)

測試類型

Vitest UI

原始碼測試

測試上下文

測試環境

擴展匹配器

IDE 整合支援

偵錯

與其他測試執行器的比較

遷移指南

常見錯誤

Profiling Test Performance

提升效能

本頁導覽

模擬 ​

在撰寫測試時,您遲早會需要建立內部或外部服務的「虛擬」版本。這通常被稱為 模擬 (mocking)。Vitest 透過其 vi 輔助工具提供相關功能。您可以 import { vi } from 'vitest',或者在 全域設定 啟用時,直接全域存取它。

WARNING

請務必在每次測試執行之前或之後清除或還原模擬,以避免模擬狀態在不同測試之間互相影響!有關更多資訊,請參閱 mockReset 文件。

如果您想直接了解更多細節,請查看 API 部分;否則,請繼續閱讀以深入探索模擬的世界。

日期 ​

有時您需要控制日期,以確保測試時的一致性。Vitest 使用 @sinonjs/fake-timers 套件來操作計時器及系統日期。您可以在 這裡 找到有關特定 API 的詳細資訊。

範例 ​

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const businessHours = [9, 17];

function purchase() {
  const currentHour = new Date().getHours();
  const [open, close] = businessHours;

  if (currentHour > open && currentHour < close) {
    return { message: 'Success' };
  }

  return { message: 'Error' };
}

describe('purchasing flow', () => {
  beforeEach(() => {
    // 讓 Vitest 使用模擬時間
    vi.useFakeTimers();
  });

  afterEach(() => {
    // 在每次測試執行後還原日期
    vi.useRealTimers();
  });

  it('allows purchases within business hours', () => {
    // 設定營業時間內的小時
    const date = new Date(2000, 1, 1, 13);
    vi.setSystemTime(date);

    // 存取 Date.now() 將會回傳上面設定的日期
    expect(purchase()).toEqual({ message: 'Success' });
  });

  it('disallows purchases outside of business hours', () => {
    // 設定營業時間外的小時
    const date = new Date(2000, 1, 1, 19);
    vi.setSystemTime(date);

    // 存取 Date.now() 將會回傳上面設定的日期
    expect(purchase()).toEqual({ message: 'Error' });
  });
});

函數 ​

模擬函數可以分為兩種類別:監控 (spying) 與模擬 (mocking)。

有時您只需要驗證特定函數是否已被呼叫(以及可能傳遞了哪些參數)。在這些情況下,只需使用監控即可,您可以直接使用 vi.spyOn() (在此處閱讀更多)。

然而,監控只能幫助您監控函數,它們無法更改這些函數的實作。在我們確實需要建立函數的模擬版本的情況下,我們可以使用 vi.fn() (在此處閱讀更多)。

我們使用 Tinyspy 作為模擬函數的基礎,但我們有自己的包裝器,使其與 jest 相容。vi.fn() 和 vi.spyOn() 都共享相同的方法,但只有 vi.fn() 的回傳結果是可呼叫的。

範例 ​

js
import { afterEach, describe, expect, it, vi } from 'vitest';

const messages = {
  items: [
    { message: 'Simple test message', from: 'Testman' },
    // ...
  ],
  getLatest, // 如果支援,也可以是 `getter 或 setter 屬性`
};

function getLatest(index = messages.items.length - 1) {
  return messages.items[index];
}

describe('reading messages', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should get the latest message with a spy', () => {
    const spy = vi.spyOn(messages, 'getLatest');
    expect(spy.getMockName()).toEqual('getLatest');

    expect(messages.getLatest()).toEqual(
      messages.items[messages.items.length - 1]
    );

    expect(spy).toHaveBeenCalledTimes(1);

    spy.mockImplementationOnce(() => 'access-restricted');
    expect(messages.getLatest()).toEqual('access-restricted');

    expect(spy).toHaveBeenCalledTimes(2);
  });

  it('should get with a mock', () => {
    const mock = vi.fn().mockImplementation(getLatest);

    expect(mock()).toEqual(messages.items[messages.items.length - 1]);
    expect(mock).toHaveBeenCalledTimes(1);

    mock.mockImplementationOnce(() => 'access-restricted');
    expect(mock()).toEqual('access-restricted');

    expect(mock).toHaveBeenCalledTimes(2);

    expect(mock()).toEqual(messages.items[messages.items.length - 1]);
    expect(mock).toHaveBeenCalledTimes(3);
  });
});

更多 ​

  • Jest 的模擬函數

全域變數 ​

您可以使用 vi.stubGlobal 輔助工具來模擬 jsdom 或 node 中未提供的全域變數。它會將全域變數的值放入 globalThis 物件中。

ts
import { vi } from 'vitest';

const IntersectionObserverMock = vi.fn(() => ({
  disconnect: vi.fn(),
  observe: vi.fn(),
  takeRecords: vi.fn(),
  unobserve: vi.fn(),
}));

vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);

// 現在您可以將其作為 `IntersectionObserver` 或 `window.IntersectionObserver` 存取

模組 ​

模擬模組會監控在其他程式碼中調用的第三方函式庫,允許您測試參數、輸出,甚至重新宣告其實現。

有關更深入的詳細 API 描述,請參閱 vi.mock() API 部分。

自動模擬演算法 ​

如果您的程式碼正在匯入一個被模擬的模組,而該模組沒有任何相關的 __mocks__ 檔案或 factory,Vitest 將會自動載入該模組並模擬其所有匯出。

適用以下原則:

  • 所有陣列都將被清空
  • 所有基本類型和集合都將保持不變
  • 所有物件都將被深度複製
  • 所有類別的實例及其原型都將被深度複製

虛擬模組 ​

Vitest 支援模擬 Vite 虛擬模組。它的工作方式與 Jest 處理虛擬模組的方式不同。您不需要將 virtual: true 傳遞給 vi.mock 函數,而是需要告訴 Vite 該模組存在,否則解析將會失敗。您可以透過幾種方式做到這一點:

  1. 提供別名
ts
// vitest.config.js
export default {
  test: {
    alias: {
      '$app/forms': resolve('./mocks/forms.js'),
    },
  },
};
  1. 提供一個解析虛擬模組的外掛程式
ts
// vitest.config.js
export default {
  plugins: [
    {
      name: 'virtual-modules',
      resolveId(id) {
        if (id === '$app/forms') {
          return 'virtual:$app/forms';
        }
      },
    },
],
};

第二種方法的優點是您可以動態建立不同的虛擬入口點。如果您將多個虛擬模組指向單一檔案,那麼所有這些模組都將受到 vi.mock 的影響,因此請務必使用唯一的識別碼。

模擬陷阱 ​

請注意,無法模擬在同一檔案內,由其他方法內部呼叫的方法。例如,在此程式碼中:

ts
export function foo() {
  return 'foo';
}

export function foobar() {
  return `${foo()}bar`;
}

不可能從外部模擬 foo 方法,因為它是直接引用的。因此,此程式碼對 foobar 內部對 foo 的呼叫沒有影響(但會影響其他模組中對 foo 的呼叫):

ts
import { vi } from 'vitest';
import * as mod from './foobar.js';

// 這只會影響原始模組外部的 "foo"
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
  return {
    ...(await importOriginal<typeof import('./foobar.js')>()),
    // 這只會影響原始模組外部的 "foo"
    foo: () => 'mocked',
  };
});

您可以透過直接為 foobar 方法提供實作來確認此行為:

ts
// foobar.test.js
import * as mod from './foobar.js';

vi.spyOn(mod, 'foo');

// 匯出的 foo 引用模擬方法
mod.foobar(mod.foo);
ts
// foobar.js
export function foo() {
  return 'foo';
}

export function foobar(injectedFoo) {
  return injectedFoo === foo; // false
}

這是預期的行為。當模擬以這種方式被使用時,通常是程式碼設計不良的徵兆。考慮將您的程式碼重構為多個檔案,或透過使用 依賴注入 等技術來改進您的應用程式架構。

範例 ​

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';

// 取得待辦事項
export async function getTodos(event, context) {
  const client = new Client({
    // ...clientOptions
  });

  await client.connect();

  try {
    const result = await client.query('SELECT * FROM todos;');

    client.end();

    return success({
      message: `${result.rowCount} item(s) returned`,
      data: result.rows,
      status: true,
    });
  } catch (e) {
    console.error(e.stack);

    client.end();

    return failure({ message: e, status: false });
  }
}

vi.mock('pg', () => {
  const Client = vi.fn();
  Client.prototype.connect = vi.fn();
  Client.prototype.query = vi.fn();
  Client.prototype.end = vi.fn();

  return { Client };
});

vi.mock('./handlers.js', () => {
  return {
    success: vi.fn(),
    failure: vi.fn(),
  };
});

describe('get a list of todo items', () => {
  let client;

  beforeEach(() => {
    client = new Client();
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  it('should return items successfully', async () => {
    client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });

    await getTodos();

    expect(client.connect).toBeCalledTimes(1);
    expect(client.query).toBeCalledWith('SELECT * FROM todos;');
    expect(client.end).toBeCalledTimes(1);

    expect(success).toBeCalledWith({
      message: '0 item(s) returned',
      data: [],
      status: true,
    });
  });

  it('should throw an error', async () => {
    const mError = new Error('Unable to retrieve rows');
    client.query.mockRejectedValueOnce(mError);

    await getTodos();

    expect(client.connect).toBeCalledTimes(1);
    expect(client.query).toBeCalledWith('SELECT * FROM todos;');
    expect(client.end).toBeCalledTimes(1);
    expect(failure).toBeCalledWith({ message: mError, status: false });
  });
});

檔案系統 ​

模擬檔案系統可確保測試不依賴實際檔案系統,從而使測試更可靠和可預測。這種隔離有助於避免先前測試的副作用。它允許測試錯誤條件和邊緣情況,這些情況可能難以或不可能透過實際檔案系統重現,例如權限問題、磁碟空間不足或讀/寫錯誤。

Vitest 不提供任何內建的檔案系統模擬 API。您可以使用 vi.mock 手動模擬 fs 模組,但很難維護。相反,我們建議使用 memfs 為您完成此操作。memfs 建立一個記憶體內檔案系統,它模擬檔案系統操作而不會實際存取磁碟。這種方法快速且安全,避免對真實檔案系統產生任何潛在副作用。

範例 ​

若要自動將每個 fs 呼叫重新導向到 memfs,您可以在專案根目錄建立 __mocks__/fs.cjs 和 __mocks__/fs/promises.cjs 檔案:

ts
// 我們也可以使用 `import`,但這樣做的話
// 每個匯出都應該明確定義

const { fs } = require('memfs');
module.exports = fs;
ts
// 我們也可以使用 `import`,但這樣做的話
// 每個匯出都應該明確定義

const { fs } = require('memfs');
module.exports = fs.promises;
ts
// read-hello-world.js
import { readFileSync } from 'node:fs';

export function readHelloWorld(path) {
  return readFileSync(path);
}
ts
// hello-world.test.js
import { beforeEach, expect, it, vi } from 'vitest';
import { fs, vol } from 'memfs';
import { readHelloWorld } from './read-hello-world.js';

// 告訴 vitest 使用 __mocks__ 資料夾中的 fs 模擬
// 如果 fs 應該始終被模擬,這可以在設定檔中完成
vi.mock('node:fs');
vi.mock('node:fs/promises');

beforeEach(() => {
  // 重置記憶體內 fs 的狀態
  vol.reset();
});

it('should return correct text', () => {
  const path = '/hello-world.txt';
  fs.writeFileSync(path, 'hello world');

  const text = readHelloWorld(path);
  expect(text).toBe('hello world');
});

it('can return a value multiple times', () => {
  // 您可以使用 vol.fromJSON 來定義多個檔案
  vol.fromJSON(
    {
      './dir1/hw.txt': 'hello dir1',
      './dir2/hw.txt': 'hello dir2',
    },
    // 預設 cwd
    '/tmp'
  );

  expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
  expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});

請求 ​

由於 Vitest 在 Node 中執行,模擬網路請求較為複雜;Web API 不可用,因此我們需要一些東西來模仿網路行為。我們建議使用 Mock Service Worker 來完成此任務。它將允許您模擬 REST 和 GraphQL 網路請求,並且不受框架限制。

Mock Service Worker (MSW) 的工作原理是攔截您的測試發出的請求,讓您無需更改任何應用程式程式碼即可使用它。在瀏覽器中,這使用 Service Worker API。在 Node.js 中,對於 Vitest,它使用 @mswjs/interceptors 套件。要了解有關 MSW 的更多資訊,請閱讀他們的 介紹

設定 ​

您可以在 設定檔 中如下使用它

js
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, http, HttpResponse } from 'msw';

const posts = [
  {
    userId: 1,
    id: 1,
    title: 'first post title',
    body: 'first post body',
  },
  // ...
];

export const restHandlers = [
  http.get('https://rest-endpoint.example/path/to/posts', () => {
    return HttpResponse.json(posts);
  }),
];

const graphqlHandlers = [
  graphql.query('ListPosts', () => {
    return HttpResponse.json({
      data: { posts },
    });
  }),
];

const server = setupServer(...restHandlers, ...graphqlHandlers);

// 在所有測試之前啟動伺服器
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

// 在所有測試之後關閉伺服器
afterAll(() => server.close());

// 在每次測試後重置處理器 `對於測試隔離而言很重要`
afterEach(() => server.resetHandlers());

使用 onUnhandleRequest: 'error' 設定伺服器可確保當請求沒有對應的請求處理器時,都會拋出錯誤。

更多 ​

MSW 還有更多功能。您可以存取 cookie 和查詢參數、定義模擬錯誤回應等等!要查看 MSW 的所有功能,請閱讀 他們的說明文件。

計時器 ​

當我們測試涉及逾時或間隔的程式碼時,我們可以透過使用「模擬」計時器來模擬對 setTimeout 和 setInterval 的呼叫,從而加快測試速度,而不是讓測試等待或逾時。

有關更深入的詳細 API 描述,請參閱 vi.useFakeTimers API 部分。

範例 ​

js
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

function executeAfterTwoHours(func) {
  setTimeout(func, 1000 * 60 * 60 * 2); // 2 小時
}

function executeEveryMinute(func) {
  setInterval(func, 1000 * 60); // 1 分鐘
}

const mock = vi.fn(() => console.log('executed'));

describe('delayed execution', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });
  afterEach(() => {
    vi.restoreAllMocks();
  });
  it('should execute the function', () => {
    executeAfterTwoHours(mock);
    vi.runAllTimers();
    expect(mock).toHaveBeenCalledTimes(1);
  });
  it('should not execute the function', () => {
    executeAfterTwoHours(mock);
    // 推進 2 毫秒不會觸發函數
    vi.advanceTimersByTime(2);
    expect(mock).not.toHaveBeenCalled();
  });
  it('should execute every minute', () => {
    executeEveryMinute(mock);
    vi.advanceTimersToNextTimer();
    expect(mock).toHaveBeenCalledTimes(1);
    vi.advanceTimersToNextTimer();
    expect(mock).toHaveBeenCalledTimes(2);
  });
});

類別 ​

您可以透過單一 vi.fn 呼叫來模擬整個類別 - 因為所有類別也都是函數,所以這內建支援。請注意,目前 Vitest 不會遵守 new 關鍵字,因此函數主體中的 new.target 始終為 undefined。

ts
class Dog {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  static getType(): string {
    return 'animal';
  }

  speak(): string {
    return 'bark!';
  }

  isHungry() {}
  feed() {}
}

我們可以使用 ES5 函數重新建立此類別:

ts
const Dog = vi.fn(function (name) {
  this.name = name;
});

// 請注意,靜態方法直接在函數本身上模擬,
// 而不是在類別的實例上
Dog.getType = vi.fn(() => 'mocked animal');

// 模擬類別每個實例上的「speak」和「feed」方法
// 所有 `new Dog()` 實例都將繼承這些監控器
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();

何時使用?

一般來說,如果類別是從另一個模組重新匯出的,您會在模組工廠內部重新建立該類別:

ts
import { Dog } from './dog.js';

vi.mock(import('./dog.js'), () => {
  const Dog = vi.fn();
  Dog.prototype.feed = vi.fn();
  // ... 其他模擬
  return { Dog };
});

此方法還可以用於將類別實例傳遞給接受相同介面的函數:

ts
// ./src/feed.ts
function feed(dog: Dog) {
  // ...
}

// ./tests/dog.test.ts
import { expect, test, vi } from 'vitest';
import { feed } from '../src/feed.js';

const Dog = vi.fn();
Dog.prototype.feed = vi.fn();

test('can feed dogs', () => {
  const dogMax = new Dog('Max');

  feed(dogMax);

  expect(dogMax.feed).toHaveBeenCalled();
  expect(dogMax.isHungry()).toBe(false);
});

現在,當我們建立 Dog 類別的新實例時,它的 speak 方法(以及 feed)已經被模擬了:

ts
const dog = new Dog('Cooper');
dog.speak(); // loud bark!

// 您可以使用內建斷言來檢查呼叫的有效性
expect(dog.speak).toHaveBeenCalled();

我們可以為特定實例重新設定回傳值:

ts
const dog = new Dog('Cooper');

// "vi.mocked" 是一個型別輔助函式,因為
// TypeScript 不知道 Dog 是一個模擬類別,
// 它將任何函數包裝在 MockInstance<T> 型別中
// 而不驗證該函數是否為模擬
vi.mocked(dog.speak).mockReturnValue('woof woof');

dog.speak(); // woof woof

要模擬屬性,我們可以使用 vi.spyOn(dog, 'name', 'get') 方法。這使得可以在模擬屬性上使用監控斷言:

ts
const dog = new Dog('Cooper');

const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');

expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);

TIP

您也可以使用相同的方法監控 getter 和 setter。

速查表 ​

INFO

以下範例中的 vi 是直接從 vitest 匯入的。如果您在 設定 中將 globals 設定為 true,也可以全域使用它。

我想要…

模擬匯出變數 ​

js
// some-path.js
export const getter = 'variable';
ts
// some-path.test.ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');

模擬匯出函數 ​

  1. vi.mock 範例:

WARNING

請注意,vi.mock 呼叫會被提升到檔案頂部,並始終在所有匯入之前執行。

ts
// ./some-path.js
export function method() {}
ts
import { method } from './some-path.js';

vi.mock('./some-path.js', () => ({
  method: vi.fn(),
}));
  1. vi.spyOn 範例:
ts
import * as exports from './some-path.js';

vi.spyOn(exports, 'method').mockImplementation(() => {});

模擬匯出類別實作 ​

  1. vi.mock 和 .prototype 範例:
ts
// ./some-path.ts
export class SomeClass {}
ts
import { SomeClass } from './some-path.js';

vi.mock(import('./some-path.js'), () => {
  const SomeClass = vi.fn();
  SomeClass.prototype.someMethod = vi.fn();
  return { SomeClass };
});
// SomeClass.mock.instances 將會有 SomeClass
  1. vi.spyOn 範例:
ts
import * as mod from './some-path.js';

const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();

vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);

監控從函數回傳的物件 ​

  1. 使用快取範例:
ts
// some-path.ts
export function useObject() {
  return { method: () => true };
}
ts
// useObject.js
import { useObject } from './some-path.js';

const obj = useObject();
obj.method();
ts
// useObject.test.js
import { useObject } from './some-path.js';

vi.mock(import('./some-path.js'), () => {
  let _cache;
  const useObject = () => {
    if (!_cache) {
      _cache = {
        method: vi.fn(),
      };
    }
    // 現在每次呼叫 useObject() 都會
    // 回傳相同的物件實例
    return _cache;
  };
  return { useObject };
});

const obj = useObject();
// obj.method 在 some-path 內部被呼叫
expect(obj.method).toHaveBeenCalled();

模擬模組的一部分 ​

ts
import { mocked, original } from './some-path.js';

vi.mock(import('./some-path.js'), async importOriginal => {
  const mod = await importOriginal();
  return {
    ...mod,
    mocked: vi.fn(),
  };
});
original(); // 具有原始行為
mocked(); // 是一個監控函數

WARNING

請注意,這只會 模擬 外部 存取。在此範例中,如果 original 在內部呼叫 mocked,它將始終呼叫模組中定義的函數,而不是模擬工廠中的函數。

模擬目前日期 ​

要模擬 Date 的時間,您可以使用 vi.setSystemTime 輔助函數。此值不會在不同測試之間自動復原。

請注意,使用 vi.useFakeTimers 也會更改 Date 的時間。

ts
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// 重置模擬時間
vi.useRealTimers();

模擬全域變數 ​

您可以透過將值指派給 globalThis 或使用 vi.stubGlobal 輔助工具來設定全域變數。當使用 vi.stubGlobal 時,它不會在不同測試之間自動復原,除非您啟用 unstubGlobals 設定選項或呼叫 vi.unstubAllGlobals。

ts
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');

模擬 import.meta.env ​

  1. 要更改環境變數,您只需為其指派一個新值。

WARNING

環境變數值不會在不同測試之間自動復原。

ts
import { beforeEach, expect, it } from 'vitest';

// 您可以在 beforeEach 鉤子中手動復原它
const originalViteEnv = import.meta.env.VITE_ENV;

beforeEach(() => {
  import.meta.env.VITE_ENV = originalViteEnv;
});

it('changes value', () => {
  import.meta.env.VITE_ENV = 'staging';
  expect(import.meta.env.VITE_ENV).toBe('staging');
});
  1. 如果您想自動復原值,您可以使用 vi.stubEnv 輔助工具,並啟用 unstubEnvs 設定選項(或在 beforeEach 鉤子中手動呼叫 vi.unstubAllEnvs):
ts
import { expect, it, vi } from 'vitest';

// 在執行測試之前,"VITE_ENV" 是 "test"
import.meta.env.VITE_ENV === 'test';

it('changes value', () => {
  vi.stubEnv('VITE_ENV', 'staging');
  expect(import.meta.env.VITE_ENV).toBe('staging');
});

it('在執行另一個測試之前,該值會被復原', () => {
  expect(import.meta.env.VITE_ENV).toBe('test');
});
ts
// vitest.config.ts
export default defineConfig({
  test: {
    unstubEnvs: true,
  },
});
Pager
上一頁快照
下一頁測試類型

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors

https://v2.vitest.dev/guide/mocking

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors