kazokmr's Blog

試したこと、読んだこと、見たこと、聴いたことを書きたくなったら書くブログ

BDD in Action, 2nd Edition 第7章 を読んだ感想メモ

この本はまだMEAPで全部が公開されていないわけだけど、 6章の公開が去年の6月末で、7章は去年の大晦日でした。
まぁ自分の感想は、そこから更に5ヶ月近くかかっていますが。

www.manning.com

6章までを軽くおさらいすると、このような流れでシナリオをfeatureファイルに書き出しました。

  • ステークホルダーやQAエンジニアと協力して、

  • 要件を深掘りして詳細化し、

  • 実例を出して話し合いながら、

  • (テスト)実行可能なシナリオを作る。

  • シナリオはfeatureファイルに、Gherkinフォーマット(Given, When, Then) で書く。

そしてこの7章から少しずつ テストコード (受け入れテストコードと言った方がいいかも。)を書き始めるので、これまでよりもCucumberの使うためのテクニックの説明に入ります。
ですが7章と8章は、テストコードを書き方というよりは、準備やテストコード設計のための原則を説明しています。
中でも7章は、6章までで見てきたシナリオ と ステップ定義を紐づけるときのテクニック、ステップ定義の文法、Gherkinの主なキーワードの説明のような、Cucumberの基本的な使い方の説明が主な内容となっている。

尚、本文ではJavaJavaScript の2種類のパターンで説明していますが、本ブログは基本的にJavaで書くことを前提とした感想になっている。

ステップ定義 (グルーコード)

ステップ定義 (StepDefinition)は、featureファイルに書いたシナリオから、ステップ別にメソッドを定義する。
ステップとは Gherkinの Given, When, Then を指し、ステップをAnd や But で繋げている場合は、それらも1つのステップ定義としてメソッドにする。

ステップ定義メソッドには、シナリオのどのステップに対応しているかを明示するために、 アノテーション(@Given, @When, @Then) を付け、値にシナリオのステップ文を入力する。

ステップ定義は、Givenメソッドがテストの前提条件、Whenメソッドがテストの入力条件、そして Thenメソッドが結果の出力と検証(assertion) を書く。
Cucumberが シナリオのテストを実行する時は、書かれているステップの順番でメソッドを実行するみたい。

ステップ定義は、Cucumberに対して、(featureファイルの)シナリオと対応するテストコードを結びつける役割を持つので、グルーコード (glue code: glue は接着剤)と呼ばれる。

Step Definitions - Cucumber Documentation

Cucumber Test Runner クラス

Cucumber は専用のランナークラスを作成して、テストを実行する。 JavaJUnitを利用する場合は、JUnitの @RunWithアノテーションを利用して、ランナークラスを定義する。

またこのランナークラスの内部では、featureファイルのルートディレクトリ、ステップ定義のルートパッケージなどの設定も行える。

Cucumber Reference - Cucumber Documentation

ステップ定義パラメータ

シナリオのステップ文に書かれた値は、ステップ定義メソッドのパラメータに渡すことができる。

パラメータとして渡す値は、アノテーションの値に記載したステップ文で {} で囲むことでメソッドが参照することができる。{} の中は定数だけでなく、Cucumber Expression(Cucumber独自の正規表現)と 一般的な正規表現が使えるのでテストデータの値を変えたい場合はステップの文が変わらない限り、featureファイル側のシナリオの値を変えるだけで対応できる。
そのため、featureファイルのシナリオ側は、値をリストやテーブル形式で定義することができ、ステップ定義のパラメータはList型オブジェクトで受け取ることも可能。

ステップ定義のパラメータは、@ParameterTypeアノテーションを付けたカスタムクラスを利用することもできる。
シナリオの値を内包した、アクター(ペルソナ)や ビジネスの関心事(ドメイン)として、テスト用のオブジェクト用意することもできます。
Cucumberのテストに、この ペルソナやドメイン などの概念を取り入れて、テストコードをレイヤー化する話は次の8章で詳しく説明することになる。

Cucumber Expressions - Cucumber Documentation

Cucumber Reference - Cucumber Documentation

Background と Hook

Background と Hook(@Befor, @After, など) は、複数のシナリオ、あるいは フィーチャーを横断して、前述のオブジェクトを共有したり、テスト実行前の前処理あるいは後処理の工程を定義することができる。

Backgroundは、主にFeatureファイル内の複数のシナリオで共用するデータテーブルのことを指す。 このデータテーブルをステップ定義で、@DataTableTypeアノテーションを付与したメソッド内でデータオブジェクト生成し、ステップ定義のメソッドで参照できるようになる。

Backgroudは featureファイルで宣言して記載することから、このデータテーブル自体もビジネス側の関心事の1つであることを意識する。

Gherkin Reference - Cucumber Documentation

Hookは、開発/テストチームがテスト実行のために必要な前処理及び後処理を定義するときに使う。
使用事例としては、「テスト開始前にブラウザを開き、終了後にブラウザを閉じる。」といった操作、「テストの整合性のためにテスト実行時にデータ初期化する。あるいはテスト終了後に破棄する」など。

要するに「適切にテストするためには必要な工程だが、ビジネス側の関心事ではない。」ような、シナリオには現れない工程に対して宣言するのが主な使い方になる。

尚、どのシナリオでどのHookを使うかは、シナリオに任意のタグを設定し、ステップ定義メソッドのHook側のアノテーション内にタグを指定すれば、タグに紐づくシナリオを実行する時だけHook処理を行う。といった条件が設定できる。

Cucumber Reference - Cucumber Documentation

テスト環境の準備

章の最後は、テスト環境の構築についての話。

BDDが対象としているテストは、主に統合テスト、受け入れテスト、およびエンドツーエンドテストとなる。このためテストを行う際はテスト対象アプリと連携する他のシステムの構築が必要になることが多い。

データベースであれば、H2などのインメモリータイプを利用すれば実現しやすいが、本番で使うデータベースと異なったり、データベース以外にも連携するコンポーネントがあると、本番と同等環境でテストを行うことが大事になってくる。

そういった点で、テスト環境としてできる限り仮想化環境、特に最近ならDockerでコンテナ化したコンポーネントでテストを実行しようというお話でした。

この章の最後のセクションでは、テスト用のDockerコンテナを管理しやすくする、TestConteinersというオープンソースライブラリが紹介されています。

www.testcontainers.org

TestContainersは私自身も初めて聞いた気がしますが、PostgreSQLなどのRDBMS以外にも、NoSQL、Kafka、Elasticsearch といったコンポーネントを Dockerコンテナで起動し、更にそれらをJavaオブジェクトとしてテストコードで扱うことができるようです。

TestContainersを使えば、テストコードで連携するコンポーネントやテストデータを管理できるので、便利な気がします。

所感

  • ステップ定義メソッドはできるだけシンプルにしたいが、冗長化を避けて汎用的にしようとすると Cucumber Expressionなり、正規表現を多用してしまいそうな気がした。

  • 逆にステップ定義のglue code が複雑になりそうなら、シナリオを見直すことも考えておいた方が良さそう。

  • TestContainersは、連携サービスをJavaオブジェクトで一括管理できそうな気がしていて、個別にdockerfileを用意しなくても良さそうなのが便利な気がするので、今度使ってみたい。

次の8章も公開されているので、続けて読んでいきます。

JetBrains Academyの実施記録

去年の8月頃から流れでJetBrains Academyを取り組んでいた。

kazokmr.hatenablog.com

無料期間を過ぎてもキリが悪かったのでそのまま続けていて、満足いく所まで終えられたので2021年5月末でサブスクを一旦解除するので、やったことの記録をまとめておく。

注)内容が宣伝っぽくなってしまったり、未経験者の勉強のまとめみたいになっているが、今の会社に転職してからはコードを書くことが減ってしまったが、前職で10年ほど開発でガリガリ書いていました。

Jetbrains Academyは主に、未経験者向けのサービスだと思うので、正直場違いな感じで続けていましたけど、受けてみて初めて知ることも多かったので、毎日楽しく学ぶことができました。

プロフィール

hyperskill.org

去年のお盆の頃に始めて、毎日最低1日は解いていた。(毎日1問ずつ問題が出るのでそれは必ずやるようにした。)

トラック

トラックは専攻とかコースのようなもの。それぞれに決められたトピックとプロジェクトを進めていき達成していく。

今回自分がクリアしたものは、Java Developer, Kotlin Developer, Frontend Developer, Preparing for the AP Computer Science (Java), Java Desktop Application Developer の5つ

最後の2つのトラックは特に意識していなくて、Javaのトピックを全て進めていたら達成していた。

f:id:kazokmr:20210513052102p:plain
Tracks

プロジェクト

達成したプロジェクトはJava, Kotlin, Frontend(HTML, CSS, Javascript)合わせて33
難易度の内訳は、Easy : 7, Medium : 5, Hard : 9, Challenging : 12

達成したプロジェクトのコードはポートフォリオとして公開しても問題無いみたいなので、GitHubで公開している。

GitHub - kazokmr/Jetbrains-academy-Java: Jetbrains Academy Java Developerコースでクリアしたプロジェクトです

GitHub - kazokmr/Jetbrains-Academy-Kotlin: Jetbrains Academy でクリアしたKotlinプロジェクトです

GitHub - kazokmr/Jetbrains-Academy-Frontend: Jetbrains Academy の Frontendjj Developerコースでクリアしたプロジェクトです

トピック (ナレッジマップ)

トピックは1つの学習テーマで、セオリーと呼ぶ説明ページを読んだ後、プラクティスとして幾つかの問題に正解することで進めていく。

ラクティスは、選択式もあれば、実際にコードを書いてテストするタイプもある。間違っても特にペナルティは無いが、プロジェクトを達成することでもらえるGemを使ってヒントを得たり、答えを知ることもできる。

コードを書く問題の場合は、正解後に自分のコードを公開したり、他の人がアップしたコードを見ることができる。

さらには受講者同士でコメント欄で、意見や質問、ヒントを出し合ったりもできる。

自分も実際にコードをアップして、イイねといったリアクションをもらったり、「何でこんな書き方をしているの?」って質問をもらって説明したりした。

以下は、達成したトピックをカテゴリ別にマッピングしたもの

Java

Java11 の内容がベースで体系的に200近くのトピックがある。現在の

f:id:kazokmr:20210513060406p:plain
Java

Kotlin

まだβ版扱いだったのでJavaほど網羅されてはいない。スコープ関数も無かったけど進めていくうちに他のサイトを見て覚えた。
Kotlinを本格的に触るのはこれが初めてだったけど、Javaより使いやすい点もあると思ったし好きになった。

f:id:kazokmr:20210513060559p:plain
Kotlin

JavaScript

Javaコースに含まれている課題もあったので他にも行ってみた。 JavaScriptはここまで体系的に学んだことが無かったのでやってみてよかった。

f:id:kazokmr:20210513060735p:plain
JavaScript

Scala

Scalaもβ版でトピックも8つしか無かったので理解したとは言い難いが、前述のKotlinをクリアした後だったので、Kotlinの雰囲気で学ぶことはできた。

f:id:kazokmr:20210513060823p:plain
Scala

Backend / Android / Java Swing

どれも前職で関わったプロジェクトで1回以上は触ったことがあったけど、JavaScriptと同じで体系的に学んだことが無かったので良かった。これらを理解した上でもう一度あの時にプロジェクトを進められたら面白そうだと思った。

f:id:kazokmr:20210513060925p:plain

Frontend

利用期間が終了するまでに日数があったので最後にプロジェクトも含め進めた。前職の時からそうだったのだけど、CSS使って、画面のレイアウトを考えたりするのは苦手だなと改めて感じた。(大きさや位置を調整したり、どんな色がいいか考えたりする所)

HTML4 や CSS2 の頃のことしか知らなかったので、その頃より書きやすくなった気はするし、Javascriptと合わせて動的な制御を行うのは面白い。

f:id:kazokmr:20210513061005p:plain

Fundamentals

情報工学の基礎やアルゴリズムが学べ、その他にもビルドツールやGit、IDEなどの開発系ツール、データベース・SQLについて学習できる。

アルゴリズムは、大学の専攻が情報系では無かった(電気工学)だったので、一から学ぶことができて知識の底上げになった。

f:id:kazokmr:20210513061106p:plain

Math

プロジェクトを達成するために必要なものトピックだけを実施した。

英語で書かれているのもあるが、数学は大学以降はほとんど勉強していなかったから結構難しかった。
学生時代は得意科目の一つだったと思っていたけど、かなり衰えてしまっていた。

全部のトピックを取り組みたい気持ちもあったけど、それはまた別の機会にする。

f:id:kazokmr:20210513061409p:plain

まとめ

プログラミングだけでなくて、情報工学の基礎的なことも体系的に学べるので、個人的にも学生時代の復習ができた気がしてチャレンジして良かった。

セオリーも問題も全て英語なので、説明がわからないことや問題文の意味を取り違えてしまうことも多かったけど、DeepLを使って続けることができた。

今回、大体の内容をクリアできたし他に学びたいこともあるので、一旦やめるが、トピックが増えて復習したいと思ったら再開すると思う。

mkcertで生成した証明書をHAProxyにインポートしローカルでSSLオフロードする

前回、ローカル環境でHAProxyを使った分散環境が用意できたので、HTTPS接続にも対応させてみる

kazokmr.hatenablog.com

mkcert で自己証明書を生成する

mkcert というツールを使い、ローカルCA証明書を作成・インポートして、サーバー証明書を作成します。 手順は、mkcert のGitHubページに従って進めます。

GitHub - FiloSottile/mkcert: A simple zero-config tool to make locally trusted development certificates with any names you'd like.

mkcert のインストール

brewでインストールします。nssはFireforx からアクセスする場合に必要です。

brew install mkcert
brew install nss

ローカル CA 証明書を作り 端末にインポートする

まずは自己認証局(CA)を作るため、mkcert -installを実行します。途中でrootパスワードを求められたら、端末の管理者パスワードを入力します。

mkcert -install
Created a new local CA 💥
Sudo password:
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊
The local CA is now installed in Java's trust store! ☕️

ローカルのCA証明書が作成されMacにインポートされているか確認します。 mkcert -CAROOT で表示されるディレクトリに CAのルート証明書秘密鍵が作成されます。インポートは 「Keychain Access」を開き左側のメニューで「System」を選び、「Certificates」タブを選ぶと、"mkcert ....." というルートCA証明書がインポートされていることが確認できます。

サーバー証明書を生成する

mkcertで作成したCAを利用してサーバー証明書を作成します。今回は localhost ドメインに対する証明書を作成してみます。mkcert localhost とコマンド実行すると、localhost.pem と localhost-key.pem が生成されます。

mkcert localhost

Created a new certificate valid for the following names 📜
 - "localhost"

The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅

It will expire on 23 April 2023 🗓

ドメインとしては、他にもワイルドカード (*.example.com) や ループバックアドレス (127.0.0.1) などでも作成できる。

HAProxyにSSLオフロード設定を行う

サーバー証明書をHAProxyにインポートする

HAProxyにインポートする証明書ファイル(.pem)は、1つのファイルにまとめる必要があるため、先程作成したキーペアを1つにまとめておきます。

HAProxy version 2.3.19 - Configuration Manual

cat localhost.pem localhost-key.pem > cert.localhost.pem

作成したPemファイルをDockerコンテナで利用するようにDockerfileを修正します

FROM haproxy:2.3
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
COPY cert.localhost.pem /usr/local/etc/haproxy/cert.localhost.pem

設定ファイルを編集する

haproxy.cfg に 証明書のパス、httpヘッダーへのx-forwardedの追加、httpからhttpsへのリダイレクトを追加します。

...
frontend http-in
    bind *:80
    default_backend servers

    # サーバー証明書のパスを指定
    bind :443 ssl crt /usr/local/etc/haproxy/cert.localhost.pem

    # x-forwarded-for を有効にし、プロトコルとポートもheader にセットする
    option forwardfor
    http-request set-header x-forwarded-proto https
    http-request set-header x-forwarded-port 443

    # httpからのアクセスをhttpsにリダイレクトする
    redirect scheme https if !{ ssl_fc }
...

最後にHAProxyのコンテナイメージを再ビルドして実行します。 またホスト側から443ポートでアクセスできるようにすること。

...
services:
  haproxy:
    build: ./haproxy
    depends_on:
      - app01
      - app02
      - app03
    ports:
      - 80:80
      - 443:443
...

動作確認

ブラウザから https でアクセスできました。httpでアクセスした場合もhttpsにリダイレクトします。 またSpringBootアプリのセッション管理も正常でした。

httpsでアクセス

ローカルでSSLやLBの動作確認できるの便利。 今日の設定内容は証明書を除いてGitHubリポジトリにあります。

github.com

参考記事

多機能なロードバランサとして使える多機能プロクシサーバー「HAProxy」入門 | さくらのナレッジ

多機能プロクシサーバー「HAProxy」のさまざまな設定例 | さくらのナレッジ

HAProxy version 2.3.19 - Configuration Manual

HA構成のアプリのHTTPセッション管理にRedis を使う

今更な話ではありますが、AWSのECS Fargate と ALBを使った簡単なデモアプリの環境構築をTerraformで進めていて、セッション管理について調べていたら「ALB側でパーシステンス (スティッキーセッション)を実装するより、ErastiCashなどをセッションストアとして使う」というのを知りました。

個人的にHA構成の経験がほとんどなくRedisも使ったことが無かったことと、またローカル環境でHA構成の動作確認が行えると便利だと思ったので作ってみました。

構成

ソースコードGitHubで公開しています。

github.com

ローカル環境で動作確認をしたかったので、Docker-compose を使って以下のような構成を用意します。

  • ロードバランサ: HAProxy
  • アプリケーションコンテナ: Spring Boot
  • セッションストア: Redis

Spring Boot アプリ

まずはSpring Boot (Spring MVC)を使って、セッションの状態を確認できる簡単なアプリを作ります。

こちらはセッション管理するBean

package net.kazokmr.study.hasession;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@SessionScope
public class Reception implements Serializable {

    private static final long serialVersionUID = -3101986789734320497L;

    private String host;
    private LocalDateTime receptionAt;
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getReceptionAt() {
        return receptionAt == null ? null : DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(receptionAt);
    }

    public void setReceptionAt(LocalDateTime receptionAt) {
        this.receptionAt = receptionAt;
    }
}

コントローラではこのBeanに「状態」「ホスト名」「実行日時」を登録するメソッドとセッションを破棄するメソッドを用意してリクエストを受け付けます。

package net.kazokmr.study.hasession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;

@RestController
public class ReceptionController {

    private final Reception reception;

    @Autowired
    public ReceptionController(Reception reception) {
        this.reception = reception;
    }

    @GetMapping("/hello")
    public String hello() throws UnknownHostException {
        if (reception.getReceptionAt() == null) {
            reception.setHost(InetAddress.getLocalHost().getHostName());
            reception.setReceptionAt(LocalDateTime.now());
            reception.setMessage("セッション開始");
        } else {
            reception.setMessage("セッション中");
        }
        return String.format("%s,  ", reception.getMessage()) +
                String.format("Host: %s,  ", reception.getHost()) +
                String.format("受付日時: %s", reception.getReceptionAt());
    }

    @GetMapping("/bye")
    public String goodbye(HttpSession session) {
        session.invalidate();
        return "セッション切断";
    }
}

これをIntelliJから起動しChromeからアクセスすると以下のように出力します。

初回アクセス

リロード時

セッション切断

Spring Boot アプリをDockerコンテナで起動する

このアプリケーションをDockerコンテナから起動してアクセスしてみます。尚、ホスト名をわかりやすくするために起動時のコマンドに -h オプションを付けます。

docker run --name <コンテナ名> -p 8080:8080 -h docker-container-1 <イメージ>

docker コンテナで起動したアプリにアクセス

HAProxy をDockerで起動する

HAProxyのDockerイメージは公式に記載されている通り、公式イメージに haproxy.cfg を上書きしたイメージをビルドして起動します。

Docker

FROM haproxy:2.3
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

haproxy.cfg はアプリケーションコンテナを3つ用意し、checkで死活監視とラウンドロビンで振り分けているだけとなります。

global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

frontend http-in
    bind *:80
    default_backend servers

backend servers
    balance roundrobin

    server app01 app01:8080 check
    server app02 app02:8080 check
    server app03 app03:8080 check

このhaproxyコンテナとSpring Bootアプリケーションコンテナを docker-compose で起動します。

app01 - 03 は予めDockerイメージを作成しています。またhostnameオプションを使って、どのコンテナにアクセスしているわかりやすくしました。

version: '3'

services:
  haproxy:
    build: ./haproxy
    ports:
      - 80:80

  app01:
    image: sessiontest:latest
    hostname: docker-container-1

  app02:
    image: sessiontest:latest
    hostname: docker-container-2

  app03:
    image: sessiontest:latest
    hostname: docker-container-3

結果

haproxy ではパーシステンスを行っていないためセッションが保持されません。なのでブラウザをリロードする度に、ラウンドロビン方式によって別のアプリコンテナに対して新規接続を行ってしまいます。

haproxy経由でアクセス

HAProxyでパーシステンスを実現する場合

以下のブログの内容を参考にhaproxy.cfg のbackend セクションで、JSESSIONID に振り分け先のserver追加してcookie にセットすることで2回目以降のアクセスも同じアプリケーションサーバーに接続するようにします。

Load Balancing, Affinity, Persistence & Sticky Sessions

HAProxyでHTTPロードバランシング - CLOVER🍀

backend servers
    balance roundrobin

    cookie JSESSIONID prefix nocache

    server app01 app01:8080 check cookie app01
    server app02 app02:8080 check cookie app02
    server app03 app03:8080 check cookie app03

haproxyのDockerイメージを再ビルドして起動したところ、セッションが継続されました。

haproxyでCookieにサーバー情報を埋め込んだ場合

Redis でセッション管理を行う

本題はこちらなので、HAProxyは、再度Cookie設定を戻してセッションが継続されない状態に戻します。

アプリをRedisに対応させる

Reidsをキャッシュストアとして利用するように Spring Boot アプリを変更します。

はじめに開発用にRedisのDockerコンテナを起動します。 Docker

docker run --name redis -p 6379:6379 -d redis

Spring Boot アプリケーション側の設定変更はこちらのチュートリアルを参考にしました。 https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html

RedisをSessionキャッシュにするために Spring Session Data Redis を依存関係に追加します。

更にアプリケーションからRedisに接続すためのクライアントとしてJedis も追加します。 Jedisは前述のチュートリアルでは含まれていませんが、これを入れないとSpring Boot起動時にRedisに接続できずにエラーとなってしまいます。

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>

最後に application.properties で sessionの保存先をredis に変えます。

spring.session.store-type=redis

これだけでHttpSessionの保存先をRedisに変えるための springSessionRepositoryFilter beanを生成してくれるようです。 またRedisの接続先やSessionタイムアウトの管理なども application.properties に追加してカスタマイズできますが、ローカルのRedisに接続するだけであればこれだけでOKです。

詳細は前述のチュートリアルをご覧ください。

Redis に保存されているセッション情報を見る

Docker Composeで環境を作る前に、IntelliJからアプリケーションを起動してRedisにどのように格納されているか見るため、アプリにアクセスしていない状態から確認します。データの検索はRedisのクライアントツールであるRedis-cli を使用します。このツールは Redisにバンドルされているので、Redisコンテナの中に入り利用してみます。

docker exec -it redis /bin/bash
root@redis:/data# redis-cli
127.0.0.1:6379>

接続したら keys * と実行すると保存されているKeyの一覧が表示されます。まだアプリケーションにアクセスしていないので (empty array) と出力されます。

127.0.0.1:6379> keys *
(empty array)

今度はブラウザからアクセスしてから検索します。

127.0.0.1:6379> keys *
1) "spring:session:sessions:expires:7f57263c-0743-471f-b4cc-5e3d41b60e3d"
2) "spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d"
3) "spring:session:expirations:1611233700000"

"spring:session:" から始まるKeyが3つ出力されました。この中で2)にセッションデータが入っているようです。 type <key> でデータの形式を確認するとhash と出力されたので hgetall <key> で検索します。

127.0.0.1:6379> hgetall spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d
1) "sessionAttr:scopedTarget.reception"
2) "\xac\xed\x00\x05sr\x00%net.kazokmr.study.hasession.Reception\xd4\xf3\x87s#\xeev\x8f\x02\x00\x03L\x00\x04hostt\x00\x12Ljava/lang/String;L\x00\amessageq\x00~\x00\x01L\x00\x0breceptionAtt\x00\x19Ljava/time/LocalDateTime;xpt\x00\x11MacBook-Pro.localt\x00\x15\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8bsr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe5\x01\x15\x15\x18\x02:|FXx"
3) "creationTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6"
5) "maxInactiveInterval"
6) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6"

所々にbeanのデータが読めるのでそれっぽいですがわかりづらいですね。そこで下記の記事に書かれているようにセッションデータのシリアライズ方法をJava標準のものからJson形式にしてみます。

Spring Boot+Spring SessionでスケーラブルなステートフルWebアプリが簡単につくれるよ〜 #nginx - Qiita

余談でSpring Securiy も使うなら以下の記事のようにSpring Securityの拡張モジュールの適用もした方が良いと思います。

Spring Security 4.2 主な変更点 #Java - Qiita

package net.kazokmr.study.hasession;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class HttpSessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

(注意) JSON形式に変更した結果、LocalDateTimeで持っていて受付日時のシリアライズで例外が発生してしまいました。本来なら対応するべきですが今回はString型で取得した現在時刻を保持するように変えました。

アプリを起動する前にRedisに残っているデータを全て削除して、再起動しセッションデータを検索します。

127.0.0.1:6379> hgetall spring:session:sessions:a3b43f71-3c5b-48e6-a700-807d7bcb48c5
"sessionAttr:scopedTarget.reception"
"{\"@class\":\"net.kazokmr.study.hasession.Reception\",\"host\":\"MacBook-Pro.local\",\"receptionAt\":\"2021/01/21 21:50:27\",\"message\":\"\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8b\"}"
"creationTime"
"1611233427618"
"maxInactiveInterval"
"1800"
"lastAccessedTime"
"1611233427618"

セッション情報がJSON形式で出力され、生成日時や最終アクセス日時なども見やすくなりました。

とこでRedisに保管されているSESSIONキーですが、これはJSESSIONIDではなく、SESSIONという専用のCookieを使っているようです。ですが、Redisで検索するとUUIDの形式で出力されますが、Chromeの開発者モードで見るとUUIDでは出力されていません。なのでChormeのCokkieからSESSION情報を検索するのは難しそうです。

RedisというかSpring Sessionを利用すると、SessionIDのCookie名が JSESSIONIDではなく SESSION になるようです。名称などのCookieのカスタマイズは以下のように CookieSeriarizer Beanを定義してあげれば良さそう

docs.spring.io

Redis を追加したDocker Composeでアプリを起動する

docker-compose.yml にはRedisを追加し、Redis > app01-03 > HAProxy の順番に起動するように依存性を追加しました。

version: '3'

services:
  haproxy:
    build: ./haproxy
    depends_on:
      - app01
      - app02
      - app03
    ports:
      - 80:80

  redis:
    container_name: redis
    image: redis

  app01:
    image: sessiontest:latest
    hostname: docker-container-1
    depends_on:
      - redis

  app02:
    image: sessiontest:latest
    hostname: docker-container-2
    depends_on:
      - redis

  app03:
    image: sessiontest:latest
    hostname: docker-container-3
    depends_on:
      - redis

また Spring BootアプリのコンテナもDocker Compose のRedisと接続できるように以下の設定を変更しました。

  1. Docker Compose 専用の application-docker.properties を追加し、compose上のcontainer_name: redis に接続するようにホスト名を追加しました。
spring.redis.host=redis
  1. アプリケーションコンテナをビルドする前に、Maven で clean package を実行するようにしました。これは maven のTestフェーズでRedisとの接続が必要なため、ローカル環境のRedisコンテナにアクセスしてMavenビルドを実行したかったためです。

  2. アプリケーションコンテナ用のDockerfile の内容を変更してDockerイメージを再ビルドしました。 上記のNo.1と2に対応する形で、マルチステージビルドを止め、ENDPOINTでjarを起動時にapplication-docker.properties を参照するようにprofileのactivate 設定オプションを追加しました。

FROM openjdk:11-jre
COPY ./target/*.jar .
EXPOSE 8080
ENTRYPOINT ["java","-jar","ha-session-0.0.1-SNAPSHOT.jar","--spring.profiles.active=docker"]

Redisを使ったセッションストアでアクセスした時

感想

新しい技術では無く沢山の情報もあったことやRedisのセッション管理もSpring Session Data Redisが良い感じで行ってくれるので難しくは無かった。

ただ Redisでセッション管理するにはアプリケーション側の方で設定を行うことになるので利用しているフレームワークなどによっては導入コストが掛かる場合もあると思うので、その場合はロードバランサーのパーシステンスの採用を検討した方が良いかもしれない。

これでローカルの開発環境で冗長化構成の動作確認を簡単に行うテンプレートを用意することができた。これに、HTTPS接続とRDBMSの接続も追加しておくと個人的にはより便利になりそう。

JetBrains Academy で Java Developer コースを進めてみた

今週末で無料で受けられる期間が終了するので、使ってみた感想を書きます。

www.jetbrains.com

動機

8月に Twitterか何かで 見かけて「最近コードを書いていないなー」って思ったので、鉛切った身体を鍛え直すつもりで、最大で2ヶ月ちょっとは無料で受けられるので使ってみました。

進め方

ログインするとまずはTrack と呼ばれる、言語ごとのコース選択があります。私は Java Developer を選択しました。 他にはPython Developer があり、Kotlin Developer や Web Developer などがβ版として選択ができます。

次にプロジェクトを選びます。プロジェクトはこの学習のなかで完成させるアプリケーションとなっていて、プロジェクトの中で5−6のステージに別れています。各ステージではトピックと呼ばれる特定のスキルに対する説明と演習からなる学習が並んでおり、トピックを順番にこなすとステージの最後にプロジェクトの課題が提示され、これまでのトピックで学んだことを活用しながら、課題をクリアします。

トピックの演習とステージの課題は、IntelliJ IDEAと連動しておりIntelliJ でコードを書いて評価します。 ちなみに間違えても特にペナルティはなく、何度もでも挑戦できますし、クリアしないと次に進めない仕組みになっています。 また各演習については参加している他のメンバーがコメントしたりヒントを提示していたり、クリアすると他の人のコードを見たり、自分のコードを公開したりできます。 海外のプログラマーのコードと自分のコードを比較するのも勉強になることもあったり、あまり変わらないなーと思ったり、なんでそんな書き方しているんだろ?って思ったりと面白かったです。

プロジェクトとトピック

プロジェクトはJava Developerだと32種類あり、Easy, Medium, Hard, Challenging の4つの難易度になっています。 トピックは、Javaに関する様々なスキルを学ぶことができ、Java以外にもデータベースやフロントエンドに関するトピックスや、ソフトウェア工学アルゴリズム、代数・幾何と言ったジャンルもあり、全部で403のトピックがあり、知らないことも多かったりしてとても勉強になりました。

Java Developer – JetBrains Academy

JetBrains Academy - Learn programming by building your own apps

進捗

2ヶ月続けて、11プロジェクト(Easy:1, Medium: 3, Hard:5 , Challenging:2 )完了し、トピックは 221/403 (55%) でした。 Hard位だと割とスムーズにクリアできるのですが、Challengingは時間がかかりました。平日は朝と夜に少しずつ進めて、休日に家にいるときに進めていましたが、子供の面倒や外出などもあって、まとまって進める時間はあまり取れませんでした。 f:id:kazokmr:20201022220609p:plain (一応、クリアしたプロジェクトはGitHubに上げているのですが、課題の回答などにもなり規約とかも有りそうなのでPrivateにしてあります。)

感想

Javaについて言えば、色々な範囲を細かくカバーして演習が出来たのでとても勉強になった。特にLambda や StreamAPI などは前職のJavaのバージョン的な都合もあって、あまり触れる機会がなくて、IDEの予測機能などを駆使したパワープレイ的な感じで書くことが多く、いまいち理解できてなかったのですが、関数型インターフェースの学習から始まり、StreamAPIの様々な使い方をトピックとして学べたので力がついたと思います。

またアルゴリズムの勉強や代数・幾何の連立方程式行列式などの計算プログラムを作るプロジェクトもあり、学生時代を思い出しながら復習できたこと、あるいはブロックチェーン の基本的な仕組みが理解できるプロジェクトなどに取り組むことで新しいことも覚えられたのがとても良かったです。

またコースは全て英語になっているので、細かいニュアンスや専門用語にわからない部分が多かったりしました。問題文を見ても何を求められているのかがわからなかったりする部分が多く、DeepLは欠かせなかったです。 海外の人が「この問題の説明はよくわからないな」「英語というのは時折、正確に伝えないことがある。」みたいなことをコメントしていることもあり、自分の英語スキルの問題だけではなさそうでしたが。

またプロジェクトの課題についても、難易度に関係なく説明が大雑把だったりして、どうしたらいいのかを探りながら進めたり、初期の頃は「このプロジェクトのステージやトピックでやったことを使ってクリアしないと」という気持ちが強すぎて、シンプルにコードを書くことができなかったりしました。

今後

無料期間が過ぎたら受講料が発生するのですが、現時点で中途半端な進捗なのとまだ学習してみたいトピックもあるので、多分継続することになると思います。最初のEasyコースが簡単だったので2ヶ月あれば終わるんじゃないかなと思っていたのですが甘過ぎました。

と、JetBrains Academyがベストかは分かりませんが、IntelliJを使いながらプログラミングスキルを上げつつ、英語の勉強にもなるので自分にはあっているなとは感じました。

BDD in Action 2nd Edition 第6章を読んだ

BDD in Action 2nd Edition の第6章は、正しい要件を定義することの最終章として、前章で探索した主要な例を使って、フィーチャーのシナリオをGherkinフォーマットで書く方法を説明しています。

www.manning.com

前半は、Gherkinの主要なキーワードの用途や使い方の説明で、これはCucumberのチュートリアルなどでも学べる内容でした。 この中で知らなかったキーワードにはBackground があり、複数のScenarioの冗長性を排除することができます。

これは書籍に載っていたことではありませんが、キーワードにRuleExampleが追加されていました。 RuleFeatureの中で、Scenarioをグループ化できます。ExampleScnarioの別名なので、用途はScenarioと変わりません。

RuleExampleは、より実例マッピングに近い形式で表現できます。

章の後半では、優れたGherkinの書き方を紹介しています。

Gherkin を書く際に気を付けることとして以下のようなことが挙げられていました。どれもプログラミングの原則でよく言われていることと似ています。

  • シナリオでは宣言を書き、命令的な指示は書かない。(Howではなく、Whatを書く)
    操作することや入力する内容などを1つ1つ詳細に書くのではなく、シナリオで達成したい目的に対して何をしなければいけないかに着目して書くとよい。

  • 1つの関心事に着目してシナリオを書く。 特に初めはシンプルな関心事から初めて、徐々に複雑なシナリオを組み立てる。

  • シナリオの登場人物に個性を付ける(ペルソナを作る) ユーザーや管理者のような味気ない役割だけではなく、目標・能力・背景などを付けると良い。また初めからペルソナを詳細に書くことに時間を費やさずにシナリオを書きながら付け足すと良い。

  • シナリオには必要なことだけを書く。容易に想定できることは省略する。
    とあるフィーチャーのシナリオを書く時に、そのフィーチャー(画面)にたどり着くまでの汎用的で容易に想像がつく手順まで記述しない。特にGivenについてはシナリオを検証するために必要な前提条件だけを書く。

  • 既存のテストスクリプトやテストコードの手順をそのままシナリオとして書かない。
    これらは指示・命令的なステップが多いから。「比較する」や「確認する」が含まれているシナリオは大抵、テストスクリプトのような書き方になりがち。

  • 各シナリオは独立して実行できるようにする シナリオごとに必要な前提条件を宣言して独立して実行できるようにする。一連のシナリオの纏まりを順番に実行する必要がある構造にはしない。(あるシナリオが失敗したら、後続のシナリオが実行できないことが無いようにする。)

個人的には経験上、指示的な書き方になりがちなので、この辺を気をつけていきたい。

ここまでの章で、シナリオをまとめたFeatureファイルを成果物とした、要件定義の進め方を学んできました。これまで自分が行って来たやり方とは全然違っているので慣れるまでに時間がかかりそうだが、作るべきことにより着目して明確に分かりやすい書き方で整理できそうに思えました。

BDD in Action 2nd Edition 第5章

5章を読み終えましたので、学んだことをまとめてみます。

www.manning.com

5章は、BDDの活動サイクルの中の Illustrateフェーズでの作業として、3〜4章の Speculateフェーズで特定し、詳細化したフィーチャー、あるいはユーザーストーリーを明確にしていきます。

フィーチャーの内容を明確にするための手段は「例を使ったビジネス側との会話」です。

例を使って話し合う

  • 話し合いは、ユーザやステークホルダーも参加して、フィーチャーの受け入れ基準を理解し、定義し、合意していく。

  • 話し合いでは、良く適切な質問ができなかったり、脱線したりして、思うように進まずに長引くことがある。

  • フィーチャーに対して、ビジネス側の言葉を使った例で説明することで、話し合いが円滑に進む。

  • 例を使って話う会う時は反例も出すことで矛盾や誤解を知り、不確実なことを洗い出して明確にすることができる。

  • 例を使って理解したことを確認したり拡張したりするために追加の例を使う。

表を使って例を整理する

  • 表はインプットと期待する結果が簡単に説明できるような要件に適している、データ中心的なアプローチ

  • デシジョンテーブルを作るイメージ。だけどこの段階では網羅することよりも要件の理解を優先する

  • アプリケーションの振る舞いなどは表現できないので、実例マッピングやフィーチャーマッピングと組み合わせて使うのが良い。

実例マッピング (Example Mapping)

  • フィーチャー(ストーリー)に紐づくビジネスルールや制約、そしてルールに対する例と反例のリストを特定していく。

  • 目的は、フィーチャーやルール(受け入れ基準)の共通理解を得ること

  • 挙げられたルール、または例の数が多いと、フィーチャーが大きいか、複雑なことを示していて分割した方が良いかもしれない。

  • 話し合いは一つのフィーチャー(ストーリー)に対して、25-30分のタイムボックスで行い、そのフィーチャーに集中する。

  • 話し合いの最後に「作業を始めるために十分な理解を得られたか?」「更に話し合う必要があるか?」を投票して決める。

  • 話し合いで回答できない例や質問は、別に記録しておいて、その話し合いではそれ以上議論しないようにする。

  • 例を追加していく時は「もしこうだった場合は?」「そのルールはいつもそうなのか?」「ルールが適用できない場合はあるか?」などの視点で質問すると良い。

フィーチャーマッピング

  • 大きく、不明確なフィーチャーに適している。

  • 実例マッピングと違う点は「例をステップ単位に分けて説明する」「ステップの最後に期待する結果となる結論(Consequence)を書く」などがある。

  • 実例マッピングが「ビジネスルールを基に例を挙げていく」ように進めるとしたら、フィーチャーマッピングはどちらかといえば「ストーリーから想定される例を上げていって、ビジネスルールや制約を特定する」ようなイメージに読み取れた。

  • フィーチャーマッピング自体は、大きなフィーチャー(あるいはエピック)をルールや制約単位に分割することも目的の一つのなので、その後実例マッピングを使って理解を深めていくと良さそう。

スリーアミーゴス

例を使った話し合いでは、テスター、プロダクトオーナー (ビジネスアナリスト)、開発の3つの役割を持ったメンバーがそれぞれの視点でルールや例を質問していくと良い。

  • テスターは、細部に注意を払い、検証することに着目して、エッジケースなど見落としがちなシナリオを指摘する

  • プロダクトオーナー は、ビジネス視点の妥当性や相対的な価値に着目して、シナリオを指摘する

  • 開発者は、技術的な検討事項を指摘する。

まとめ(感想)

  • 顧客と要件定義を行う時に、わからないことがあれば例を使って話し合うことがたまにあったが、それを意図的に利用して要件の理解を深めることが目的。

  • 付箋あるいはホワイトボードツールを使って、ルールと例を整理する。

  • ここで作ったルールや例のうち主要なものは、Gherkinのシナリオ(Given...When...Then)として使うことになる。だけどこの時点ではそれを意識して書く必要はない。実際このフェーズで上がった例のほとんどはシナリオとしては使わないらしい。

  • この5章は、初版には載っていなかった(あるいは前後の章の中で少し触れていた程度だった)が、2nd Editionになって、「フィーチャーやストーリーを定義する」と「Gherkinのためのシナリオファイルを書く」の間にこの章が作られたことを考えると、これまでフィーチャーからシナリオファイルに変換する作業が難しかったので、例を用いた様々なテクニックが使われるようになったのかなと推測した。

次の6章は、要件定義について解説した第2部の最後に当たり、成果物としていよいよシナリオを書いてFeatureファイルを完成させていくようなので楽しみ。