Markdownでテスト仕様書を書けるGaugeを試してみる

E2EテストやATDDについて調べていたら、 Markdown でテスト仕様書を書ける Gauge を知り、ちょっと試してみたのでメモ。

環境

Gauge v1.1.5。テストは Node.js v14.15.0 で記述。

Gauge とは

インテグレーションテスト用の仕様 (Specification) を Markdown で記述し、それに紐づけたテストを実行して結果などを管理するフレームワーク

テスト仕様書を Markdown で記述することで、読み取りが容易となり、再利用性を高められ、また実装を切り分けられる。

テストの実装は、開発元が同じ TaikoSelenium 、またツールを使わずスクレイピングしたりと任意。言語も複数選択可能。

インストール

公式のインストールページを参照。

Windows の場合、Chocolatey でもインストールできるが、バージョンが古いため、公式の手順でインストールを推奨。

2020/11/15 時点で、Chocolatey でインストールできる Gauge は v1.1.1 だが、リリース済みバージョンは v1.1.5。v1.1.1 では、 VSCode からプロジェクトを作成しようとするとエラーが発生する。 ( gauge template が実行されるが、 v1.1.1 時点で template コマンドが未実装)

エディタやIDEとの連携用のプラグインがあるが、公式のインストールページでは、Visual Studio Code しか選択できない。 Intellij IDEA 用のプラグインはあるが、Eclipse 用のプラグインサードパーティ製のものしかない模様。

使い方

前述のインストールページで指定した OS, Language, IDE/Editor で、それ以降の説明が変わる模様。

プロジェクトの作成

https://docs.gauge.org/getting_started/create-test-project.html

VSCode からテスト用のプロジェクトを作成。

コマンドパレットに create project を入力し、 Gauge: Create a new Gauge Project を選択。

コマンド gauge template --list --machine-readable を実行し、言語と説明、テンプレートの ZIP ファイルの URL を JSON で取得していた。

v1.1.5 の時点で、言語およびプロジェクト管理ツールや Selenium との組み合わせで 12 種類。

今回は js を選択。

続いて展開先のディレクトリ選択、最後にプロジェクト名の入力となる。

「展開先ディレクトリ/プロジェクト名」にテンプレート ZIP ファイルが展開され、VSCode の新しいウィンドウで開かれる。

js プロジェクトの場合、ウィンドウが開くまでに npm install が実行されるため、そのまま実行が可能。

Specification の実行

https://docs.gauge.org/getting_started/running-a-specification.html

specs/example.spec を開くと、Markdown 形式のファイルとなっており、最上部に Run Spec | Debug Spec が表示されている。

Run Spec をクリックすると、 npm test が実行され、あらかじめ用意されている spec の実行が可能。

テスト結果の確認

https://docs.gauge.org/getting_started/view-a-report.html

reports/html-report/index.html に、テストレポートが出力される。

Specification の記述

https://docs.gauge.org/writing-specifications.html

Markdown でテスト仕様書を記述できる。

ファイル拡張子は .spec が仕様、 .cpt がコンセプトとなり、 specs/ ディレクトリ配下に保存する。

保存先ディレクトリは env/default/default.properties で変更可能。詳しくはこちら

見出しとシナリオ

h1 タグで仕様の見出しを、h2 タグでシナリオ名を記述。シナリオ配下に、ワークフローを記述していく。

データテーブル

見出しと最初のシナリオの間に、テーブルを記述すると、データテーブルとして扱える。

後述のステップで <テーブルヘッダ名> を記述すると、そのヘッダ名の列の値を動的パラメータとして扱える。

また、シナリオ単位でテーブルを記述してパラメーター化テストを行うこともできるが、実験的機能の模様。

ステップ

順序なしリストをシナリオ配下に記述すると、それぞれがステップとなる。ただ、VSCode 上では * しかステップとして判断されず、 -|+ ではエラーとなる模様。

ステップに " 2 つ、または <> で囲んだ文字列を記述すると、実装コードに引数として渡される。

このため、 " , < , > を単純なテキストとしては記述できない。

" 2 つで囲まれた文字列は静的パラメータとなり、そのまま実装コードに渡される。

<> で前述のデータテーブルのヘッダ名を囲むと動的パラメータとなり、データテーブルから値を取得して実装コードに渡される。

タグ

見出しやシナリオには、 Tags: タグ名[, タグ名] でタグ付けが可能。

記述例では、見出しには Tags: search, admin としてそのテストの分類や対象を、シナリオには成功を期待するテストに Tags: successful がつけられていた。

見出しに付けられたタグは仕様自体のタグとなり、その仕様に含まれるシナリオ全てにも付与される。

コンテキストステップ

見出しと最初のシナリオの間に、ステップを記述すると、コンテキストステップとなる。

各シナリオの実行前にコンテキストステップが実行される。

ティアーダウンステップ

最後のシナリオの後にアンダースコア 3 つ、 ___ で区切り、その後に記述したステップはティアーダウンステップとなる。

各シナリオの実行前にティアーダウンステップが実行される。

コメント

単純なテキストなど、前述の記述以外はすべてコメントとなる。

コンセプト

再利用可能な記述を拡張子 .cpt のファイルに記述可能。

h1 タグでコンセプトヘッダを記述する、そこに <パラメータ名> を含めることが可能。

その直下に順序なしリストでコンセプトステップを記述することで、仕様からコンセプトヘッダを呼び出すことが可能となる。

Taiko でのテストコード実装

Taiko は REPL (Read-Eval-Print Loop) で対話形式に実行可能。ブラウザには Google Chrome が使われる。

yarn global add taiko でインストールし、 taikoインタラクティブレコーダーを実行。

.apiAPI一覧を表示。

コマンドを実行後、 .code [ファイルパス] で実行コマンドを出力 (ファイルパス未指定ではコンソール出力)。

また、 .step で Gauge のステップとして実行可能なコードを出力できる。

.exit で終了。

ツールとしては、スマートセレクター動的コンテンツのロード待ちinterceptによるスタブ化・モック化などの特徴がある。

コマンドプロンプトで実行した場合、タブでのコード補完が効いたり、なかなか使い勝手はいいが、いかんせん情報が少ないのが難点か。

ステップとの紐づけ

js テンプレートの場合、 step('ステップに記述したテキスト', async <function>); を記述すると、ステップから実行できる。

VSCode上で、Specification のステップに対応するテストがない場合はエラーとなったり、テストの step が何か所から実行されるかが分かるようになっている。

パラメータを使用している場合、該当部分を <> で囲み、関数の引数にできる。

実装例

以下の Markdownspec/search.spec に保存。

# 検索エンジンを開く

Tags: example, search

検索エンジンを開く

   |url                    |
   |-----------------------|
   |https://www.google.com/|
   |https://www.bing.com/  |
   |https://yahoo.com/     |

## 検索エンジンを開く

本当は、タイトルの確認くらいはしたかったが、Taikoでタイトルを取得する方法がわからなかったため、ページを開くだけにする

* URL <url> を開く

続いて、 tests/search.js にテストを保存して実行...したら、ブラウザ起動に失敗するようになった。

ファイル名にルールがあるのか、1ファイルしか書けないのか、原因がわからなかったため、デフォルトで用意されている tests/step_implementation.js に以下を追記。

step('URL <url> を開く', async (url) => {
  await goto(url);
});

この状態で、 gauge run specsnpm test を実行すると、テストが実行された。

振り返り

Gauge 自体は非常に使いやすい印象。開発前のユースケースの段階で、自然言語でテスト仕様を記述できる。

一方、 Taiko は情報が少なく、公式のドキュメントを読むしかない状態で、使いこなせれば高機能だと思うが、簡単なテストを書きたいレベルでは、やや扱いにくかった。複数ファイルを tests/ に配置してエラーになったのも、書き方が悪いのか設定で回避できるのか、はたまた仕様かの判断がつけられなかったが、仮に tests/step_implementation.js しか書けないのであれば、ちょっとテストファイルが肥大化しそう。(「テストランナーとの統合」を見ると、ファイル固定っぽい気がする)

既存のテストコードとしては、Selenium を使った PythonJavaのコードが多いので、他言語でのテストプロジェクト作成を試してみたい。

Selenide のような Page Object Pattern との相性は良さそうな気がする。

WindowsでGradleのbootRunがCreateProcess error=206になるときの対応方法と、Kotlinで記述している場合の注意点

開発端末がmacOSだけのプロジェクトに、ひとりWindowsアサインされた。

プロジェクト管理がGradle、フレームワークがSpring Boot、開発言語がJavaまたはKotlinだったので、さっそく gradlew.bat bootRun すると、 A problem occurred starting process 'command '...\java.exe'' というエラーが発生。

Gradle + Spring Bootではよくあることなので、対応方法をメモ。

さらに、JavaではなくKotlinで開発しているプロジェクトでは、別途問題が発生したので、そちらについてもメモ。

環境

Windows 10 64bit Pro Version 2004。

Windows(Win32 API)起因の問題のため、Gradle, Spring Boot, JavaやKotlinのバージョンは関係ないと思われる。

問題

対象のプロジェクトで gralde bootRun すると、以下のエラーが発生する。

* What went wrong:
Execution failed for task ':bootRun'.
> A problem occurred starting process 'command '${java.exeのパス}''

gradle bootRun -i -s でログとスタックトレースを出力すると、以下のようになる( Starting process の出力は改行している)。

> Task :bootRun FAILED
Excluding [com.google.protobuf:protobuf-java]
Task ':bootRun' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
Starting process 'command '${java.exeのパス}''.
 Working directory: ${projectDir}
 Command: ${java.exeのパス} ${-Dオプション...}
 -cp ${依存JARなどへのパス...}
 ${@SpringBootApplication を付けたクラスの完全修飾クラス名}
:bootRun (Thread[Task worker for ':' Thread 3,5,main]) completed. Took ... secs.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':bootRun'.
> A problem occurred starting process 'command '${java.exeのパス}''
* Try:
Run with --debug option to get more log output. Run with --scan to get full insights.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':bootRun'.
  // 中略
Caused by: org.gradle.process.internal.ExecException: A problem occurred starting process 'command '${java.exeのパス}''
  // 中略
Caused by: net.rubygrapefruit.platform.NativeException: Could not start '${java.exeのパス}'
  // 中略
Caused by: java.io.IOException: Cannot run program "${java.exeのパス}" (in directory "${projectDir}"): CreateProcess error=206, ファイル名または拡張子が長すぎます。
    at net.rubygrapefruit.platform.internal.DefaultProcessLauncher.start(DefaultProcessLauncher.java:25)
    ... 7 more
Caused by: java.io.IOException: CreateProcess error=206, ファイル名または拡張子が長すぎます。
    ... 8 more

最も深い例外は「CreateProcess error=206, ファイル名または拡張子が長すぎます。」。

コマンドプロンプトだけでなく、PowerShellやGit Bashから実行しても同様のエラーが発生する。

原因

-cp に大量のJARなどのパスが渡されることで、Win32 APICreateProcess 関数に渡せる文字列の、最大長を超過している模様。エラーになった時のコマンド全体の文字列長は、約57,000文字だった。

詳細は確認していないが、 What is the command line length limit? - The Old New Thing によると、文字数は最大32,767文字の模様。

The maximum command line length for the CreateProcess function is 32767 characters. This limitation comes from the UNICODE_STRING structure. CreateProcess is the core function for creating processes, so if you are talking directly to Win32, then that’s the only limit you have to worry about.

UNICODE_STRING (subauth.h) - Win32 apps | Microsoft Docs を見ると、バイト長を最大65,535の USHORT で保持しているので、1文字2バイトの小数部切り捨てで32,767文字かな?

対応

検索すると、 GRADLE_USER_HOME の位置を変えてパスを短くする、 pathingJar タスクを定義するなど、いろいろ対応方法が出てくる。

この問題解決用のGradle Plugin、 com.github.ManifestClasspath があるので、今回はそれを利用し、クラスパスを単一のJARファイルにまとめることで実行できるようにする。(プラグインの実装は確認していないが、AntのManifestclasspathタスクと同じことをしているのかな?)

ManifestClasspathプラグインのインストール、設定

Gradle Plugin PageGitHubにインストール方法が記述されている。

今回のプロジェクトでは、Gradle設定はGroovyで記述されていたため、 build.gradle に以下を記述。なお、 @SpringBootApplication を付与したクラスの完全修飾クラス名は、 hepokon365.Application としておく。

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE"
  }
}

apply plugin: "application" // mainClassName を有効化するため
apply plugin: "com.github.ManifestClasspath"
mainClassName = "hepokon365.Application"

この状態で gradle bootRun -i すると、 -cp ${projectDir}\build\mfjars\bootRun_ManifestJar.jar となり、引数が短くなっているのが確認できる。

コマンド全体の文字列長も約4,000文字となり、開発言語がJavaの場合は bootRun で起動可能となった。

Gradle Kotlin DSLの場合

Kotlin DSLの場合は以下の用意に記述する。

plugins {
    application
    id("com.github.ManifestClasspath") version "0.1.0-RELEASE"
}

application {
    mainClassName = "hepokon365.Application"
}

開発言語がKotlinの場合に発生しうる問題

Javaであればめでたしめでたしだが、開発言語がKotlinのプロジェクトでは、前述の対応をして bootRun すると、「エラー: メイン・クラス hepokon365.Application が見つからなかったかロードできませんでした」が発生。

対象のクラスが記載された Application.kt ファイルを見ると、Kotlin + Spring Bootの公式チュートリアルの記載と同様、以下のように記述されている。

package hepokon365

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

Kotlinでメインクラスが見つからない場合の対応

前述の build.gradle の設定では、 Application.main を実行しようとしているが、Kotlinでクラス外関数を記述した場合、 ${ファイル名}Kt クラスのstaticメソッドとしてコンパイルされていたのが原因。

gradle bootRun がプロセス実行エラーになっている際、 -i オプションでログを出していると、コマンドの末尾のメインクラス名がKt付きになっているのが確認できる。

対応方法としては、単純に build.gradlemainClassName に設定するクラス名を ${ファイル名}Kt に変更するか、 companion objectメンバ関数として main を宣言し、 @JvmStatic アノテーションを付与すればいい。

@SpringBootApplication
class Application {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            runApplication<Application>(*args)
        }
    }
}

既存環境への影響を考え、今回は mainClassName の変更で対応。

mainClassName = "hepokon365.ApplicationKt"

ちなみに、 @file:JvmMultifileClass@file:JvmName("Application") アノテーションでmain関数もApplicationクラスで実行できないかと思ったが、クラスが重複しているということでコンパイルエラーに。

振り返り

対応できなければmacを使わされるところだった、あぶないあぶない。

何回か書いている気もするが、Spring Boot使うときは、GradleよりMavenを使うのが無難だと思う。

あとは、Project Jigsawによるモジュール機能を使えば、よしなにできるのかな?

備考

Grailsフレームワークとして使用している場合は、 pathingJar が設定できる模様。

qiita.com

GradleでprocessResources時にシンボリックリンクを作成(Windows, macOS対応)

Spring Bootを使ったJavaプロジェクトで、 bootRun をしてから起動するまで数分かかっていた。

調べてみると、 processResources タスクで src/main/webapp/WEB-INFbuild/build/resources/main/WEB-INF にコピーしていたが、 src/main/webapp/WEB-INF 配下がNodeパッケージとなっており、node_modules配下のファイルコピーで時間がかかっていた。

取り急ぎ、シンボリックリンクを作って対応したのでメモ。mklinkについて調べたのはこのため。

環境

Java v8.262.10, Gradle v6.3。

Windows 10およびmacOSで動作確認済み。

シンボリックリンクの作成

build.gradle に以下を追加。

Windowsの場合、開発者モードを有効化するか管理者権限で mklink を実行しないとシンボリックリンクが作れないため、PowerShell経由で管理者権限で起動したコマンドプロンプトで、 mklink を実行している。

import java.nio.file.Files

final def isWindows = System.getProperty('os.name')
    .toLowerCase()
    .contains('windows')
final def srcDir = 'src/main/resources/WEB-INF'

task createWebInfSymbolicLink(type: Exec) {
  // Windowsの場合、スラッシュをバックスラッシュに置換
  // FilenameUtils#separatorsToSystem(String) などでもいい
  def normalizePath = { isWindows ? it.replaceAll('/', '\\\\') : it }
  def destDir = "${projectDir}/build/resources/main/WEB-INF"
  def destDirFile = destDir as File
  def normalizedSrcDir = normalizePath("../../../${srcDir}")

  if (destDirFile.directory) {
    def destDirPath = destDirFile.toPath()

    // シンボリックリンクかを判定
    if (Files.isSymbolicLink(destDirPath)) {
      // srcへのシンボリックリンクがあれば作業不要。
      // ExecタスクではExecSpecを返す必要があるため、適当にechoする。
      def destLink = Files.readSymbolicLink(destDirPath)
      if (normalizedSrcDir == destLink.toString()) {
        def executer = isWindows ? ['cmd', '/c'] : []
        return commandLine(executer + ['echo', 'exists link'])
      }

      // シンボリックリンクでも、パスが違っていれば削除
      destDirFile.delete()
    } else {
      // 実ファイルであればディレクトリを削除
      destDirFile.deleteDir()
    }
  }

  // Windows以外であれば、lnでシンボリックリンクを作成
  if (!isWindows) {
    return commandLine('ln', '-s', "${normalizedSrcDir}", "${destDir}")
  }

  // Windowsの場合、コマンドプロンプトからPowerShellを起動し、
  // さらにPowerShellから管理者権限のコマンドプロンプトでmklinkを実行。
  // 直接PowerShellを起動しても大丈夫な気がするが、
  // やったことないのでとりあえずコマンドプロンプト経由で。
  def arguments = [
      '/c', 'mklink', '/d', normalizePath(destDir), normalizedSrcDir
  ]
  commandLine 'cmd', '/c', 'powershell',
      '-Command', '"Start-Process -Verb Runas"',
      '-FilePath', '"cmd"',
      '-ArgumentList', arguments.collect { /"${it}"/ }.join(',')
}

// processResources の後に実行するよう設定
processResources.finalizedBy createWebInfSymbolicLink

振り返り

mklinkシンボリックリンクを作ろうとすると、やはり権限が必要になるのがネック。

今回の方法では管理者権限が必要になるので、 gradle clean などを実行した後だとUACのポップアップが出てしまう。

ディレクトリジャンクションを使って、管理者権限なしで実行できそうなので、そのうち改良したい。

Windowsでシンボリックリンクを作るにはmklinkが使えるし、管理者権限なしでフォルダにリンクを張りたいならジャンクションでいいかも

会社で「Windowsだとシンボリックリンクが作れない」という話になった。 mklink で作れると話したら驚かれたのでメモ。

導入が Windowsでシンボリックリンクを作る | Developers.IO と全く同じだなぁ...

環境

Windows 10 Pro 64bit v1909、コマンドプロンプト Version 10.0.18363.1139。

docs.microsoft.com

mklink [オプション] <link> <target> で、シンボリックリンクなどのリンクを作成する。

link はリンク作成先、 target はリンクが指す実際のファイル/ディレクトリパスを、絶対パスまたは相対パスで指定する。

オプションで作成するリンクの種類が異なり、それぞれでファイルまたはディレクトリの指定可否が決まる。

オプション 作成されるリンク target指定先
未指定 ファイルのシンボリックリンク ファイル
/d フォルダのシンボリックリンク ディレクト
/h ハードリンク ファイル
/j ディレクトリジャンクション ディレクト

targetに相対パスを指定する際の注意

target相対パスを指定する場合、オプションによって、パスの起点となるディレクトリ位置が異なる。

未指定または /dシンボリックリンクを作成する場合は、 ln と同様、 link からの相対パスとなる。

/h でハードリンクを作成、または /jディレクトリジャンクションを作成する場合、コマンド実行しているカレントディレクトリからの相対パスとなる。

リンクを作成するディレクトリをカレントディレクトリとすれば、どの方法でも同じ相対パスで指定できる。

コマンド例

以下のディレクトリ構成で、 link ディレクトリ直下にリンクを作る場合。

C:\root
  ┣ target
  ┃  ┗ src
  ┃    ┗ exp.txt
  ┗ link
root直下でコマンド実行
リンク種類 コマンド
ファイルのシンボリックリンク mklink link\symlink_file ..\target\src\exp.txt
フォルダのシンボリックリンク mklink /d link\symlink_dir ..\target\src
ハードリンク mklink /h link\hardlink.txt target\src\exp.txt
ディレクトリジャンクション mklink /j link\junction target\src
link直下でコマンド実行
リンク種類 コマンド
ファイルのシンボリックリンク mklink symlink_file ..\target\src\exp.txt
フォルダのシンボリックリンク mklink /d symlink_dir ..\target\src
ハードリンク mklink /h hardlink.txt ..\target\src\exp.txt
ディレクトリジャンクション mklink /j junction ..\target\src

リンクの表示と確認

エクスプローラーでの表示

ファイルのシンボリックリンクは、ファイルの種類が「.symlink」、ファイルサイズが0になる。アイコンはファイルへのショートカットと同様。

また、ディレクトリのシンボリックリンクディレクトリジャンクションは、アイコンはフォルダへのショートカットと同じだが、ファイルの種類は「ショートカット」ではなく「ファイル フォルダー」となる。

f:id:hepokon365:20201024204000p:plain
コマンド例で作成したリンクの、エクスプローラーでの表示

dirコマンドでの表示

dir コマンドでの属性には、以下のように表示される。

また、シンボリックリンクディレクトリジャンクションには、リンク先のファイルパスが表示される。

C:\root\link>dir /o:gn
YYYY/MM/DD  HH:MI    <DIR>          .
YYYY/MM/DD  HH:MI    <DIR>          ..
YYYY/MM/DD  HH:MI    <JUNCTION>     junction [C:\root\target\src]
YYYY/MM/DD  HH:MI    <SYMLINKD>     symlink_dir [..\target\src]
YYYY/MM/DD  HH:MI       %FILE_SIZE% hardlink.txt
YYYY/MM/DD  HH:MI    <SYMLINK>      symlink_file [..\target\src\exp.txt]

相対パスでリンクを作成すると、シンボリックリンクの場合は ln と同様、相対パスを保持しているが、ディレクトリジャンクションの場合は絶対パスに変換される模様。

相対パスで作成したシンボリックリンクを移動すると、移動先からの設定されている相対パスと同名のファイル/ディレクトリがない限りリンクを開けなくなるが、ディレクトリジャンクションを移動しても、リンク先のパスが変わらなければ、問題なく開くことができる。

lsコマンドでの表示

Git Bashから ls コマンドを使うと、シンボリックリンクディレクトリジャンクションには、Linuxなどでの実行結果と同様、ファイル名の後ろに @ がつく。

また、 ls -l で、シンボリックリンクディレクトリジャンクションにはリンク先のパスが表示される。

ディレクトリジャンクションは、絶対パス指定のディレクトリのシンボリックリンク扱いの模様。

$ ls -1 --group-directories-first
junction@
symlink_dir@
hardlink.txt
symlink_file@

$ ls -l --group-directories-first
total 1
lrwxrwxrwx 1 ... 18 ... junction -> /c/root/target/src/
lrwxrwxrwx 1 ... 13 ... symlink_dir -> ../target/src/
-rw-r--r-- 2 ...  1 ... hardlink.txt
lrwxrwxrwx 1 ... 21 ... symlink_file -> ../target/src/exp.txt

リンクの削除

作成されたシンボリックリンクや、ディレクトリジャンクションに対し、 unlink のような削除用コマンドは用意されていない。

通常のファイルやディレクトリと同様、 rd/rmdirdel で削除可能。エクスプローラーからも削除できる。

ln コマンドとの違い

ln コマンドは ln [OPTION]... [-T] TARGET LINK_NAME なので、 mklink とは linktarget の順序が逆になる。

lnTARGET相対パス指定する際は、カレントディレクトリではなく LINK_NAME からの相対パスにする必要があるので、 mklink のほうがわかりやすい気もする。

一方、絶対パス指定であれば ln -s src dest で指定できるので ln のほうがシンプルか。

また、デフォルトではハードリンクが作成されるのも mklink と異なる(あまり ln をハードリンク作成に使うこともないとは思うが)。

注意点

シンボリックリンクの作成権限

オプション未指定、または /dシンボリックリンクを作成する場合、デフォルトでは権限がないため、 この操作を実行するための十分な特権がありません。 というエラーが発生する。

シンボリックリンクを作成するには、コマンドプロンプトを管理者権限で実行して実行するか、開発者モードを有効とする必要がある模様。

開発者モードを有効化するには、Windows10の「設定」>「更新とセキュリティ」>「開発者向け」から「開発者モード」を選択し、再起動する。

f:id:hepokon365:20201024170258p:plain

コマンドプロンプトからしか実行できない

PowerShellや、Git for Windowsに付属するGit Bashから実行しようとしても、エラーとなる。

PS C:\Users\hepokon365> mklink
mklink : 用語 'mklink' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されま
せん。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してく
ださい。

exeにパスを通そうと思って where mklink しても、ファイルパスが見つからない。

C:\Users\hepokon365>where mklink
情報: 与えられたパターンのファイルが見つかりませんでした。

どうやら、 cmd.exe の組み込みコマンドの模様。

例えばPowerShellから実行する場合、 Start-Process cmd "/c mklink ..."コマンドプロンプトを起動し、コマンドとして渡してやればいい。この時、 -Verb Runas オプションを使用すれば、管理者権限でコマンドプロンプトの実行が可能。

Start-Process -Verb Runas cmd "/c mklink /d <link> <target>"

なお、Git Bashでは ln が使えるが、 -s オプション付きで実行しても、ファイルコピーが行われるだけの模様。

振り返り

シンボリックリンクが作れるとはいえ、管理者権限または開発者モードを有効にしないと使えなかったりと制限が多い。

ディレクトリのシンボリックリンクを作る用途で、かつ作成後にターゲットディレクトリを移動することがないのであれば、 /j によるディレクトリジャンクションリンクが権限不要で同じように使えるため、そちらを検討してもよさそう。

従業員数に応じた企業規模の分類(SMB,MM,GBなど)とその正式名称(たぶん)

IR資料などでたまに見かける、「SMB」といった従業員数に応じた企業の分類と、その正式名称が毎回わからなくなるのでメモ。

企業分類の例

ビズリーチSalesforceの記事に、分類とそれぞれの企業規模が記載されている。

www.bizreach.jp

従業員数4,000名未満のお客様を対象とし、1~14名(ESB)、15名~99名(SMB)、100~499名(MM)、500~4,000名未満(GB)と従業員数ごとで分類

この分類は、Salesforceによるものの模様。Salesforceを利用している会社では、この分類をそのまま使用することが多いようで、IR資料などでちょくちょく見かける。

分類ごとのSalesforceにおける人数と正式名称(仮)

正式名称は、それっぽいのをピックアップしただけなので、間違っているかもしれない。

ESB

「Entrepreneurship and Small Business」の略称。15人未満の企業を指す。

起業直後くらいのイメージかな?

SMB

「Small and Medium Business」の略称。100人未満の、いわゆる中小企業を指す。

MM

「Mid Market」の略称。500人未満の企業を指す。日本語では、中堅企業のイメージか。

Wikipediaにそれらしいページがあった。

Middle-market company - Wikipedia

Definitions of the middle market are generally derived by dividing the United States economy into three categories: small business, middle-market, and big business.

ということで、中小企業と大企業の中間を指す模様。海外では一般的な分類なのかな?

GB

「General Business」の略称。4000人未満の企業を指す。日本語では、準大手企業のイメージか。

EBU

「Enterprise Business Unit」の略称。4000人以上の企業を指す。ここまで行くと、まごうことなき大企業だろう。

その他の分類

この分類は、Salesforceによるものの模様。会社によっては、分類が異なったり、同じ分類でも従業員数の閾値が異なる。

以下を見ると、ガートナー社ではGBは1000人未満らしい(未確認)。

makitani.net

  • 中堅企業(General Business):従業員数1000未満500人以上の企業
  • 中規模企業(Mid-Market Business):従業員数500人未満100人以上の企業
  • 小規模企業(Small Business):従業員数100人未満の企業

また、この「中堅企業」「中規模企業」「小規模企業」といった従業員数1000未満の企業を「Commercial Business Unit (CBU)」と称し、それより規模の大きい企業を「Enterprise Business Unit (EBU)」と分類するところもある。

といった記載もある。「分類するところ」がどこなのかわからないが。

振り返り

それぞれの略称でWeb検索すると、海外の企業の採用ページなどが出てくる。いくつか見てみたが、従業員数の閾値は規模が大きくなるほどバラつきがあった。

ガートナーのGBが1000人未満を指しているとすれば、SalesforceのGBとは最大で4倍の従業員数差が生じる。

自社内でのやり取りや、Salesforceの利用者間でのやり取りなど、それぞれの前提がそろっていればいいが、そうでない場合はどの程度の規模や従業員数をどう分類するかをすり合わせておかないと、齟齬が出そうだと思った。

余談

「GB」が最も検索しずらかった。「Salesforce GB」で検索すると、ストレージ容量の話しか出ないんだもの...

あとは、なんで「MM」だけ「Business」がつかないんだろう?

Javaでinstanceofの代わりに使えるClassクラスのメソッドはisInstanceとisAssignableFromだというメモ

タイトル通り。

使おうと思うたびに「 instanceof 演算子の代わりに使えるメソッドなんだっけ?」となるのでメモ。

環境

OpenJDK 8 8.262.10。

それぞれの使い方

instanceof 演算子

instanceOf ではない。

インスタンス(参照型) instanceof クラス/インターフェース名 と記述。

以下の条件に該当すれば true 、該当しなければ false を返す。

  • 右辺がクラス名の場合、左辺が右辺で指定したクラス、またはサブクラスのインスタンス
  • 右辺がインターフェース名の場合、左辺が右辺で指定したインターフェース、またはそのサブインターフェースを実装している

後述のClassクラスのメソッドの説明を借りると、「左辺が右辺と代入互換の関係にあるか」を判定する。

地味に便利なのは、左辺のインスタンスnull でも例外は発生せず、 false を返してくれる。

Object str = "str";
assert str instanceof String;
assert str instanceof CharSequence;
assert str instanceof Object;

Object sb = new StringBuilder("sb");
assert !(sb instanceof String);
assert sb instanceof CharSequence;
assert sb instanceof Object;

Object nil = null;
assert !(nil instanceof String);
assert !(nil instanceof CharSequence);
assert !(nil instanceof Object);

Class#isInstance(Object)

Class<?>.isInstance(インスタンス) のように記述。

instanceof 演算子とは、左辺と右辺が逆になる。

Javadoc

指定されたObjectが、このClassが表すオブジェクトと代入互換の関係にあるかどうかを判定します。このメソッドは、Java言語のinstanceof演算子と動的に等価です。

こちらも、引数として渡すインスタンスnull でも、例外は発生しない。

Object str = "str";
assert String.class.isInstance(str);
assert CharSequence.class.isInstance(str);
assert Object.class.isInstance(str);

Object sb = new StringBuilder("sb");
assert !String.class.isInstance(sb);
assert CharSequence.class.isInstance(sb);
assert Object.class.isInstance(sb);

Object nil = null;
assert !String.class.isInstance(nil);
assert !CharSequence.class.isInstance(nil);
assert !Object.class.isInstance(nil);

Class#isAssignableFrom(Class<?>)

Class<?>.isAssignableFrom(Class<?>) のように記述。こちらはインスタンスではなく、 Class を引数にとる。

Javadoc

このClassオブジェクトが表すクラスまたはインタフェースが、指定されたClassパラメータが表すクラスまたはインタフェースと等しいかどうか、あるいはそのスーパー・クラスあるいはスーパー・インタフェースであるかどうかを判定します。

instanceofClass#isInstance(Object) との違いとして、 null を渡すと NullPointerException が発生する。

Class<?> strClass = String.class;
assert String.class.isAssignableFrom(strClass);
assert CharSequence.class.isAssignableFrom(strClass);
assert Object.class.isAssignableFrom(strClass);

Class<?> sbClass = StringBuilder.class;
assert !String.class.isAssignableFrom(sbClass);
assert CharSequence.class.isAssignableFrom(sbClass);
assert Object.class.isAssignableFrom(sbClass);

Class<?> objClass = Object.class;
assert !String.class.isAssignableFrom(objClass);
assert !CharSequence.class.isAssignableFrom(objClass);
assert Object.class.isAssignableFrom(objClass);

try {
    Object.class.isAssignableFrom(null);
    assert false;
} catch (NullPointerException e) {
    assert true;
} catch (Exception e) {
    assert false;
}

引数として渡すインスタンスnull でなければ、 Class.isInstance(obj) == Class.isAssignableFrom(obj.getClass()) となる。

注意点

Class#isInstance(Object) に、 Class を渡してもコンパイルエラーにならない。

昔、それが原因のバグを修正する羽目になったので、それからは null チェックしてから getClass() して Class#isAssignableFrom を使うのが好み。

振り返り

レガシーなコードに手を入れている時、 instanceofif else で5個くらいつながっていたので、書き直そうと思ったが、どうしても isAssignableFrom が思い出せなかった。

老化がヤバい。

Groovyの名前付き引数(パラメータ)について

Groovy + Spockでユニットテストを書いているが、Groovyの名前付き引数・名前付きパラメータを知らないメンバーがいた。

なかなか特殊な仕様なのでメモ。

環境

サクッとGroovy Web Consoleで確認。

println System.getProperty('java.version')
println GroovySystem.version

してみたところ、

1.8.0_181-google-v7
2.5.7

だった。

仕様

ドキュメントにはサラッとした記述しかない。

昔は Named argument と呼んでいたが、今見ると Named parameters と呼ばれている。

いつから変わったのかと思ったら、 v2.5.3までは Named argumentv2.5.4Named parameters になった模様。

コンストラクタの名前付き引数

デフォルトコンストラクタが存在するか、第1引数にMapを取るコンストラクタが存在する場合、フィード名をキーとしたMapを渡すことで、名前付き引数として扱える。

また、GroovyでのMapリテラル[key: value] だが、名前付き引数呼び出し時であれば、 [] で囲む必要がなくなる。

// デフォルトコンストラクタの場合
@groovy.transform.ToString
class ExampleClass1 {
  Integer id
  String name
}

assert new ExampleClass1().toString() == 'ExampleClass1(null, null)'
assert new ExampleClass1(name: 'name').toString() == 'ExampleClass1(null, name)'
assert new ExampleClass1(id: 1).toString() == 'ExampleClass1(1, null)'
assert new ExampleClass1(name: 'name', id: 1).toString() == 'ExampleClass1(1, name)'

// Mapを引数に取るコンストラクタの場合
@groovy.transform.ToString
class ExampleClass2 {
  Integer id
  String name

  // デフォルト値として空のMapを指定するか、Nullセーフ演算子を使わないと、引数省略時にNullPointerExceptionが発生する
  ExampleClass2(Map parameters) {
    this.id = parameters?.id
    this.name = parameters?.name
  }
}

assert new ExampleClass2().toString() == 'ExampleClass2(null, null)'
assert new ExampleClass2(id: 1, name: 'name').toString() == 'ExampleClass2(1, name)'

メソッドやクロージャの名前付き引数

メソッドやクロージャの場合も、第1引数にMapを取れば名前付き引数となる。

名前付き引数呼び出し時であれば、 [] が省略できるのもコンストラクタと同様。

// デフォルト値として空のMapを指定するパターン
def ExampleMethod(Map parameters = [:]) {
  return [parameters.id, parameters.name]
}

assert ExampleMethod() == [null, null]
assert ExampleMethod(id: 1) == [1, null]
assert ExampleMethod(name: 'name') == [null, 'name']
assert ExampleMethod(id: 1, name: 'name') == [1, 'name']

注意点

第1引数にMapを取る場合、デフォルト値として空のMapを指定しても、明示的に null を渡されると参照時に NullPointerException が発生する。

null が渡される可能性があれば、Mapの値の参照は、常にNullセーフ演算子 ?. で行っておくのが無難。

名前付き引数のデフォルト値の設定

第1引数にMapを取る場合、デフォルト値としてMapを設定し、そこにkeyとvalueを指定すると、引数省略時はそのMapおよびkeyとvalueが使用される。

だが、引数の一部を指定した場合、未指定の引数はnullになる。

@groovy.transform.ToString
class ExampleClass {
  Integer id
  String name

  ExampleClass(Map parameters = [id: 0, name: 'default']) {
    this.id = parameters.id
    this.name = parameters.name
  }
}

// 引数省略時はうまくいったように見える
assert new ExampleClass().toString() == 'ExampleClass(0, default)'

// 引数をすべて指定した場合も問題なし
assert new ExampleClass(id: 1, name: 'name').toString() == 'ExampleClass(1, name)'

// 引数の一部を指定した場合、未指定の引数はnullになる
assert new ExampleClass(id: 1).toString() == 'ExampleClass(1, null)'
assert new ExampleClass(name: 'name').toString() == 'ExampleClass(null, name)'

引数の一部を指定した場合でも、未指定の引数にデフォルト値を設定したい場合、コンストラクタやメソッド内でデフォルト値を設定したMapを用意し、そこに第1引数のMapをputAllするのがよさそう。

def ExampleMethod(Map parameters) {
  def params = [id: 0, name: 'default'] + (parameters ?: [:])
  return [params.id, params.name]
}

assert ExampleMethod() == [0, 'default']
assert ExampleMethod(id: 1) == [1, 'default']
assert ExampleMethod(name: 'name') == [0, 'name']
assert ExampleMethod(id: 1, name: 'name') == [1, 'name']

名前付き引数と通常の引数の組み合わせ

第2引数以降に、通常の変数を宣言することも可能。

その場合、 key: value 形式で指定した引数は第1引数のMapに、それ以外の引数は第2引数以降に指定順に渡される。

必須としたい引数は第2引数以降で宣言し、必須ではない引数は名前付き引数にする、といったことが可能。

ただし、第1引数のMapにはデフォルト値を指定しておかないと、名前付き引数が1つも渡されていない場合、 MissingMethodException が発生する。

Groovyの仕様では、引数のデフォルト値を指定すると、通常はそれ以降の引数にもデフォルト値を設定する必要があるが、この名前付き引数と通常の引数の組み合わせの場合、第2引数以降はすべて必須パラメータとなり、デフォルト値を指定できない(指定しても、省略してメソッドを呼び出せない)模様。

そのため、第2引数以降を省略したければ、メソッドをオーバーロードする必要がある。

def ExampleMethod(Map parameters = [:], int id, String name) {
  return [id, name, parameters.address, parameters.email]
}

def ExampleMethod(Map parameters = [:], int id) {
  return ExampleMethod(parameters, id, 'default')
}

assert ExampleMethod(1) == [1, 'default', null, null]
assert ExampleMethod(1, 'name') == [1, 'name', null, null]
assert ExampleMethod(address: 'tokyo', email: 'mail@example.com', 1) ==[1, 'default', 'tokyo', 'mail@example.com']

// 名前付き引数の宣言順は不定
assert ExampleMethod(
    email: 'mail@example.com',
    1,
    'name',
    address: 'tokyo'
) == [1, 'name', 'tokyo', 'mail@example.com']

ここの挙動について、ドキュメントが見つけられなかった。動きから逆算しているので、仕様とは説明が異なるかもしれない。

振り返り

いつの間にやら「名前付き引数と通常の引数の組み合わせ」を使っていたが、どういった経緯でこの方法を知ったかは思い出せない。

機能名としては「名前付き引数」で通したが、ドキュメントの英語からだと「名前付きパラメータ」が正式名称になるのかな?