更新日:
投稿日:

Jestで無限ループ処理をテストするときにループから抜けられなくて困ったのでメモ。

参考までにこの記事ではイベントループのように脱出手段のある無限ループを想定しています。

確認環境

Env Ver
@types/jest 29.5.7
jest 29.7.0
typescript 5.2.2

サンプルコード

テスト対象のコード

import { getHandle } from './lib/get-handle';

export const main = () => {
  for (;;) {
    const handle = getHandle();
    if (handle === 'Click') {
      console.log('click');
    } else if (handle === 'KeyDown') {
      console.log('keydown');
    } else {
      break;
    }
  }
};

テストコード

.mockReturnValue()ではなく.mockReturnValueOnce()を使うのが肝です。

.mockReturnValue()だと同じ値が毎回返るので無限ループから抜けられませんが、.mockReturnValueOnce()は一回だけなので抜けられます。チェーンすると二回目、三回目の返り値も設定できます。

なお、以下の例では.mockReturnValueOnce()をチェーンさせていますが、させなくてもテストとしては正常に動作します。指定がない場合はundefinedが返るようです。(基本的には明示的に指定しておいた方が良いと考えています。

import { main } from '.';
import * as getHndl from './lib/get-handle';

jest.mock('./lib/get-handle');

describe('main', () => {
  it('Clickイベントの分岐に入ること', () => {
    jest
      .spyOn(getHndl, 'getHandle')
      .mockReturnValueOnce('Click')
      .mockReturnValueOnce('');

    const spiedConsoleLog = jest.spyOn(console, 'log');

    main();

    expect(spiedConsoleLog).toHaveBeenCalledWith('click');
  });
});

余談

.toHaveBeenCalledWith().toEqual()同じロジックで判定しているらしいので、.toStrictEqual()版もあると便利な気がしました。気が向いたらPR出してみようかな…。

参考

ESM化が叫ばれて久しいですが、未だにJestはESMとCJSが混在したコードを処理してくれません。

Getting StartedにもSWCに対する言及がないので、きっともう忘れられているのでしょう。swc-project/jestの方も特にやる気はなさそうだし、やりたければ自分でPR書きましょうって感じだと思います。きっと。

確認環境

node_modules配下にESMで作られたモジュールが存在し、コードはTypeScript、トランスパイルにはSWCを利用する。

Env Ver
@swc/cli 0.1.62
@swc/core 1.3.92
@swc/jest 0.2.29
@swc/register 0.1.10
jest 29.7.0
typescript 5.2.2

やったけど意味がなかったこと

  • package.jsontypemoduleにする
  • jest.config.jstransformIgnorePatternsにESMモジュールのパスだけ除外する設定を書く
  • 上記に加えてtransform.jsc.pathpkg-name: ['node_modules/pkg-name']を追記する
  • node --experimental-vm-modules node_modules/jest/bin/jest.jsで実行する
    • 多少マシになったがコケるものはコケる
  • ESMで書かれたモジュールを丸ごとモックする
    • 一切効果なし

所感

多分Webpackでバンドルしてnode_modulesの中身も外も関係ない状態にするのが一番無難なのではないかと思いました。

Node.jsの組み込みテストランナーにすれば解決するかな?と思ったものの、こちらは現状SWCでは使えそうにないので諦めました。
参考までに以下のコマンドで走らさせられます。

node --require @swc/register --test ./src/**/*.spec.ts

取り敢えずESMに引っかったモジュールはCJS時のバージョンを維持しておくことにしましたが、このままだとSWC使えないし、なんとかなって欲しいですね。Webpack使えば解決できるのはわかるんですが、このために使いたくないので、テストを重視する場合、Vitestを持つViteが有力候補になって来そうです。

2024-02-17追記

esbuild + Node.js built-in test runnerの組み合わせであればテストはできるが肝心の実行ができず無意味だった

更新日:
投稿日:

今の職場でコードを書いていてifに対して原則elseを書かないという雰囲気があるのですが、個人的にはelseを書きたいのでその話です。

世間的にelseを書かないというコーディングルールは一定の支持があるようで、まぁ一種の宗教だとは思っていますが、個人的にはどうなんかなと思っています。まぁ割と最近の流行りな気もしていますが…。(昔はなかったと思う…というとアレですが)

elseがあってほしい理由

else ifelseブロックがあると処理の関連性が見やすくなると考えています。

処理の関連性がわかりやすくなる

例えば以下のコードの場合、ifが連続しているよりelseがある方が処理の関連性が明示的に見えると思います。elseで繋げないことは基本的に処理として関連性がないはずです。

// ifのみ
export const hoge = (mode: 1|2|3) => {
  if (mode === 1) {
    // なんかの処理
    return 'hoge';
  }
  if (mode === 2) {
    // なんかの処理
    return 'piyo';
  }
  if (mode === 3) {
    // なんかの処理
    return 'fuga';
  }
}

// elseあり
export const piyo = (mode: 1|2|3) => {
  if (mode === 1) {
    // なんかの処理
    return 'hoge';
  } else if (mode === 2) {
    // なんかの処理
    return 'piyo';
  } else if (mode === 3) {
    // なんかの処理
    return 'fuga';
  }
}

予期せぬ不具合が入り込みづらくなる

ifだけで構成されたコードでは以下のようにifブロックの外に処理を入れ込むことができますが、このときA1, B1, C1のifの外にある処理は後続処理に影響します。しかしこの書き方だと処理の影響範囲が見積もりづらく、不必要なバグを生む元になるため、個人的には避けたいと考えています。(他の処理の作用に引っ張られているので、副作用のある実装ということになると思っています)

またA1, B1, C1の行数が伸びるとコードの見通しが悪くなり、if同士の関係性が分かりづらくなります。

elseを使うとこういった副作用的な振る舞いをする実装が作りづらくなり、影響範囲もブロックインデントで分かれるので、そういった心配がなくなります。

// ifのみ
export const hoge = (mode: 1|2|3) => {
  // なんかの処理 A1
  if (mode === 1) {
    // なんかの処理 A2
    return 'hoge';
  }
  // なんかの処理 B1
  if (mode === 2) {
    // なんかの処理 B2
    return 'piyo';
  }
  // なんかの処理 C1
  if (mode === 3) {
    // なんかの処理 C2
    return 'fuga';
  }
}

elseが必要なのに書き忘れることが減る

elseを書かないことに捕らわれていると以下のようなコードが生まれることがあります。

さてこのコードにおいて!validHoge(hoge) || !hasPiyoのケースはどのように処理されるのでしょうか?答えは処理されませんが、もしこのコードに対してUnit testが実装されておらず、特段のドキュメントもなければ、それが正しいのかどうかをコードから読み取ることが出来ません。

const isValidUsername = (username: string, hasInputed: boolean) => {
  if (validUsername(username) && hasInputed) {
    return requestSearchUsername(username);
  }
}

せめてこう書いてあれば判断も付くでしょう。

const isValidUsername = (username: string, hasInputed: boolean) => {
  if (validUsername(username) && hasInputed) {
    return requestSearchUsername(username);
  } else {
    // 何もしない
  }
}

因みにこの処理はユーザー登録フォームでユーザー名の書式が正しければ既に存在するユーザー名かどうかをAPIに問い合わせ、既に存在すればエラーメッセージを、存在しなければtrueを返すという内容ですが、そもそも書式が不正である場合は何もしないため、ユーザーはエラーであることを知ることが出来ません。

もしelseブロックを書いていれば考慮漏れに気づけた可能性もあったのではないかと思います。

elseを使うケースを考えなくて良くなる

ifに対して原則elseを書かないというルールがあってもelseを書かないと成り立たないケースは存在します。そう言った場合に原則ifしか書いてはならないというルールがあるとelseを書いてよいかどうか考える必要が出てきますが、元からelseも書いて良いルールであればそこは考える必要がありません。個人的に判断の余地が生じるコーディングルールはチーム開発ではない方が良いと考えています。

またこのルールの結果、本来必要だったelseを書かなかったことにより不具合が発生する可能性もあります。(実装者が軽率にelseを使わなかったことによる判断ミス)

そもそも何故elseを書いてはならないのでしょうか?コードのネストが深くなるからでしょうか?少なくともelse if相当のifには、その作用はありません。あるとしたら純粋なelseブロックはインデントが減るでしょう。しかしこのインデントはあったほうが処理の関連性が掴みやすくなると思います。

elseのないifは悪か?

ここまで散々elseを書くべきと言ってきましたが、ではelseがないifは悪かと言うと、そうは思いません。例えば、以下のような早期リターンと呼ばれる記法であればそれは良いと考えています。では何を早期リターンとするかですが、個人的には例外的に処理を継続しない場合を一つの基準にしています。私もここの感覚は上手く言語化できていないのですが、恐らく何もしないときだけelseを書かないのは問題ないと考えています。

これは本質的ではない処理のせいで、いたずらにネストが増えるのを防ぐためです。関連性がある場合にelseを使うのがコードのメリハリとして目視で読んだときの認知性の向上に繋がると考えています。

export const putLog = (message: string) => {
  if (message === '') return;

  console.log(message);
}

この点については個人的に共感できる意見があったので以下の記事を紹介させて頂きます。

else句を使わないのが良いコードなの?いや、そんなはずは・・・ · DQNEO日記

記事中にある以下の部分、特殊ケースに対してガード節を使うというところで、例外ケースでのみ早期リターンをするという部分がありますが、やはりこれが一番無難だなと思いました。

条件記述の単純化 「ガード節による入れ子条件記述の置き換え」
特殊ケースに対してガード説を使う

ただ例外ケースとは何か?それは数値化でき、チーム開発で標準的に取り扱えるものなのか?と言われると、正直私もそこまでは言語化できていないので難しい部分ではあります。そもそも人間が書くコードが完全に均一になることはないと考えているので、そこまで強い思想を持って考えてはいないです。(もしChatGPTに書かせてもバラツキは出るでしょう)

私がこの記事で言いたいのは単にelseを書いてもいいのではないかということと、その場合でも早期リターンは許容しても問題ないだろうというところなので、一旦早期リターンの基準についてはここでは考えないこととします。

そうなると結局elseなしでifだけあればいいじゃないかという話に戻ってくるとは思うのですが、正直そこは常識で考えてほしいというところです。まぁ常識も人の数だけあるので難しいですし、そんな物があればこんな記事も生まれていないわけで、ソフトウェア開発は難しいですね。

更新日:
投稿日:

なんとなくアプリを作りたくなったので。Hello worldするとこまで。

確認環境

Env Ver
Windows 11 Pro 22H2
Intel VT-x 有効
Hyper V 有効

手順

  1. 公式サイトからインストーラーを落としてくる
  2. NextとAgree連打でインストールする
  3. Empty Activityを選ぶ
    1. 作成するプロジェクトはEmpty Activityを選ぶ
  4. Tools→Device Managerでデバイスがなんか入ってるのを確認する
    1. Tools→Device Managerでデバイスがなんか入ってるのを確認する
  5. 再生ボタンみたいなやつを押す
    1. 再生ボタンみたいなやつを押す
  6. しばらくするとエミュレーターが起動するのでドロワー中央のAndroidアプリを起動
    1. ブランクのアプリみたいなアイコンをタップ
  7. 画面が出てくればOK
    1. Hello worldが表示される
更新日:
投稿日:

Groovy Scriptの読み方が分からなかったので読み解き方のメモ。全て憶測

確認環境

Env Ver
Jenkins 2.249.1
Groovy Script 不明

サンプルコード

def credentials = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
    org.jenkinsci.plugins.plaincredentials.StringCredentials.class,
    jenkins.model.Jenkins.instance.getItemByFullName("ここにフォルダパス")
)

def cred = credentials.findResult { it.id == "ここに取得したいやつのCredentials ID" ? it : null }

読み解き方

個人的な解釈なので特に根拠はない。全て憶測。

lookupCredentialsの部分

端的に言うとデータ型とストア名のようなものを指定し、認証情報を取得する機能であると思われる。Groovy Script的にはCollectionが返ってくる。

findResultの部分