GradleでコンパイルするJavaのバージョンを指定するときはJavaVersion列挙型が使える

小ネタ。

GradleでJavaコンパイルするバージョンなどを指定する際、お決まりで

apply plugin: 'java'

sourceCompatibility = 1.8
targetCompatibility = '1.8'

のように記述するが、ここに列挙型が使える。

ただ、毎回クラス名が何だったか忘れるのでメモ。

sourceCompatibility などの仕様と JavaVersion 列挙型

JavaPluginExtensionsourceCompatibilitytargetCompatibility をみると、型が JavaVersion になっている。

よくある記述例のように、数値や文字列を設定すると、 JavaVersion#toVersion(Object) メソッドで、JavaVersionに変換されている模様。

JavaVersion は別途importなどせずに使用可能。また、 targetCompatibility のデフォルトは sourceCompatibility なので、通常省略できる。

apply plugin: 'java'

sourceCompatibility = JavaVersion.VERSION_1_8
// targetCompatibility = JavaVersion.VERSION_1_8

Gradleのバージョンで利用可能なJavaのバージョンが列挙型の値として宣言されているため、JavaVersionを使えば記述ミスなどが起こらないのがメリットか。

枯れたバージョンならともかく、常にGradleが対応する最新のJavaを使って動作検証などしたい場合には、 JavaVersion.VERSION_HIGHER も使える。

ただ、新しいバージョンのJavaの動作検証をしたい場合、Gradle v6.7 以降の Toolchain を使うほうが無難だと思うので、需要はなさそうだが。

なお、 Toolchain を使うときの、ビルド用Javaバージョン指定には、 JavaLanguageVersion を使う。こちらはインターフェースで、ファクトリーメソッド JavaLanguageVersion#of(int) または JavaLanguageVersion#of(String)インスタンスを生成する模様。

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(14)
  }
}

振り返り

初期設定で書いたら終わりなので、毎回「クラス名なんだっけ、う~ん」となっては過去につくったプロジェクトを見て設定、をやり続けているので、いい加減アウトプットしてみる。

JavaVersion に JavaLanguageVersion 実装しておいてくれれば、Gradleの実行とToolchainでのビルドに使用するJavaのバージョンを簡単に合わせられるんだけどなぁ。

Intellij IDEAで生成するテストクラスの接尾語を変更する

Intellij IDEAで、Javaなどのクラスから Alt + Enter でテストクラスを生成できる。

ただ、テストフレームワークにSpockを使用する場合、慣例としてテストクラスの接尾語は Spec だが、デフォルトでは接尾語が Test になってしまう。

接尾語の変更ができないかと調べたのでメモ。

環境

Intellij IDEA v2020.3

設定変更方法

Create tests—IntelliJ IDEA

  1. Editor > Code Style > Java を開く
  2. Code Generation タブをクリック
  3. Naming section > suffix を変更

デフォルトでは Test となっているため、 Spec に変更すればいい。

プロジェクトごとの設定のため、JUnitなどとSpockが混在しているプロジェクトではどちらか一方しか設定できない。

振り返り

Eclipse だとテストフレームワークごとに接尾語を変えてくれたので、Intellij IDEA ではやや面倒。

Kotlinのchunkedのように、Javaでサイズ/要素数によるList分割をするにはGuavaかCommons Collectionsを使えばいい

Kotlinの Iterable#chunked が便利だな~と思っていた矢先、JavaでListを複数のリストに分割する機会があった。

検索すると独自の実装方法が上位に出てくるが、たぶんCommons CollectionsGuavaに用意されてるだろうと思ったら、両方にあったのでメモ。

Kotlinのchunked

Iterable または Sequence の関数。引数として数値を取り、その要素数ごとに分割した List を返す。

val list = (0..5).toList() // 0~5までの6要素

// 引数で指定した要素数のサブリストのリストが返る
assert(list.chunked(3) == listOf(listOf(0, 1, 2), listOf(3, 4, 5)))

// 割り切れない場合、末尾のリストの要素数が少なくなる
assert(list.chunked(4) == listOf(listOf(0, 1, 2, 3), listOf(4, 5)))

// 要素数以上の値を指定した場合、先頭のリストにすべての要素が含まれる
assert(list.chunked(6) == listOf(listOf(0, 1, 2, 3, 4, 5)))
assert(list.chunked(7) == listOf(listOf(0, 1, 2, 3, 4, 5)))

第2引数で変換関数を取ることもできるが割愛。

内部的には、Iterable#windowedSequence#windowedの引数 sizestep に第1引数を、 partialWindowstrue を指定している模様。

val list = (0..5).toList()

// windowedのsizeは分割する要素数
// stepは次のリスト生成時の先頭要素の移動数
// partialWindowsはsize未満のリストを保持するか
assert(list.chunked(3) == list.windowed(size = 3, step = 3, partialWindows = true))
assert(list.chunked(5) == list.windowed(size = 5, step = 5, partialWindows = true))
assert(list.chunked(6) == list.windowed(size = 6, step = 6, partialWindows = true))
assert(list.chunked(7) == list.windowed(size = 7, step = 7, partialWindows = true))

Javaでの実装

これをJavaでやりたい場合、Guava v19.0以降、またはCommons Collections v4以降を使用する。

Guava

Guava v19.0で追加された、Lists#partitionで、第1引数のListを第2引数の要素数で分割できる。

分割された内部リストはList#subListによるもののため、第1引数で渡したリストへの変更や、内部リストへの変更がそれぞれに反映されることに注意。

Commons Collections4

Commons Collections v4.0で追加された、ListUtils#partitionを使用可能。

Adapted from http://code.google.com/p/guava-libraries/

とあるように、Guavaの Lists#partition と同じように使える。 List#subList を使っていることなども同じ。

振り返り

Commons Collectionsはv4以上でないと使えないのと、Spring Bootなどのフレームワークを使って実装している場合、依存関係で自然とGuavaが使えたりするので、Guavaのほうが利用のためのハードルは低そう。

独自で実装とかすると、エラー出たときとか面倒なので、極力ライブラリを使いたいものです。

JavaScriptでE2Eテストを記述できる、TestCafeを試してみる

E2Eテストについてちょこちょこ調べていて、ブラウザ操作の自動化でSelenium以外に何かないかなと思っていたら、TestCafeが割と良さそうだったのでメモ。

環境

TestCafe v1.10.0。

インストールにはNode.js v14.15.2、およびYarn v1.22.5を使用。

TestCafeとは

devexpress.github.io

JavaScriptまたはTypeScriptで実装できるE2Eテストツール。

インストールすればすぐに使え、Seleniumのように各ブラウザごとのドライバーなど用意しなくても動かせるのがメリット。

インストール

npmなら npm install -g testcafe 、Yarnなら yarn global add testcafe でグローバルインストールする。

testcafe -v でバージョン表示、 testcafe -b で、利用可能なブラウザ一覧を確認できる。

Redirecting…

FirefoxGoogle ChromeChromiumMicrosoft Edge、おまけでIE11の入ったWindows 10では、インストール時点で以下のように表示される。

$ testcafe -b
Using locally installed version of TestCafe.
firefox
chrome
ie
edge

テスト実行

Redirecting…

コマンドラインから実行する場合、 testcafe [-c 並行実行数] [実行するブラウザ] テスト実装ファイル

ブラウザは firefox,chrome,ie のように、カンマ区切りで複数実行可能。 firefox および chrome は、それぞれ firefox:headless および chrome:headless と記述することで、ヘッドレスモードでも実行できる。

また、 -c 並行実行数 を指定することで、並行実行も可能。

この他にも、後述のFixture名でフィルタリングしたりできる模様。

JavaScriptからの実行

APIを使用して、JavaScriptからも実行できる。

Redirecting…

const createTestCafe = require('testcafe');
const testcafe = await createTestCafe();
const runner = testcafe.createRunner();

await runner
    .src(['tests/fixture1.js', 'tests/fixture2.js'])
    .browsers(['firefox', 'chrome:headless', 'ie'])
    .concurrency(3)
    .run();

テスト実装

fixtureとtest

Redirecting…

テストファイル内に1つ以上のfixtureを、また1つのfixtureにつき1つ以上のtestを記述する。

fixture `MyFixture`;

test('Test1', async t => {
    // テストコードを記述
});

fixture, test、どちらでも、 page で実行する際のURLを指定できる。fixtureにpageを指定すると、そのfixtureに含まれるすべてのtestがそのURLを開いた状態で実行される。testでpageを指定することで、fixtureのURLを上書きできる。

fixture `MyFixture`
    .page `https://google.com`;

test('Test1', async t => {
    // Googleで実行される
});

この他、meta でメタ情報を付与したり、fixtureには beforeEach および afterEach 、testには before および after で前処理/後処理を記述したりできる。

TestController

testの第2引数で渡している関数の引数、 t の実体はTestControllerというクラス。

文字の入力、クリック、エンター押下などはTestControllerを通して行う。

また、 ctx プロパティにKey-Valueで値を設定することで、fixtureとtest間や、testのbeforeとテストコード間などで値の共有が可能。

Selector

HTMLの要素の取得などは、Selectorを使用する。

以下の記述より、SelectorはTestControllerのメソッドに渡されたり、アサート対象になったタイミングで、遅延評価される模様。

The selector value is evaluated each time you :

  • use the selector for an action;
  • assert selector’s properties;
  • call the selector directly in code to get it’s state;

Assert

TestControllerのメソッドとして、Assert用のメソッド expect が用意されている。

expect にactualを渡し、メソッドチェーンで比較用メソッドを実行して期待値を渡す。

expect に渡された値が、Selectorで取得したDOMノードやPromiseの場合、動的ページに対応するために、テストが失敗してもすぐにエラーにはならず、タイムアウトまで複数回実行されるとのこと。Smart Assertion Queryと呼ばれている。

Page Object Pattern

サンプルを見るとほとんど手続き的にテストを書いているが、Page Object Patternでも記述できる模様。

Redirecting…

振り返り

ちょっと試したが、なんといっても複数ブラウザでの実行が、Seleniumに比べると非常に楽。

タイムアウトまで再試行というのは、素のSeleniumを使っているならいざ知らず、Selenideのようなラッパーを使っていれば対応できるので、あまりメリットには感じなかった。

一方、やや不便に感じたのは、非同期処理には async/await を使用しているため、都度記述するのが面倒。これは慣れるしかないか。

セットアップや複数ブラウザ対応が楽というのは、運用開始時にはプラスになるが、運用が始まればあまり意味がなくなるので、既にSeleniumなどのE2Eテスト資産があれば乗り換えるメリットはないが、新規でE2Eテスト環境を構築しようと思っているのであれば十分候補になるかと思った。

Kotlinの@JvmOverloadsをプライマリコンストラクタに書く方法

小ネタ。

Kotlinで、プライマリコンストラクタに @JvmOverloads アノテーションを付ける方法を知らないメンバーがいたのでメモ。

状況

多いのは以下のような data class

data class DataClass(...)

と書かれている場合、 @JvmOverloadsconstructor にしか付与できないため、このままでは記述できない。

対応

class DataClass(...)class DataClass constructor(...) の省略形なので、以下のように constructor を明示し、それにアノテーションを付与してやればいい。

data class DataClass @JvmOverloads constructor(...)

振り返り

3か月くらいKotlinとJavaが混じった環境でコードを書いているが、Javaとの相互運用を考えると、Kotlinオンリーの環境でなければ @JvmOverloads はコンストラクタには必須にしておくほうがよさそう。

GaugeのJava実装を試してみる

前回の続き。

Taikoを使ったJavaScriptによるテスト実装でもろもろ躓いたので、Javaによるテスト実装を試してみる。

環境

Gauge v1.1.5、AdoptOpenJDK v11.0.9.1。

Selenium WebDriverはChocolateyでインストールしたChrome Driverを使用。

JDKのバージョンについて

環境にも書いたが、公式の記述によると、JDK11以上が必要とのこと。

今回はChocolatey の cinst adoptopenjdk11 でインストールした、AdoptOpenJDKを使用。

プロジェクトの作成

前回記事参照java_maven_selenium を選択した。

当初はVSCodeでしか実装できないのかと思ったが、Eclipseプロジェクト用の .classpath.project も生成されており、またテスト実行も mvn test で行えるため、プロジェクト生成をVSCodeで行ってしまえば、任意の開発環境で実装可能と思われる。

(公式では紹介されていないとはいえ、GaugeプラグインのあるIntellijはともかく、Eclipseで実装するのは辛そうだが)

pom.xmlの修正

作成直後では、 pom.xml文字コード指定が行われていない。

mvn test でテストの実行ができるが、UTF-8spec ファイル記述し、そこに日本語などが含まれると、MS932で読み込もうとしてエラーになるため、 project.build.sourceEncoding を追加しておく。

<project ...>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

また、デフォルトではアサーションライブラリとしてAssertjが依存性に含まれているため、適宜変更。JUnit4などのテストフレームワークは依存性に含まれない。

Java実装

公式のWrite Gauge specifications#Step implementationsに、テスト実装の方法が記載されている。

WebDriver用のヘルパークラス

java_maven_selenium でプロジェクトを作成した場合、 driver パッケージに Driver および DriverFactory クラスが生成されている。

Driver.webDriver staticフィールドに、 DriverFactory にて、 System.getenv("BROWSER") に応じた WebDriver インスタンスが設定される。

  1. IE ならば InternetExplorerDriver
  2. FIREFOX ならば FirefoxDriver
  3. それ以外(未指定含む)は ChromeDriver
    • ChromeDriver の場合、 System.getenv("HEADLESS")Y または y ならばヘッドレスモードで実行

異なるWebDriverを利用する場合、DriverFactoryを変更すればいい。

実行フック

JUnit4などと同様、任意の public void メソッドにアノテーションを付与することで、ライフサイクルごとに前処理/後処理が可能。「Test execution hooks」と呼ばれている。

ライフサイクル 前処理 後処理
スイート @BeforeSuite @AfterSuite
スペック @BeforeSpec @AfterSpec
シナリオ @BeforeScenario @AfterScenario
ステップ @BeforeStep @AfterStep

例えば、先の driver.Driver でのWebDriver生成処理は @BeforeSuite で、 WebDriver#quit() の実行は @AfterSuite の付いたメソッドで実行されている。

これらのアノテーションが付与されたメソッドでは、引数として ExecutionContext を指定しておくことで、実行時のコンテキストを取得できる模様。

また、 スイート以外のアノテーションでは、 tags パラメーターを指定することで、特定のタグが付与されている場合のみ実行といったフィルタリングが可能。

デフォルトでは tags に指定したタグのAND条件だが、tagAggregation = Operator.OR を指定しておくことで、OR条件に変更可能。

データの保存

ライフサイクルに応じたデータの保存用に、いくつかのデータストアクラスが用意されている。各ライフサイクルが終了すると、設定されているデータが破棄される。

ライフサイクル データストア
スイート SuiteStore
スペック SpecDataStore
シナリオ ScenarioDataStore

それぞれstaticメソッドとして put(Object key, Object value) , get(Object key) , remove(Object key) などが用意されており、ほぼ Map<Object, Object> として使える。

(というか、中身は ThreadLocal<ConcurrentHashMap<Object, Object>> )

ステップとメソッドの紐づけ

com.thoughtworks.gauge.Step アノテーションに、 .spec で記述したステップの文字列を記述する。

TaikoによるJavaScript開発時と同様、VSCode上でテスト実装のない .spec のステップや、パラメータを使用しているステップと紐づけたメソッドに引数がない/多い場合など、きちんとエラーにしてくれる。

レポートにスクリーンショットを追加

テスト失敗時には、自動的にスクリーンショットが取られ、レポートに出力される。

任意のタイミングでスクリーンショットを取り、レポートに出力したい場合、 com.thoughtworks.gauge.Gauge#captureScreenshot() を実行すると、そのメソッドに対応するステップに、スクリーンショットが追加される。

試しにスクリーンショットを取ってみたが、デフォルトではブラウザだけでなく、デスクトップ画面全体が映っていた。WebDriverのウィンドウサイズを全画面化するなど、ちょっと工夫がいるかもしれない。

レポートにカスタムメッセージを追加

com.thoughtworks.gauge.Gauge#.writeMessage(String format, String... args) を呼び出すと、そのメソッドに対応するステップに、メッセージが追加される。

文字列は String#format(String format, Object... args) でフォーマットされるので、 %sプレースホルダとして利用可能。

実装例

前回のリベンジ。

spec/search.spec に保存していたMarkdownを、以下のように変更。

# 検索エンジンのタイトル確認

Tags: example, search

   |url                    |title |
   |-----------------------|------|
   |https://www.google.com/|Google|
   |https://www.bing.com/  |Bing  |
   |https://yahoo.com/     |Yahoo |

## 検索エンジンを開いて、タイトルを確認する

* URL <url> を開く
* タイトルが <title> となることを確認

続いて、以下のクラスを src/test/java/SearchSpec.java に保存。

import com.thoughtworks.gauge.Gauge;
import com.thoughtworks.gauge.Step;
import driver.Driver;

import static org.assertj.core.api.Assertions.assertThat;

public class SearchSpec {

    @Step("URL <url> を開く")
    public void openSearchEngine(String url) {
        Driver.webDriver.get(url);
        Gauge.captureScreenshot();
    }

    @Step("タイトルが <title> となることを確認")
    public void checkTitle(String expected) {
        String actual = Driver.webDriver.getTitle();
        Gauge.writeMessage("search engine title: %s", actual);
        assertThat(actual).isEqualTo(expected);
    }
}

実行してみると上手くいった。また、 .spec.java のファイル名が別々でも問題なく動いたり、 specs ディレクトリにサブディレクトリを作ってそこに search.spec を移動したり、 SearchSpec.java を適当なパッケージ配下に移動しても問題なく実行できた。

Taikoで試したときの、複数の実装ファイルがあるとエラーになるのは何だったんだ...

振り返り

JUnitこそ使っていないものの、それに寄せて仕様が決められている印象で、特に詰まることなく実装出来た。

src/main 配下にSelenideなどでPage Object Patternに乗っ取ったクラスを記述し、テスト実行はGaugeで、とかできると捗る気がする。

JavaScriptで毎回メソッド名を忘れる、insertAdjacentHTMLとinsertAdjacentElementについて

JavaScriptで、「文字列をElementにHTMLとして追加できて、 innerHTML じゃなくて、第1引数で追加位置を指定できるメソッド、名前なんだっけ?」と毎回なるのでメモ。

insertAdjacentHTML

developer.mozilla.org

モダンブラウザすべてで利用可能。IE4から対応とあるが、もともとはIEの独自実装だった模様。

第1引数の position で、挿入位置を変えられる。文字列なこと、すべて小文字なことに注意。

  • beforebegin: 要素の前
  • afterbegin: 要素の子要素の先頭
  • beforeend: 要素の子要素の末尾
  • afterend: 要素の後

文字列は before|afterbegin|end の組み合わせからなるが、追加位置との対応がわかりにくい。英語力がないからか

insertAdjacentElement

HTML文字列ではなく、Elementを第2引数として指定する、 insertAdjacentElement もある。

developer.mozilla.org

こちらも、モダンブラウザすべてで利用可能。

マイナーなのか、2020/11/28現在で日本語訳されていない。それぞれ、Elementの親インターフェースである、Nodeの insertBeforeappendChild が使われることが多いからかな?

第1引数の position は、 insertAdjacentHTML と同様。子要素への追加は Node.appendChild でいいが、他のパターンは Node.insertBefore を使って頑張るよりもシンプルになると思う。

// 従来の記述方法の例
const targetElement = ...;
const insertElement = ...;

// targetElement.insertAdjacentElement('beforebegin', insertElement); 相当
targetElement.parentNode.insertBefore(insertElement, targetElement);

// targetElement.insertAdjacentElement('afterbegin', insertElement); 相当
targetElement.insertBefore(insertElement, targetElement.firstChild);

// targetElement.insertAdjacentElement('beforeend', insertElement); 相当
targetElement.appendChild(insertElement);

// targetElement.insertAdjacentElement('afterend', insertElement); 相当
targetElement.parentNode.insertBefore(insertElement, targetElement.nextSibling);

備考

insertAdjacentHTML のMDNには、

余分なシリアル化のステップを回避できる分、 innerHTML への代入による直接的な操作よりもはるかに高速な動作となります。

とあるが、以下のページによると、2015年末時点で、それほど大きな差はないみたい。

ginpen.com

innerHTML への代入を使える時は、素直にそちらを使っておいたほうが楽だと思う。