虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Deno 1.21 へのアップデートと変更事項まとめ

皆さんこんにちは。おっくんです。

去る 2022 年 4 月 21 日に Deno 1.21 がリリースされました。 今回も、リリースノートを参考に 変更事項の気になるところを紹介します。

Deno 1.21

Deno 1.21 での変更事項をDeno 1.21 リリースノートを元に確認します。

deno check コマンドの追加と deno run 時の型チェックの無効化へ

Deno 1.21 では、deno check コマンドが追加になります。 これは、ソースコードの「型チェックだけ」を行うコマンドです。

例えば、あえて型チェックをパスしない次のソースを用意します。

[src1.ts]

interface User {
  id: number;
}

const u: User = { name: "User1" };

console.log(u.id);
$ deno check src1.ts
Check file:///usr/src/app/src1.ts
error: TS2322 [ERROR]: Type '{ name: string; }' is not assignable to type 'User'.
  Object literal may only specify known properties, and 'name' does not exist in type 'User'.
const u: User = { name: "User1" };
                  ~~~~~~~~~~~~~
    at file:///usr/src/app/src1.ts:10:19

Deno はこれまで、実行時に(--no-checkをつけない限り)型チェックが行われていました。 この動作がこれから変更され、deno run を実行するときにデフォルトは型チェックを行わないようになっていきます。 このデフォルトの型チェックがなくなるのに対応し、明示的に型チェックを行うようにしたのが、deno check です。

これまでの deno run のタイミングで常に型チェックをする動作は、良いユーザー体験に寄与してきた反面、アプリケーションの起動パフォーマンスが遅くなる原因になっていると考えたようです。 また、実行時に型チェックをせずとも開発者は型チェックの診断結果を IDE で見ることができ、実行時の型チェックは役に立っていなくとも必要な「待ち時間」になっていることも判断の理由になったようです。

破壊的変更であると判断されているため、ゆっくりとデフォルトでタイプチェックをしない Deno への移行を進めることが宣言されています。

いち早くこの設定が提供された状態を確認するには、環境変数 DENO_FUTURE_CHECK=1 を設定することで確認できるようになっています。

$ DENO_FUTURE_CHECK=1 deno run src1.ts

deno check で行われる型チェックの範囲は、これまでの deno run --no-check=remote と同様の外部モジュールはチェックしないというものです。 外部モジュールを含め型チェックをおこなう場合には、deno check --remote とする必要があります。

globalThis.reportError が追加

このリリースで、reportError が追加になっています。 reportError は、Deno 独自のものではなく Web 標準の API です。

mdn web docs - reportError()

実行例は次の通りです。

[src2.ts]

reportError(new Error("something went wrong!")); // <= 終了コード0以外で終了する

[src3.ts]

// グローバルなErrorイベントとしてキャッチできる
window.onerror = (e) => {
  e.preventDefault(); // <= e.preventDefault(); しない場合、終了コード0以外で終了する
  console.error("We have trapped an uncaught exception:", e);
};

reportError(new Error("something went wrong!"));

REPL のアップデート

REPLを起動する deno repl--eval-file フラグが追加されました。 --eval-file を使うことで、対話的動作に入る前に実行させたいファイル(と URL)を指定できます。

実行すると次のようになります。

$ cat message.ts
function message(str: string) {
  console.log(`message:${str}`);
}

$ deno repl --eval-file=message.ts
Deno 1.21.0
exit using ctrl+d or close()
> message("Hello!")
message:Hello!

外部モジュールを試すときなどに REPL の開始の都度読み込み直す操作をシュートカットできるようになるなど、有効に使えそうです。

また、過去のコンソール表示を消す clear() が、追加されました。

deno bench がアップデート

Deno 1.20 で登場した deno bench は、対象のコードのパフォーマンスを測定することができます。

コミュニティの意見として、

  • 反復回数が少ない
  • レポートの情報が少ない

というものがあり、改善を行ったそうです。

Deno.BenchDefinition.nDeno.BenchDefinition.warmup の 2 つが削除され、ベンチマーク結果が統計的に有意でなくなるまで繰り返し実行するようになりました。

また、group と baseline のオプションが増えました。 比較してみます。

[src5.ts]

function plus(a: number, b: number): number {
  return a + b;
}

// 詳細な設定
Deno.bench({
  name: "function plus 1",
  fn: () => {
    plus(1, 2);
  },
});

// 詳細な設定
Deno.bench({
  name: "function plus 2",
  fn: () => {
    plus(11111, 22222);
  },
});

これを実行すると次のようになります。

$ deno bench --unstable src5.ts
Check file:///usr/src/app/src5.ts
cpu: Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz
runtime: deno 1.21.0 (x86_64-unknown-linux-gnu)

file:///usr/src/app/src5.ts
benchmark            time (avg)             (min … max)       p75       p99      p995
------------------------------------------------------- -----------------------------
function plus 1  173.84 ns/iter  (154.2 ns … 380.59 ns) 172.42 ns 310.29 ns 357.51 ns
function plus 2  185.47 ns/iter (170.76 ns … 402.74 ns) 185.65 ns 305.15 ns 326.81 ns

続けて、追加された group と baseline を設定します。

[src5.ts(修正)]

function plus(a: number, b: number): number {
  return a + b;
}

Deno.bench({
  name: "function plus 1",
  group: "plus",
  //baseline: true, 
  fn: () => {
    plus(1, 2);
  },
});


Deno.bench({
  name: "function plus 2",
  group: "plus",
  baseline: true, 
  fn: () => {
    plus(11111, 22222);
  },
});
$ deno bench --unstable src5.ts
Check file:///usr/src/app/src5.ts
cpu: Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz
runtime: deno 1.21.0 (x86_64-unknown-linux-gnu)

file:///usr/src/app/src5.ts
benchmark            time (avg)             (min … max)       p75       p99      p995
------------------------------------------------------- -----------------------------
function plus 1  166.76 ns/iter    (154.35 ns … 848 ns) 164.38 ns 224.98 ns 268.12 ns
function plus 2  183.11 ns/iter (168.72 ns … 732.51 ns) 182.15 ns 248.97 ns 254.98 ns

summary
  function plus 2
   1.1x times slower than function plus 1

設定した group: "plus" に基づいて、summary が追加されました。 baseline: truefunction plus 2 に適用したことで、function plus 1 との比較結果が示されています。 baseline: true の設定が無い場合は、同じグループで先に記述されているものが基準として表示されます。

サブプロセス用の新しい unstable なAPIが追加されました。

Deno 1.21 では、Deno名前空間に新しいサブプロセスAPIを追加します。 コミュニティから寄せられた Deno.run へのフィードバックが反映されているそうです。

高レベル API Deno.spawn

Deno.spawn は、Deno.run と同じオプションを取り、Promiseを返す関数です。 Promiseは、実行結果と出力結果を含みます。 利用例は次のようになります。

[src6.ts]

const { status, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

次のように実行できます。

$ deno run --unstable --allow-read --allow-run=/usr/bin/deno src6.ts

低レベル API Deno.spawnChild

Deno.spawnChilddeno.run と同じオプションを取りますが、Deno.spawn と異なり、返すのは Deno.Process です。

Deno.spawnChild の使用例として紹介されているのが、次のコードですが、動作確認できませんでしたので、内容だけ紹介します。

const child = Deno.spawnChild(Deno.execPath(), {
  args: [
    "eval",
    "console.log('Hello World')",
  ],
  stdin: "piped",
});

// open a file and pipe the subprocess output to it.
child.stdout.pipeTo(Deno.openSync("output").writable);

// manually close stdin
child.stdin.close();
const status = await child.status;

同期的 API Deno.spawnSync

Deno.spawnSync は、同期的な動作をする Deno.spawn です。

[src7.ts]

const { status, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

実行時は次の通りです。同期的な動作をするだけですので、Deno.spawn の実行時と変わりません。

$ deno run --unstable --allow-read --allow-run=/usr/bin/deno src7.ts

新しいサブプロセスAPIが増えるという紹介でしたが、既存の Deno.run は近々のリリースで非推奨になるそうです。

deno test がアップデートされました

deno test にかかわるアップデートや、標準モジュールの拡充がされました。

ユーザー定義の出力の改善

これまで deno test を実行した際に、テストコード中のコンソール出力とテスト結果出力が混ざっていました。 Deno 1.21 からは、ユーザーが定義したコンソール出力をマーカーで囲むようになりました。

[src8.ts]

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

function plus(a: number, b: number): number {
  return a + b;
}

Deno.test("plus function",()=>{
  const result  = plus(1,1)
  console.log(result)
  assertEquals(result,2)
})

実行すると次のようになります。

$  deno test src8.ts
running 1 test from ./src8.ts
plus function ...
------- output -------
2
----- output end -----
ok (4ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (17ms)

デバッグのため、マーキングする関数や、都度マーキングする処理を書く必要がなくなったのはありがたい限りです。

エラーとスタックトレースが読みやすくなりました

deno test の実行時、エラーのスタックトレースをすべて出力をしていましたが、エラーの発生個所を示すようになります。

BDDスタイルのテストが標準モジュールに追加されました

BDD(ビヘイビア駆動開発)スタイルでテストを記述できるモジュールが追加されました。

次のようにサンプルが紹介されています。

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

describe("User", () => {
  it("constructor", () => {
    const user = new User("John");
    assertEquals(user.name, "John");
    assertStrictEquals(User.users.get("John"), user);
    User.users.clear();
  });

  describe("age", () => {
    let user: User;

    beforeEach(() => {
      user = new User("John");
    });

    afterEach(() => {
      User.users.clear();
    });

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

    it("setAge", function () {
      user.setAge(18);
      assertEquals(user.getAge(), 18);
    });
  });
});

実行すると次のようになります。

$  deno test bdd.ts
running 1 test from ./bdd.ts
User ...
  constructor ... ok (4ms)
  age ...
    getAge ... ok (3ms)
    setAge ... ok (3ms)
  ok (8ms)
ok (16ms)

test result: ok. 1 passed (4 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (31ms)

Deno 1.18 公開時には、BDDスタイルのテストを行うポリフィルが公開されていました

見比べて見ると内容はだいぶ異なっているものになっています。

モックユーティリティが標準モジュールに追加されました

外部APIの呼び出しなどを行うソフトウェアのテストには、その外部APIを制御できないことを理由としたテストの困難さが付きまといます。 こういった状況の解決策の1つは、モックを使用して動作のエミュレーションをすることです。 このモック用のユーティリティとして spymock の2つをサポートするようになりました。

spy

spy は、対象とする関数の呼び出しの回数や、呼び出された状況について問い合わせることでテストができます。

リリースノートでは、spy 関数の動作だけ見ていますが、モジュールのREADMEの解説のほうがより利用実態に即している内容になっているので、こちらを参考に紹介します。

[src10.ts]

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

function plus(a: number, b: number) {
  return a + b;
}

function multi(plusFn: (a: number, b: number) => number, a: number, b: number) {
  let tmp = a;
  for (let i = 1; b > i; i++) {
    tmp = plusFn(tmp, a);
  }
  return tmp;
}

Deno.test("how spy works", () => {
  // 関数 plus を spy する関数を作成
  const plusSpy = spy(plus);

  // 関数 plusSpy を実行し、テストを実行
  assertEquals(plusSpy(2, 2), 4);

  // 関数 plusSpy の呼び出しの状況と結果についてテストを実行
  assertSpyCall(plusSpy, 0, {
    args: [2, 2],
    returned: 4,
  });

  assertEquals(multi(plusSpy, 2, 3), 6);

  // 関数 multi 内で plusSpy が呼び出された状況と結果についてテストを実行
  assertSpyCall(plusSpy, 1, {
    args: [2, 2],
    returned: 4,
  });
  assertSpyCall(plusSpy, 2, {
    args: [4, 2],
    returned: 6,
  });

  // 関数 plusSpy の呼び出された回数についてテストを実行
  assertSpyCalls(plusSpy, 3);
});

spyする対象の関数を関数の外から渡すことでしか計測ができないので、少々使い方を悩みそうです。

stab

stab は、テストの対象になる関数の動作を事前に定義しておくことができます。 ここでは、利用例として fetch のスタブを作り、使用してみます。

[src11.ts]

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

async function externalCall(){
  const result = await fetch("https://example.com/api/users/exist/1")
  const resultJson = await result.json();
  return resultJson
}

Deno.test("fetch stub #1", async () => {
  const fetchStub = stub(globalThis, "fetch", returnsNext([new Response('{"result":true}', {status: 200,})]));
  try {
    const result = await fetch("https://example.com/api/users/exist/1")
    const resultJson = await result.json();

    assertEquals(resultJson.result, true);
  } finally {
    fetchStub.restore();
  }
});

// Deno.test 外で宣言された関数の中の fetch もモックの内容で動作している
Deno.test("fetch stub #2", async () => {
  const fetchStub = stub(globalThis, "fetch", returnsNext([new Response('{"result":true}', {status: 200,})]));
  try {
    const resultObject = await externalCall()

    assertEquals(resultObject.result, true);
  } finally {
    fetchStub.restore();
  }
});

この mock 関数を使って fetch 関数のモックを作ることができました。 spy と比較して、内部で使用される関数を外から渡してあげる必要も無く動作するので、非常に使いやすい印象です。

スナップショットテストが標準モジュールに追加されました

このリリースから複雑な出力を伴うテストをするためのツールとして、スナップショットテストが標準モジュールに導入されます。

リリースノートに従い試してみます。

[src12.ts]

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

function getKeyString(){
  return "gsb7UGJmas6y6J-Vj4yLJs-cJgKiwGPKStas8JC4kCyrusmwJzGUSCDh8JxsA4dzH2zNc6hJyQgSbNQRW3JThjBGgu2cYm_VjMQT"
}

Deno.test("snapshot test", async (t) => {
  const output = getKeyString();
  await assertSnapshot(t, output);
});

実行は次のように2段階の手順が必要です。

# 第1段階 基準になるスナップショットを作成
$ deno test --allow-read --allow-write ./src12.ts -- --update
snapshot test ... ok (18ms)

 > 1 snapshots updated.

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (48ms)
# 実行後./__snapshots__/src12.ts.snap が作成されています。

# 第2段階 スナップショットテストを実行
$ deno test --allow-read --allow-write ./src12.ts
Check file:///usr/src/app/src12.ts
running 1 test from ./src12.ts
snapshot test ... ok (10ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (28ms)
# 関数 getKeyString の出力結果に差が無いのでテストにパスします。

# 関数 getKeyString が返す文字列を変更し、再度実行します
$ deno test --allow-read --allow-write ./src12.ts
Check file:///usr/src/app/src12.ts
running 1 test from ./src12.ts
snapshot test ... FAILED (13ms)

failures:

./src12.ts > snapshot test
AssertionError: Snapshot does not match:


    [Diff] Actual / Expected


-   "gsb7UGJmas6y6J-Vj4yLJs-cJgKiwGPKStas8JC4kCyrusmwJzGUSCDh8JxsA4dzH2zNc6hJyQgSbNQRW3JThjBGgu2cYm_VjMQ"
+   "gsb7UGJmas6y6J-Vj4yLJs-cJgKiwGPKStas8JC4kCyrusmwJzGUSCDh8JxsA4dzH2zNc6hJyQgSbNQRW3JThjBGgu2cYm_VjMQT"


    throw new AssertionError(message);
          ^
    at assertSnapshot (https://deno.land/std@0.136.0/testing/snapshot.ts:138:11)
    at async file:///usr/src/app/src12.ts:9:3

failures:

        ./src12.ts
        snapshot test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (30ms)

error: Test failed
# 出力内容が異なっているのでエラーになります。

Deno を使ったWebアプリケーションフレームワークも活発に開発されている昨今、出力内容も複雑になってきている故、この機能のお世話になることはより増えるのではと思います。

FakeTime テストユーティリティが標準モジュールに追加されました

時間に関わるテストは、システムに依存するために難しいことがあります。 標準モジュールに、こういった時刻に関わるテストを行うためのユーティリティが標準モジュールに追加されました。

リリースノートでは、次のサンプルが紹介されています。

import { FakeTime } from "https://deno.land/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    assertEquals(Date.now(), 1640908800000);
  } finally {
    time.restore();
  }
});

最後に実行されている .restore() を必ず実施するように注意の案内がありました。

時刻の取得だけではなく時間の経過も FakeTime モジュールでエミュレーションができます。

import { FakeTime } from "https://deno.land/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    let cnt = 0;
    // Starts the interval
    setInterval(() => cnt++, 1000);

    time.tick(500);
    assertEquals(cnt, 0);

    // Now 999ms after the start
    // the interval callback is still not called
    time.tick(499);
    assertEquals(cnt, 0);

    // Now 1000ms elapsed after the start
    time.tick(1);
    assertEquals(cnt, 1);

    // 3 sec later
    time.tick(3000);
    assertEquals(cnt, 4);

    // You can jump far into the future
    time.tick(997000);
    assertEquals(cnt, 1001);
  } finally {
    time.restore();
  }
});

非常に強力なツールでした。

その他

  • インタラクティブなプロンプトを無効にする環境変数 DENO_NO_PROMPT が追加されました
  • unstable な API の改善
    • Deno.upgradeHttp が、Unix ソケットをサポートするようになった
    • Deno.Listenerref()unref() メソッドが追加
  • deno fmtdeno lint が、高速になりました

まとめ

Deno 1.21 は、これから適用される deno run 時の型チェックのデフォルト無効化に伴う deno check の登場、テスト関連機能の拡充が目立っていました。

以前「deno run 実行時の型チェックをデフォルトでは止める 」というissueについて紹介していました。
IDEで型チェックの結果を受け取れているのに、起動時間を犠牲にして都度型チェックが行われているよりも、分離したコマンドになったのは良かったとも感じます。 これについては、Deno は、フィードバックを求めているので、何か有れば貢献するのも良いかと思います。

テスト機能の拡充は、「書きたいと思ったテストを標準モジュールだけで達成できる」という状況により近づいていると感じます。

引き続き、リリースノートを追いかけます。

P.S.

採用情報

■募集職種
yumenosora.co.jp