Deno標準モジュール解説[後編] ~モジュール解説(FS~Wasi)と、Deno標準モジュールの今後の展望

Deno標準モジュールを、前編と後編の2回に分けて解説します。本記事は後編です前編はこちら⁠⁠。

モジュール解説

前編に続き、以下ではDeno標準モジュールの中の各モジュールについて解説していきます。

11. FS

FSではファイル操作用のユーティリティが実装されています。具体的には以下のような機能が提供されています。

  • copy:ファイルもしくはディレクトリをコピーする
  • detect:ファイルの内容を受け取ってファイルの改行形式を判定する
  • emptyDir:ディレクトリを空にする(ディレクトリ自体は消されない)
  • ensureDir:ディレクトリがなければ作成する
  • ensureFile:ファイルがなければ作成する
  • ensureLink:ハードリンクがなければ作成する
  • ensureSymlink:シンボリックリンクがなければ作成する
  • format:ファイルの改行形式を整形する
  • expandGlob:グロブを展開する
  • expandGlobSync:グロブを同期的に展開する
  • move:ファイルもしくはディレクトリを移動する
  • moveSync:ファイルもしくはディレクトリを同期的に移動する
  • walk:ディレクトリのすべてのエントリーを返すAsyncIterableを返す
  • walkSync:ディレクトリのすべてのエントリーを返すIterableを返す

ここでは例として、ensureDirwalkの使い方を見てみましょう。

import { ensureDir } from "https://deno.land/std@0.170.0/fs/mod.ts";
await ensureDir("./bar"); // ディレクトリがなければ作成する、すでにある場合は何もしない
import { walk } from "https://deno.land/std@0.170.0/fs/walk.ts";
import { assert } from "https://deno.land/std@0.170.0/testing/asserts.ts";

// カレントディレクトリ以下のすべてのファイル・ディレクトリについて繰り返し処理する
for await (const entry of walk(".")) {
  console.log(entry.path);
  assert(entry.isFile);
}

existsexistsSyncについて

FSモジュールにはexistsexistsSyncという現在は非推奨化された2つのユーティリティが存在します。これらの関数は、あるファイルまたはディレクトリが存在するかどうかを判定します。

この機能は一見問題なさそうに見えますが、TOCTOU(Time of check, time of use)と呼ばれるパターンのバグを生み出しやすい関数として、非推奨化されています。

if (existsSync("data.txt")) {
  // ファイルがあると前提してdata.txtを読む
  const text = await Deno.readTextFile("data.txt");
}

上記のコードは一見問題なさそうですが、実は問題があります。existsSync("data.txt")のチェックがtrueになったとしても、Deno.readTextFile("data.txt")の実行時にファイルが存在するとは限らないからです。existsSyncの呼び出しと、Deno.readTextFileの呼び出しの間には僅かながら時間差があるため、その間にファイルが消されないという保証がないためです。上記のコードは以下のように、直接ファイルを読もうとして、ない場合は例外処理として実行するのが適切です。

try {
  const text = await Deno.readTextFile("data.txt");
} catch (e) {
  if (e instanceof Deno.errors.NotFound) {
    // ファイルが無かった場合の処理
  }
}

12. HTTP

HTTPでは、HTTPのサーバー・クライアントを構築する上で有用なヘルパー群を提供しています。HTTPモジュールで特に重要なものはserve APIです。serveは以下のように使って、HTTPサーバを起動できます。

import { serve } from "https://deno.land/std@0.170.0/http/server.ts";
serve((_req) => new Response("Hello, world"));

serveはいろいろなオプションを持っています。例えばportを変更したい場合はportオプションを指定します。

import { serve } from "https://deno.land/std@0.170.0/http/server.ts";
serve((_req) => new Response("Hello, world"), { port: 3000 });

HTTPでもう一つ重要な機能はfile_serverです。file_serverはCLIツールになっていて、以下のように起動することで、指定ディレクトリ(デフォルトはカレントディレクトリ)以下のファイルを対象とした静的ファイルサーバーになります。

$ deno run --allow-read --allow-net https://deno.land/std@0.170.0/http/file_server.ts
Listening on http://localhost:4507/

以上の他に、以下のようなユーティリティが実装されています。

  • CookieMap:クッキーをMap的なインタフェースで扱うためのクラス
  • SecureCookieMap:署名付きクッキーをMap的なインターフェスで扱うためのクラス。署名の生成・検証が自動的に行われる
  • HttpError:HTTPのエラーステータスを表現するクラス
  • accepts:RequestオブジェクトからAcceptヘッダーを読み出して、受け付けているメディアタイプを優先度順に並べた配列を返す
  • acceptsEncodings:RequestオブジェクトからAccept-Encodingヘッダーを読み出して、受け付けているエンコーディングを優先度順に並べた配列を返す
  • acceptsLanguages:RequestオブジェクトからAccept-Languageヘッダーを読み出して、受け付けている表示言語を優先度順に並べた配列を返す
  • deleteCookie:Headersオブジェクトに与えられたキーのcookieを消すためのSet-Cookieヘッダーを書き込む
  • getCookie:Headersオブジェクトからクライアントから送られてきたクッキーをオブジェクト形式で抜き出して返す
  • getSetCookie:Headersオブジェクトからサーバーから送られてきたクッキーを抜き出してCookie型の配列として返す
  • isClientErrorStatus:与えられたステータスコードがクライアント由来エラー(4XX系)であるか判定する
  • isErrorStatus:与えられたステータスコードがエラー(4XXまたは5XX)であるか判定する
  • isInformationalStatus:与えられたステータスコードがインフォメーショナル(1XX系)であるか判定する
  • isRedirectStatus:与えられたステータスコードがリダイレクト(3XX系)であるか判定する
  • isServerErrorStatus:与えられたステータスコードがサーバー由来エラー(5XX系)であるか判定する
  • mergeHeaders:複数のHeadersオブジェクトをマージする
  • serveListener:与えられたDeno.Listenerオブジェクトを使ってHTTPサーバーを起動する
  • serveTls:TLS上のHTTP(https://)サーバーを起動する
  • setCookie:Headersにクライアント向きのクッキーをセットする

HTTPモジュールでは、あえてWebフレームワーク的な機能は取り込まないという方針があります。そのため、上記のユーティリティ群は、特定のWebプログラミングパターンを支援するものというよりは、どのフレームワークでも内部的に利用可能な、プリミティブなものに限定して提供されています。

より効率的にWebサーバーを記述したい場合は、より本格的な3rdパーティ製のフレームワークを参照することをお勧めします。Denoでよく使われる3rdパーティ製のWebフレームワークには以下のようなものが有名です。

OakとHonoはいわゆるexpress的なレイヤーを担うフレームワークです。REST APIサーバーを作りたい場合や、従来型のテンプレートエンジン(handlebars、ejs、nunjucks、jade等)でHTMLをレンダリングできれば良い場合、あるいはHTMLは静的ビルドするので、Webサーバーは静的配信だけで良い場合などは、OakやHonoが適切な選択肢になります。

FreshやAlephはいわゆるNext.js的なフレームワークです。ただしSSG(静的サイト)配信の機能はなく、SSR(サーバーサイドレンダリング)だけをサポートしています。React(もしくはPreact)を使ったJSXの記法でページを記述できる点が特徴です。また、特定ディレクトリ以下のファイルパスがそのままURLになる点はNext.js、まはたは、古くはPHP、CGIなどと同じ使用感になります。フレームワーク1つでより効率的にWebアプリケーションを構築したい場合は、これらのフレームワークも検討してみましょう。特にFreshについては、Deno自体が公式に開発・メンテナンスしており、将来的にも安定してサポートされる事が期待できます。

13. Log

Logではログ出力関連のユーティリティが実装されています。現状以下の5レベルのロギングをサポートしてます。

  • debug:デバッグレベル
  • info:参考レベル
  • warning:警告レベル
  • error:エラーレベル
  • critical:致命的レベル

以下のように使用します。

import * as log from "https://deno.land/std@0.170.0/log/mod.ts";

log.debug("デバッグ");
log.info("参考");
log.warning("警告");
log.error("エラー");
log.critical("致命的エラー");

logの向き先をファイルとコンソール両方にしたい場合は以下のようにして、File handlerを追加することで実現できます。

import * as log from "https://deno.land/std@0.170.0/log/mod.ts";

await log.setup({
  handlers: {
    console: new log.handlers.ConsoleHandler("DEBUG"),
    file: new log.handlers.FileHandler("WARNING", { filename: "./log.txt" }),
  },
  loggers: {
    default: {
      level: "DEBUG",
      handlers: ["console", "file"],
    },
  },
});

log.debug("デバッグ");
log.info("参考");
log.warning("警告");
log.error("エラー");
log.critical("致命的エラー");
// WARNING以上のエラーは、ターミナルとファイルの両方に書き出されます。

より詳細な使い方については公式ドキュメントを参考にしてください。

14. Media Types

Media Typesでは、メディアタイプ(MIMEタイプ)に関するヘルパー群が実装されています。

  • contentType:メディアタイプ文字列、もしくはファイル拡張子を引数にとって対応するメディアタイプ文字列を返す
  • extension:メディアタイプ文字列を受け取って、そのメディアタイプのファイルの最も代表的な拡張子を返す
  • extensionsByType:メディアタイプ文字列を受け取って、そのメディアタイプのファイルの代表的な拡張子すべての配列を返す
  • formatMediaType:メディアタイプ文字列を整形する
  • getCharset:メディアタイプ文字列から適切な文字セットを返す
  • parseMediaType:メディアタイプ文字列をパースする
  • typeByExtension:ファイル拡張子を受け取って対応するメディアタイプを返す

例として、contentType関数の使い方を紹介します。

import { contentType } from "https://deno.land/std@0.170.0/media_types/content_type.ts";

contentType(".json"); // `application/json; charset=UTF-8`
contentType("text/html"); // `text/html; charset=UTF-8`
contentType("text/html; charset=UTF-8"); // `text/html; charset=UTF-8`
contentType("txt"); // `text/plain; charset=UTF-8`
contentType("foo"); // undefined
contentType("file.json"); // undefined

15. Node

NodeモジュールではNode.jsの互換API実装が提供されています。Node.jsのすべてのビルトインAPIを提供することを目標に活発に開発が続けられています。現在の進捗度としては3449の対象のユニットテストファイルのうち638ファイル(18%)をパスできる程度ですが、利用頻度の高いモジュール(buffer、events、fs、http、child_process等)について優先的に開発が進められているため、現時点でもかなりの数のnpmモジュールを動かすことに成功しています。

この標準モジュールのNode以下のAPIはnpm:でnpmモジュールをインポートした際のNodeのAPIのshimとしても動いており、このモジュールの開発が進むことでnpm:でインストールするnpmモジュールの互換性が上がっていくという関係性になっています。

例として、Node.js互換のhttpサーバーをDenoから立ち上げる例を見てみましょう

import * as http from "https://deno.land/std@0.170.0/node/http.ts";
const server = http.createServer((req, res) => {
  res.write("Hello");
  res.end();
});
server.listen(async () => {
  const resp = await fetch(`http://localhost:${server.address().port}`);
  console.log(await resp.text()); // => Hello
  server.close();
});

詳細は公式ドキュメントを参照してください。

16. Path

Pathではファイルパス操作関連の各種ヘルパーが実装されています。具体的には以下のような機能が提供されています。

  • basename:ファイルパスのファイル名部分を取り出す
  • dirname:ファイルパスのディレクトリ名部分を取り出す
  • extname:ファイルパスの拡張子部分を取り出す
  • format:パースされたパスオブジェクトをパス文字列にする
  • fromFileUrl ファイルを表すURLオブジェクトfile:///で始まるものをパス文字列に変換する
  • isAbsolute:ファイルパスが絶対パスかどうか判定する
  • join:パスを結合する
  • normalize:ファイルパスを正規化する(重複したパス区切り文字の削除など)
  • parse:ファイルパスをパースしてパスオブジェクトにする
  • posix:POSIX準拠のパス操作機能を提供する名前空間(WindowsでPOSIXのパス操作をしたい場合に使う)
  • relative:与えらたファイルパスから、別の与えられたファイルパスを参照する相対パス表記を返す
  • resolve:与えらた複数のファイルパスを合成した絶対パスを返す
  • toFileUrl:ファイルパスをファイルURLに変換する
  • toNamespacedPath:Windows上で与えらたファイルパスを名前空間付きパスに変換する
  • win32:Windows用のパス操作機能を提供する名前空間(Linux/MacでWindows用のパス操作をしたい場合に使う)

ここでは例として、basenamedirnamejoinの使い方を見てみましょう。

import {
  basename,
  dirname,
  join,
} from "https://deno.land/std@0.170.0/path/mod.ts";

basename("/path/to/my-file.txt"); // => my-file.txt
dirname("/path/to/my-file.txt"); // => /path/to
join("foo", "bar", "baz"); // => foo/bar/baz or foo\bar\baz

PathモジュールはNode.jsのpathモジュールをベースにデザインされているため、関数名や挙動などはほとんどNode.jsと同様になっています。

17. Permissions

PermissionsではDenoのPermission機能を使うためのヘルパーが提供されています。以下のAPIが提供されています。

  • grant:複数のパーミッションを一気に要求する
  • grantOrThrow:複数のパーミッションを一気に要求し、それらが満たされない場合は例外を投げる

例としてgrantOrThrowの使い方を見てみましょう。

import { grantOrThrow } from "https://deno.land/std@0.170.0/permissions/mod.ts";
await grantOrThrow({ name: "env" }, { name: "net" });
// 後続処理ではenvとnetパーミッションがあることを想定できる

このように、Denoはデフォルトではプログラムの途中で必要なパーミッションをその都度ユーザーに聞くように挙動しますが、上記のように書くことで、プログラムの開始時にまとめてパーミッションを要求できます。

18. Semver

SemverではSemantic Versioning(Semver)の比較・操作・編集をサポートする機能が実装されています。具体的には以下の様なAPIが提供されています。

  • compare:2つのバージョンを比較した結果を返す
  • compareBuild:2つのバージョンをビルド番号まで比較した結果を返す
  • difference:2つのバージョンの差を表すリリースタイプ("major"、"minor"等)を返す
  • eq:2つのバージョンが等しい時にtrueを返す
  • gt:2つのバージョンの左が大きい時にtrueを返す
  • gte:2つのバージョンの左が大きいか等しい時にtrueを返す
  • gtr:バージョンが、バージョンレンジよりも大きい時にtrueを返す
  • increment:バージョンをリリースタイプ("major"、"minor"等)でインクリメント(プラス1)する
  • intersect:2つのバージョンレンジに交わりがあるかどうかを判定する
  • lt:2つのバージョンの左が小さい場合にtrueを返す
  • lte:2つのバージョンの左が小さいか等しい場合にtrueを返す
  • ltr:バージョンがバージョンレンジより小さい場合にtrueを返す
  • major:バージョンのメジャーバージョン部分を返す
  • maxSatisfying:バージョンの配列とバージョンレンジを受け取って配列の中からバージョンレンジを満たすもののうち最大のものを返す
  • minor:バージョンのマイナーバージョン部分を返す
  • minSatisfying:バージョンの配列とバージョンレンジを受け取って配列の中からバージョンレンジを満たすもののうち最小のものを返す
  • minVersion:バージョンレンジの中の最小バージョンを返す
  • neq:2つのバージョンが等しくない時にtrueを返す
  • outside:バージョンがバージョンレンジに含まれない時にtrueを返す
  • parse:バージョン文字列を受け取ってパージョンオブジェクトを返す
  • patch:バージョンのパッチバージョン部分を返す
  • prerelease:バージョンのプレリリース部分(番号の後ろのalpha.1やbeta.2などの部分)を返す
  • rcompare:2つのバージョンを比較した結果を返す(compareの逆)
  • rsort:バージョンの配列をソートする(ソート順はsortの逆)
  • satisfies:バージョンがバージョンレンジの中に含まれる時にtrueを返す
  • sort:バージョンの配列をソートする
  • valid:バージョン文字列がSemverのルールに沿っているかどうかを判定する
  • validRange:バージョン文字列が(npm互換の)バージョンレンジのルールに沿っているかを判定する

例として、comparesatisfiesを使う例を見てみましょう。

import { compare } from "https://deno.land/std@0.170.0/semver/mod.ts";

compare("1.2.3", "1.1.5"); // => 1
compare("1.2.3", "1.3.0"); // => -1
compare("1.2.3-alpha", "1.2.3-beta"); // => -1
compare("1.2.3", "1.2.3"); // => 0
import { satisfies } from "https://deno.land/std@0.170.0/semver/mod.ts";

satisfies("1.3.0", ">=1.2.3"); // => true
satisfies("1.3.0", "~1.2.3"); // => false
satisfies("1.2.5", "~1.2.3"); // => true
satisfies("1.3.0", "^1.2.3"); // => true

Semverモジュールはnpmモジュールのsemverを元にデザインされています。したがってバージョンレンジ表現などの非標準部分も含めて同npmモジュールに準拠したデザインになっています(ただし、一部の互換性機能などDeno標準モジュールに必要無さそうな機能については削除されています⁠⁠。

19. Signal

Signalでは、OS Signalのハンドリングについてのヘルパーが実装されています。現在は複数のsignalをまとめてAsyncIterable化出来るsignalヘルパーだけが実装されています。

import { signal } from "https://deno.land/std@0.170.0/signal/mod.ts";

setTimeout(() => {}, Infinity);

const sig = signal("SIGKILL", "SIGINT");
for await (const _ of sig) {
  // SIGKILLかSIGINTを受け取るとここが走る

  if (...) {
    // シグナル監視を止めたい場合はdispose(リソースを開放)する
    sig.dispose();
  }
}

Deno本体にはDeno.addSignalListenerというイベントハンドラー型のAPIが実装されています。AsyncIterable型の記法でシグナル監視を記述したい場合はこちらのヘルパーを使ってみましょう。

20. Streams

StreamsではWeb Stream API関連のヘルパーが実装されています。I/Oに関する高度な処理をしたい場合にこのモジュール以下のヘルパーが役に立つはずです。このモジュールでは以下のようなAPIが提供されています。なお、Go言語由来の旧I/OインターフェースであるReader/WriterのためのAPIについては将来的に非推奨になるため、記載を省略します。

  • ByteSliceStream:バイトストリームReadableStream<Uint8Array>から指定の開始位置から終了位置までを切り取るTransformStream
  • DelimiterStream:バイトストリームReadableStream<Uint8Array>を指定の区切り文字で区切ったチャンクに切り分けるTransformStream
  • LimitedBytesTransformStream:バイトストリームReadableStream<Uint8Array>から指定の先頭バイトを切り取るTransformStream
  • LimitedTransformStream:任意型のストリームから指定の先頭チャンク数を抜き出すTransformStream
  • TextDelimiterStream:テキストのストリームReadableStream<string>を指定の区切り文字で区切ったチャンクに切り分けるTransformStream
  • TextLineStream:テキストのストリームを改行文字で区切ったチャンクに切り分けるストリーム
  • earlyZipReadableStream:複数のReadableStreamを入力として受け取り、各ストリームから1チャンクづつ取り出したストリームを作る。1つの入力データが終わったタイミングで全体の読み取りを終了する
  • mergeReadableStream:複数のReadableStreamの入力をマージする。チャンクの順番は考慮されない。すべての入力データが終わるまで読み取りを続ける
  • readableStreamFromIterable:IterableもしくはAsyncIterableを受け取って、その出力をチャンクとするようなReadableStreamを返す
  • readableStreamFromReader:Readerインターフェース(Go言語由来の旧I/Oインターフェス)を受け取って、ReadableStreamに変換する
  • toTransformStream:ReadableStreamを引数として受け取るようなGenerator関数をTransformStreamに変換する
  • writableStreamFromWriter:Writerインターフェース(Go言語由来の旧I/Oインターフェス)を受け取って、WritableStreamに変換する
  • zipReadableStream:複数のReadableStreamを入力として受け取り、各ストリームから1チャンクづつ取り出したストリームを作る。すべての入力データの読み取りが終了するまでデータを読み続ける

ここでは例として、ByteSliceStreamtoTransformStreamの使い方を紹介します。

import { ByteSliceStream } from "https://deno.land/std@0.170.0/streams/byte_slice_stream.ts";
const response = await fetch("https://example.com");
const rangedStream = response.body!
  .pipeThrough(new ByteSliceStream(1000, 5000));
// rangedStreamは1001バイト目移行5000バイト目までのストリームになる
import { toTransformStream } from "https://deno.land/std@0.170.0/streams/to_transform_stream.ts";

const readable = new ReadableStream({
  start(controller) {
    controller.enqueue(0);
    controller.enqueue(1);
    controller.enqueue(2);
    controller.close();
  },
})
  .pipeThrough(toTransformStream(async function* (src) {
    for await (const chunk of src) {
      yield chunk * 100;
    }
  }));

for await (const chunk of readable) {
  console.log(chunk);
}
// output: 0, 100, 200

toTransformStreamを使うと、Generator関数を使ってTransformStreamを自然に書くことができます。Node.jsではTransformストリームを記述する際に、through2というモジュールを使うのが便利でしたが、それに近い用途に用いることができます。

21. Testing

Testingではアサーション関数を始めとする、テストを書く際に便利な各種ヘルパー機能が提供されています。

アサーション系は以下が提供されています。

  • assert:対象がtrue(より正確にはtruthy)であることをアサートする
  • assertAlmostEquals:2つの数が一定の誤差を許して、ほぼ等しいことをアサートする
  • assertArrayIncludes:配列が要素を含むことをアサートする
  • assertEquals:2つの値が等しいことをアサートする(deep equalアルゴリズムで判定)
  • assertExists:値がnullish(nullかundefined)ではないことをアサートする
  • assertFalse:値がfalse(より正確にはfalsy)であることをアサートする
  • assertInstanceOf:値があるクラスのインスタンスであることをアサートする
  • assertIsError:値がErrorオブジェクトであることをアサートする
  • assertMatch:文字列が正規表現にマッチすることをアサートする
  • assertNotEquals:2つの値が等しくないことをアサートする(deep equalアルゴリズムで判定)
  • assertNotInstanceOf:値があるクラスのインスタンスでないことをアサートする
  • assertNotMatch:文字列が正規表現にマッチしないことをアサートする
  • assertNotStrictEquals:2つの値が厳密比較で一致しないことをアサートする
  • assertObjectMatch:値が別のオブジェクトの部分になっていることをアサートする(部分であることの判定はオブジェクト構造に対して再帰的に判定される)
  • assertRejects:非同期関数がリジェクトされることをアサートする(リジェクトされた値はassertRejectsの返り値になる)
  • assertStrictEquals:2つの値が厳密に等しいことをアサートする
  • assertStringIncludes:文字列が別の文字列に含まれることをアサートする
  • assertThrows:関数が例外を投げることをアサートする(投げられた例外はassertThrowsの返り値になる)
  • equal:2つの値の比較結果を真偽値で返す
  • fail:テストを失敗させる

例としてassertEqualsassertThrowsを使う例を紹介します。

import {
  assertEquals,
  assertThrows,
} from "https://deno.land/std@0.170.0/testing/asserts.ts";

Deno.test("値の等しさをチェックする", () => {
  assertEquals("world", "world");
  assertEquals({ hello: "world" }, { hello: "world" });
});

Deno.test("URL のパースエラーをチェックする", () => {
  assertThrows(() => {
    new URL("invalid url");
  });
});

BDDスタイルテスト

TestingモジュールではJestやMochaなどのBDD(Behavior Driven Development)スタイルのテストフレームワークで提供される形のテストランナーも提供されています。BDDスタイルでテストを書く場合は以下のようになります。

import {
  assertEquals,
  assertThrows,
} from "https://deno.land/std@0.170.0/testing/asserts.ts";
import { describe, it } from "https://deno.land/std@0.170.0/testing/bdd.ts";
import { User } from "https://deno.land/std@0.170.0/testing/bdd_examples/user.ts";

describe("User", () => {
  it("users initially empty", () => {
    assertEquals(User.users.size, 0);
  });

  it("constructor", () => {
    const user = new User("Alice");
    assertEquals(user.name, "Alice");
  });

  describe("age", () => {
    it("getAge", function () {
      const user = new User("Bob");
      assertThrows(() => user.getAge(), Error, "Age unknown");
      user.age = 18;
      assertEquals(user.getAge(), 18);
    });

    it("setAge", function () {
      const user = new User("Charlie");
      user.setAge(18);
      assertEquals(user.getAge(), 18);
    });
  });
});

このファイルをtest.tsなどの名前で保存して、deno testコマンドで実行できます。

$ deno test test.ts
Check file:///path/to/test.ts
running 1 test from ./test.ts
User ...
  users initially empty ... ok (6ms)
  constructor ... ok (5ms)
  age ...
    getAge ... ok (4ms)
    setAge ... ok (5ms)
  age ... ok (16ms)
User ... ok (37ms)

ok | 1 passed (5 steps) | 0 failed (73ms)

itで指定した仕様が、テストケースとして認識されていることが確認できます。他にafterEachbeforeEachなどの各種フック関数なども提供されています。MochaやJestに親しみのあるユーザは、デフォルトのDeno.testよりもこちらのほうが手に馴染むかもしれません。

モック

Testingモジュールではモックテストを支援するためのユーティリティ群も提供されています。モックテストの例として、spystubを使う例を紹介します。

spyを使う例は以下のようになります。

import {
  assertSpyCall,
  assertSpyCalls,
  spy,
} from "https://deno.land/std@0.170.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.170.0/testing/asserts.ts";

Deno.test("how spy works", () => {
  const func = spy();
  // スパイが呼ばれていないことの確認
  assertSpyCalls(func, 0);

  assertEquals(func(), undefined);
  // スパイが空の引数リストで呼ばれた事の確認
  assertSpyCall(func, 0, { args: [] });
  assertSpyCalls(func, 1);

  assertEquals(func("x"), undefined);
  // スパイが"x"という引数で呼ばれたことの確認
  assertSpyCall(func, 1, { args: ["x"] });
  assertSpyCalls(func, 2);
});

また、stubを使う例は以下のようになります。

import {
  returnsNext,
  stub,
} from "https://deno.land/std@0.170.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.170.0/testing/asserts.ts";

Deno.test("how stub works", () => {
  // Math.randomをスタブで置き換える
  // この設定でMath.randomは最初に0.1を、次に0.2を、次に0.3を返すようになる
  const mathStub = stub(Math, "random", returnsNext([0.1, 0.2, 0.3]));
  try {
    assertEquals(Math.random(), 0.1);
    assertEquals(Math.random(), 0.2);
    assertEquals(Math.random(), 0.3);
  } finally {
    // もともとのMath.randomに戻すためには.restore()を呼びます
    mathStub.restore();
  }
});

上記のようにstubを使うと、特定のオブジェクトの挙動を任意の挙動に置き換えることができます。

スナップショットテスト

Testingモジュールではスナップショットテストの仕組みも提供しています。スナップショットテストを使うことで、たとえば、コマンドラインツールの出力であったり、何らかの言語の変換結果であったりといった、単純にassertEqualsでアサーションするには手間がかかりすぎる対象をテストしたい際にスナップショットテストが有効になります。

スナップショットテストをする簡単な例を以下に紹介します。

import { assertSnapshot } from "https://deno.land/std@0.136.0/testing/snapshot.ts";

Deno.test("出力をスナップショットと比較する", async (t) => {
  const output = generateSomething();
  await assertSnapshot(t, output);
});

上記のようにassertSnapshotを呼び出すことで、自動的にテストコンテキストtから適切なスナップショットと与えられた値outputが等しいかを比較してアサーションします。なお、スナップショットを更新したい場合は、テストコマンドの最後に-- --updateという引数を与えることで、スナップショットを更新できます。

22. UUID

UUIDではバージョン1、4、5のUUIDを生成する機能が提供されています。

import { v1, v4, v5 } from "https://deno.land/std@0.170.0/uuid/mod.ts";
v1.generate(); // UUID v1準拠の文字列が生成されます
v4.generate(); // UUID v4準拠の文字列が生成されます
v5.generate(); // UUID v5準拠の文字列が生成されます

なお、UUID v4については、Web標準APIである、crypto.randomUUID()という関数を使って生成することもできます。

23. Wasi

WasiモジュールではWASI(WebAssembly System Interface)のランタイムの実装が提供されています。以下のようにして、wasi_snapshot_preview1モジュール(2023年現在のWASIの最新バージョン)に依存したwasmバイナリを実行できます。

import Context from "https://deno.land/std@0.170.0/wasi/snapshot_preview1.ts";

const context = new Context({
  args: Deno.args,
  env: Deno.env.toObject(),
});
const binary = await Deno.readFile("path/to/wasm");
const module = await WebAssembly.compile(binary);
const instance = new WebAssembly.Instance(module, {
  "wasi_snapshot_preview1": context.exports,
});
context.start(instance);

WASIに依存したwasmバイナリの生成方法についてはWASIの公式サイトなどを参考にしてみてください。

Deno標準モジュールの今後の展望

最後にDeno標準モジュールで今後検討されているプランなどについて紹介します。

I/Oインターフェースの移行作業

Denoは当初I/Oのためのインターフェースとして、Go言語に由来するReader/Writerというインターフェースを使っていました。しかし、次第にReadableStream/WritableStreamというWeb標準由来のI/Oインターフェース(いわゆるWeb Streams)が必須であることが分かり、現在はそちらへの移行作業の最中です(この意思決定の考え方については、Denoレポジトリのissue 9795などを参照してください⁠⁠。

移行作業はかなり進捗しており、大部分がWeb Streamsで扱えるようになってはいるものの、一部のAPIがまだ旧Reader/Writerインターフェースでのみ提供されているものがあります(たとえばArchiveモジュールなど⁠⁠。これらのAPIは今後徐々にWeb Streamsベースのデザインに置き換えられることが予定されています(具体的な進捗については標準モジュールのIssue 1986を参照してください⁠⁠。

旧インターフェースでしか提供されていない機能を使う場合は、StreamsモジュールのreadableStreamFromReaderreaderFromIterableなどを使ってインターフェースを相互に変換して使う必要があります。

直近のプライオリティ

現在の標準モジュールのなかで最も開発の優先度が高いのはNodeモジュールstd/nodeです。std/nodeはDeno本体のnpm互換性を提供するための基礎部分になっており、std/nodeの開発が進むことで、Denoから使えるnpmモジュールが増えていくという関係性になっています。

std/nodeの中でも特に、net、http、tls周りの細かい互換性実装が現在は優先的に進められています。

今後追加の可能性があるモジュール

現状でもかなり分量が多い標準モジュールですが、まだまだカバー範囲が十分ではありません。

よく挙がる提案の一つとして、数学関連の処理が弱いという点があります。現状、たとえば、あるデータの平均や標準偏差を計算するような統計的な処理が標準モジュールからは提供されていません。何度かこのような機能の提案があったものの、そのような機能群をどう位置付けて開発するかという点で合意が形成できなかったために、見送りとなっています。今後のコミュニティからの提案次第で、数学関連のモジュールが追加される可能性が十分にあると言えます。

また、通信プロトコルのカバー範囲も拡張の余地があると言えるでしょう。インターネットと親和性の高いgRPCやMQTTのようなプロトコルは今後追加されるかもしれません。

まとめ

前編と後編の2回に分けて、Denoの標準モジュールについて解説しました。

おすすめ記事

記事・ニュース一覧