UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Smalltalkで『テスト駆動開発』の「第I部 多国通貨」をハンズオンしたら快適で楽しかった

今日は。 SPEEDAの開発をやっている濱口です。

SPEEDA開発チームではテスト駆動開発(TDD)、ペアプログラミングを徹底しています。
だからなのか、『テスト駆動開発』はすごく楽しく読めました。
今回ハンズオンを行った「第I部 多国通貨」でも、ペアプロをしながら著者が語りかけてくるような感じで、 読者側も、著者の意図をひとつずつ理解しながら読み進めていけるようになっています。

有意義なハンズオンを、より有意義にしたい

ただ、そうは言っても、 読むだけよりも手を動かしたほうがよいと思いますし、 さらに、書籍で使用されているJava言語で書き写すだけよりも他のプログラミング言語でコーディングを行うことで、 ハンズオンがよりアクティブ・ラーニング化されると考えました。
プログラミング言語の翻訳プロセスを挟むことで、より能動的なハンズオンになるはずです。

古くてあたらしい言語(環境)、Smalltalkにふれたい

今回、私はプログラミング言語にSmalltalkを選択しました。
まったく違うパラダイムの言語でやってみても面白いかもしれませんが、 今回はより純粋なTDD、およびオブジェクト指向プログラミング(OOP)体験を求めてそうしました。
著者も最初はSmalltalkでTDDを行っていたはずですし、 Smalltalkは開発環境も内包していることから、IDEの源流みたいなものにも触れられると考えたからです。

環境はPharoを使いました。(イメージのテンプレートはPharo 7.0)
IcebergというGitリポジトリと連携するツールも付属しますので このハンズオンを行い、ソースコードを管理する限りでは、Pharo単体で完結しました。

f:id:yhamaro:20191209054113p:plain
Pharoの開発環境。ここですべてが完結します。

Smalltalkの文法は非常にシンプルなので、『Pharo by Example』の「Part I Getting Started」にひと通り目を通せば書き始められました。
ただ、Pharo自身を含むすべてのソースコードと自分の書いたコードは全く同じ扱いになるため、「自分の設計・実装が、そのまま既存のクラスライブラリの拡張になる」という意識づけが自然になされます。
なので、既存のクラスライブラリに分け入って、自分の設計を環境になじませる必要性を常に感じることになります。
開発環境の詳細な解説は上述のドキュメントに譲りますが、System Browser、Finderなどの各種ツールが上記のような探索的開発をサポートしてくれます。
今回は、メッセージの命名やプロトコルの整理などの作法を中心に既存のコードを参考にしました。

f:id:yhamaro:20191209054645p:plain
Let's 写経!

Smalltalkでは、わりと忠実な写経が可能だった

一通りハンズオンをこなしてみて、思ったより忠実な写経が出来上がった印象です。
とはいえ、そもそも言語が異なるのでやはり違いはありました。
途中、特に気になった差異をいくつか挙げます。

Javaのコンストラクタのような特別なメソッドが無い

例えば、5ドルを現すオブジェクトのインスタンスが欲しい場合、以下のようにします。

Money dollar: 5

これを実現(コンストラクト)するコードは以下になります。

{ #category : #'instance creation' }
Money class >> dollar: anInteger [
    ^ Money amount: anInteger currency: 'USD'
]

{ #category : #'instance creation' }
Money class >> amount: anInteger currency: aString [
    ^ self new setAmount: anInteger currency: aString
]

{ #category : #'initialization - private' }
Money >> setAmount: anObject currency: aString [
    amount := anObject.
    currency := aString.
    ^ self
]

『Smalltalk Best Practice Patterns』(『ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集』)を参考にしましたが、
一番上のクラスサイドのメソッド(Shortcut Constructor Methodパターン)を入り口として、
汎用的なクラスメソッドの中でインスタンスを生成(new)し、必要なインスタンス変数初期化のためのメッセージを呼び出します(Constructor Parameter Methodパターン)。
インスタンスサイドに初期化時のみに呼び出すSetting Methodを作り、それを正しいプロトコルinitializetion -privateにカテゴライズするところが肝だと考えています。

インスタンス変数のスコープと意図を伝えるためのプロトコル

上記でも登場したMoneyクラスの定義は以下になります。

Class {
    #name : #Money,
    #superclass : #Object,
    #instVars : [
        'amount',
        'currency'
    ],
    #category : #'Example-TDD'
}

Smalltalkではインスタンス変数のスコープはすべてJavaでいうprivate、またはprotectedになります。
なので、インスタンス変数にアクセスするにはGetting/Setting Methodを介すことになります。

{ #category : #private }
Money >> amount [
    ^ amount
]

ただ、そのメソッド自体はアクセス制限されません(publicになります)。
privateプロトコルにカテゴライズすることでこのクラス以外から使用されない意図を伝えます。
「第4章 意図を語るテスト」でamountインスタンス変数をprivateにするタスクを行う際、 上記のような言語仕様の違いはありますが、Smalltalkでもテストコードでamountセレクタを使用しなくなるので、
「テスト対象オブジェクトの新しい機能を使い、テストコードとプロダクトコードの間の結合度を下げた。」という目的に適うタスクになります。

型が無い(untyped)

一番の違いはこれかと思います。
Smalltalkでは型が無く、かつ処理を全てメッセージ送信で表現するため、メッセージのシグニチャ(もちろん、それに型は含まれない)だけで多態を実現します。
なので、Javaのインタフェースのようなものが無い、というか必要無いです。
したがって、「第12章 設計とメタファー」から登場するExpressionインタフェースは必要無くなり、
「第13章 実装を導くテスト」で「Expression に reduce(String) メソッドの定義を引き上げる」というタスクを行いますが、 以下のようにSumとMoneyが等しくreduceTo:メッセージに応答するようにするだけです。

{ #category : #enumerating }
Money >> reduceTo: aCurrency at: aBank [
    | rate |
    rate := aBank rateFrom: self currency to: aCurrency.
    ^ self class amount: self amount / rate currency: aCurrency
]

{ #category : #enumerating }
Sum >> reduceTo: aCurrency at: aBank [
    | sum |
    sum := (augend reduceTo: aCurrency at: aBank) amount
        + (addend reduceTo: aCurrency at: aBank) amount.
    ^ Money amount: sum currency: aCurrency
]

上記の違いはありましたが、このハンズオンが意図する本筋から外れるものはなく、1章ずつステップバイステップで進めていけました。 (章ごとに出来上がったコードのスナップショットはコミット単位で分けています

個人的にはSmalltalkのシンプルが気に入ったので、
この本をさらに読み進めるのと、今まで読んだOOP関連の本(『実装パターン』など)をSmalltalk視点でもう一度読み直すなどし、
SmalltalkでOOPやTDDをしばらく学んでみたいと考えています。

Go言語でオブジェクト指向プログラミングの基本(型編)

こんにちは、SPEEDA開発チームの @tkitsunai です。

最近リリースされたプロダクションコードにようやくGo言語が採用されました。嬉しい。

今回はGo言語でオブジェクト指向プログラミングで型表現についてのテクニックや考え方の基礎を紹介します。もっとGopherが増えて欲しい!

対象読者

  • Go言語をこれから始める人
  • Go言語の型宣言で表現力を高めたい人
  • オブジェクト指向プログラミングに向き合いたい人

型を定義する

オブジェクト指向プログラミングの基本として「型」で表現を体現します。structはGoの値であり型です。

例題:

  • エンジニアだけが所属できる、SPEEDAプロダクトというユニットに、tkitsunaiという名前のエンジニアが所属している。
  • デザイナーだけが所属できる、SPEEDAデザインというユニットに、hiranotomokiという名前のデザイナーが所属している。

例題を悪い例として実装します。

type (
    Unit struct {
        Name string
        Members []string
    }
)

利用する実装コードは以下です。

   tkitsunai := "tkitsunai"
    hiranotomoki := "hiranotomoki"

    speedaProductUnitName := "SPEEDAプロダクトユニット"
    speedaDesignUnitName := "SPEEDAデザインユニット"

    speedaProductUnit := Unit{
        Name:    speedaProductUnitName,
        Members: []string{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name:    speedaDesignUnitName,
        Members: []string{
            hiranotomoki,
        },
    }

型で厳格さを作り、その型を洗練させる

上の構造体定義はプリミティブな型で表現されています。例題に対して型でほとんど表現できておらず悪い例といえます。

  • ユニットが変数名だけで表現されており、デザインユニットにエンジニアが入ってしまう可能性がある
  • tkitsunaiがそもそも人ではない可能性がある
  • tkitsunaiかhiranotomokiという文字だけでは、エンジニアかデザイナーか判断できない
  • etc...

プリミティブ型を多用することによって起きる問題を、型の厳格さを加えることで排除します。

改良1: プリミティブ型をなくす

Go言語には型宣言によって、基本型もしくは定義された構造体を別名で再定義することが可能です。改良を加えてみます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }

    Unit struct {
        Name UnitName
        Members []Person
    }
)

利用する実装コードは以下です。

   tkitsunai := Person{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Person{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := Unit{
        Name: speedaProductUnitName,
        Members: []Person{
            tkitsunai,
        },
    }

    speedaDesignUnit := Unit{
        Name: speedaDesignUnitName,
        Members: []Person{
            hiranotomoki,
        },
    }

少しだけ良くなりました。この改良でProductUnitに人以外が混在することはなくなりました。

また、人の名前とユニットの名前が混在することもなくなりました。

改良2: Unitの特性を型で表現する

例題を見直すと、プロダクトユニットにはそれぞれ特性があります。プロダクトユニットならばエンジニア、デザインユニットならばデザイナーである必要があります。

現在のままでは、tkitsunaiという人がデザインチームに入れられるかもしれません(それはそれで楽しそうですが)。これも型で制限していきます。

type (
    UnitName string
    PersonName string

    Person struct {
        Name PersonName
    }
    Engineer Person // <- Person型をwrapしたEngineer型として定義する
    Designer Person // <- Person型をwrapしたDesigner型として定義する

    ProductUnit struct {
        Name UnitName
        Members []Engineer
    }

    DesignUnit struct {
        Name UnitName
        Members []Designer
    }
)

利用する実装コードは以下です。

   tkitsunai := Engineer{
        Name: PersonName("tkitsunai"),
    }
    hiranotomoki := Designer{
        Name: PersonName("hiranotomoki"),
    }

    speedaProductUnitName := UnitName("SPEEDAプロダクトユニット")
    speedaDesignUnitName := UnitName("SPEEDAデザインユニット")

    speedaProductUnit := ProductUnit{
        Name: speedaProductUnitName,
        Members: []Engineer{
            tkitsunai,
        },
    }

    speedaDesignUnit := DesignUnit{
        Name: speedaDesignUnitName,
        Members: []Designer{
            hiranotomoki,
        },
    }

この改良によって、各ユニットで所属できる型が制限されたので、Engineerであるtkitsunaiはデザインユニットに入ることはできなくなりました(残念)。

型に厳格さを求める完全コンストラクタを活用する

現時点では、PersonNameやUnitNameにはどのような名前も許されていますが、オブジェクト生成時にルールを適用させることで、型を利用する場合にエラーとできるようにします。いわゆる完全コンストラクタを目指します。Go言語にはコンストラクタはありませんが、慣習としてPrefixにStruct名にNewをつけた関数を用意し、structとerrorを返すようにします。

func NewUnitName(unitName string) (UnitName, error) {
    if unitName == "" {
        return "", errors.New("ユニット名が必要です")
    }

    if strings.HasSuffix(unitName, "ユニット") {
        return "", errors.New("ユニット名は必要ありません")
    }
 
    return UnitName(unitName), nil
}

利用するコード

   speedaProductUnitName, err := NewUnitName("") // ユニット名はひとまず入れなくていーや
    if err != nil {
        panic(err)
    }

実行結果

panic: ユニット名が必要です

エラーを握りつぶされてしまったら元も子もないですが、これでUnitNameのルールを生成時に確認でき、空文字なユニット名は作ることはできなくなりました。

このように、UnitNameを持つProductUnitおよびDesignUnitは、UnitNameの完全コンストラクタを通じて、型とそのルールが表現されていきます。

ファーストクラスコレクションと型定義によってメンバーに対する操作を制限する

ProductUnitのフィールドであるMembersは[]Engineer型で表現されています。この状態では、ProductUnitがこのコレクションを直接操作しなければならず、コレクションを壊しかねません。型を抜き出して、ファーストクラスコレクションとしてstructを再定義し、操作を制限させることでコレクションの壊れにくさを作ります。

type Engineers struct {
    list []Engineer
}

func (p Engineers) Add(newEngineer Engineer) Engineers {
    engineers := make([]Engineer, len(p.list)+1)
    for i, engineer := range p.list {
        engineers[i] = engineer
    }
    engineers[len(p.list)] = newEngineer
    return Engineers{
        list: engineers,
    }
}

func (p *ProductUnit) JoinNewMember(engineer Engineer) {
    p.Members = p.Members.Add(engineer) // 増やす操作しかできない
}

上記の実装によって、増やすメソッドは存在するが、減らすメソッドを定義しないことで、コレクションに対する操作を制限できました。一度入ったら出られないプロダクトユニットへようこそ!

なお、上記の例ではストイックにstructを定義していますが、厳格さをそこまで求めないのならばEngineers []Engineerとしても良いかもしれません。

列挙型で表現力を高める

プログラム言語を持ったエンジニアを定義するような値とそれを持った型を作るならば、型表現とコレクションを応用して表現力を高めることができそうです。

type ProgrammingLanguage int
type ProgrammingLanguages []ProgrammingLanguage

const (
    Golang ProgrammingLanguage = iota
    Java
    JavaScript
    Clojure
    Elixir
    Rust
)

type Engineer struct {
    Person
    Skills ProgrammingLanguages
}

func (e Engineer) LetsGoProgramming() {
    for _, lang := range e.Skills {
        if lang == Golang {
            fmt.Println("OK, I will typing `func main() { //blablabla }`")
        }
    }
}

これもただの列挙型ではなく、型に別名をつけることで厳格化させます。

構造体埋め込みで表現をする

構造体埋め込みを利用して型定義に幅を持たせます。

package engineer

import "fmt"

type (
    FirstName string
    LastName  string
    person    struct {
        FirstName FirstName
        LastName  LastName
    }
    GoEngineer struct {
        person
    }
)

func (p person) Greet() {
    fmt.Println(fmt.Sprintf("hello I am %s", p.fullName()))
}

func (p person) fullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

func NewGoEngineer(firstName FirstName, lastName LastName) GoEngineer {
    return GoEngineer{
        person: person{
            FirstName: firstName,
            LastName:  lastName,
        },
    }
}

利用する実装コードは以下です。

package main

import (
    "engineer" // <= 擬似コード:engineerパッケージを読み込んだことにします
)

func main() {
    tkitsunai := engineer.NewGoEngineer(engineer.FirstName("takayuki"), engineer.LastName("kitsunai"))
    tkitsunai.Greet()
}

GoEngineer型のフィールドperson型定義によって型の恩恵を受けられます。同時にperson型はパッケージ外に公開していないため、完全コンストラクタを経由してGoEngineer型を利用すれば、personの詳細実装が外に漏れずに済みます。

構造体の埋め込みは普段活用していませんが、型をまとめるためだけでなく、制限範囲を限定することもできるため、応用がしやすいと思います。

まとめ

今回はGo言語における型表現について、ほんの一部だけ紹介しました。

  • プリミティブな型を作らない
  • 最初から汎用的な型を作らない
  • 完全コンストラクタで型が示す表現力を高める
  • ファーストクラスコレクションで操作を制限する
  • 列挙であるiotaも別名で型定義して利用する
  • 構造体埋め込みを上手く利用する

関連しているオブジェクト指向エクササイズも面白いので、気になる方はぜひ調べてみてください。

型を厳格にする意義

型を定義するという行為は、ルールや制限を明確かつ厳格化することであり、表現力を豊かにする行為そのものです。

オブジェクトには何かしらのルールが存在し、型そのものや、型自体で知識やルールを表現/体現する効果があります。

最近はドメイン駆動設計が随分と盛り上がりを見せており、ドメインレイヤの実装では、ドメインを型と型のメソッドで知識を表現/体現します。まさにオブジェクト指向プログラミングです。

型の恩恵を受けられるチャンスを増やしていきましょう。

最後に、この言葉をおくります。

ゆるゆるな型を使うだけでは、せっかくの型がかたなし

ありがとうございました!


SPEEDAプロダクトユニットではエンジニアを募集しています

まずは気軽に話を聞いてみたい!という方はtwitter @tkitsunai 宛にリプライをしてみてください。型が大好きな方もこれから取り組みたい方も、様々な強みを持った弊チームで働きませんか?

応募はこちらから。

apply.workable.com

Kotlinを3ヶ月書いて感じたJavaとの違い

はじめに

  • SPEEDA PDT歴3ヶ月の相川です
  • 今回はJavaを2年くらい書いていた私が、3ヶ月間で感じたKotlinの特徴を列挙していこうと思います

Kotlinの特徴

  • 型推論
  • データクラス
  • nullable
  • 検査例外の話
  • returnを明示的に書かなくても良い
  • 拡張関数
  • リスト操作の評価について
  • kotlin corutine

型推論について

  • Kotlinでは変数を定義する際に、varもしくはvalを使います
  • その際に、型推論が採用されているおかげで、Javaのように型を宣言しなくても良くなります
  • ただ、Java10から型推論は使えるようになってるので、あまりKotlinだけに言える話ではなくなっているかと思います
// Javaで書く場合
String name = "太郎";

// Kotlinで書く場合
var name = "太郎";

データクラスについて

  • Kotlinのデータクラスは非常に綺麗です
  • Javaですと、constructorやgetterを定義する必要があり、どうしてもデータクラスの記述量が増えがちです。(もちろんlombokを使えば、アノテーションで少しは減らすことはできると思います)
  • Kotlinでは、下記のように定義するだけでインスタンスを生成できますし、Getterを定義しなくてもプロパティ名からアクセスすることができます。
//  Kotlinのデータクラスはこれだけ
data class Sample(val name: String, val num: Int)

nullable

  • Kotlinを書くようになってから圧倒的にNull Pointer Exceptionにで会う回数が減りました。(嬉しいような...寂しいような)
  • というのも、Kotlinでは何も気にせず変数にnullを代入しようとすると怒られるからです。
  • なので、nullをどうしても使いたい場合は変数名の後ろに?をつける必要があるのですが、このおかげでnullが入りうる箇所を限定的にできるということがNull Pointer Exceptionを防ぐことに繋がっているのです(余計な防御的プログラミングを減らせるというのも、メリットだと思っています)
// コンパイルエラー
val hoge: String = null

// これは大丈夫
val hoge?: String = null

検査例外の話

  • Javaでは検査例外(RuntimeExceptionとそのサブクラスを除く例外たちのこと)を投げるメソッドを呼び出す際に、try-catchするなどハンドリングしてあげる必要がありました。
  • この検査例外という仕組みは現代では嫌われる傾向があり、Java以降はほとんどの言語で採用されてないようです
  • kotlinもその流れを汲んで検査例外というものを導入していないので、Javaではtry-catchを書いていた箇所も、kotlinでは書かなくてもコンパイルエラーにはなりません。

returnを明示的に書かなくても良い

  • Kotlinでは一つの式からなる関数はreturnを省略することができます
  • Javaでは返り値のある関数は全てreturnしていたかと思いますが、=で書けるようになります
  • また、返り値の型も省略できるので、簡単な関数を書く際などには=で書き、返り値の型も省略してしまった方が個人的には読みやすいと感じました。
data class Sample(val name: String, val num: Int)

fun hoge()  = Sample("hoge", 1);

fun piyo() : String = hoge().name

拡張関数と拡張プロパティ

  • Kotlinには継承をせずとも、既存のクラスを拡張できる機能が実装されています(拡張関数と拡張プロパティ)
  • これらを使うことで、既存のクラス(継承禁止クラスを含む)に、新たに関数やプロパティを追加することができます。

拡張関数

  • まず、拡張関数を見てみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加する関数名を定義します。
  • あとは、呼び出し側で関数を呼び出してあげるだけでokです
fun main() {
 // 拡張関数の呼び出し
    println("hoge".secondChar().toUpperCase())
 
}

// 拡張関数を定義する
fun String.secondChar() = this.get(1)

拡張プロパティ

  • 続いて拡張プロパティを使用してみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加するプロパティ名を定義します。
  • 拡張プロパティの場合、デフォルトのgetterが用意されないので、常に定義してあげる必要があるので、定義します。
  • あとは、呼び出し側からプロパティ名でアクセスできるようになります。
  • また必要に応じて、setterの定義もできます(その場合は、変数宣言をvarでする必要があります)
fun main() {
 // 拡張プロパティの呼び出し
    println("fuga".thirdChar)
}

// 拡張プロパティを定義する
val String.thirdChar : Char 
    get() = get(2)
  • これらの機能が追加されてる背景としては、Kotlinのメインテーマの1つとして、既存のコードとスムーズな統合というものがあります。
  • KotlinをJavaプロジェクトに統合するときに、編集対象外の既存のコードを修正せずにKotlinの長所を利用できるようにしたかったというのがこの拡張関数・拡張プロパティが追加された動機のようです。 参考

リスト操作の評価について

  • 説明に先立ってサンプルコードから書いてしまいます
  • 今までJavaを書いていた人間からすると、リスト操作をするとなると下記のような実装になると思います
fun main() {
 val list = listOf(1, 2, 3, 4, 5)
 list.filter{ ... }.map{ ... }
}
  • この場合、KotlinのIterableに対する操作とみなされ、先行評価型となります。
  • なので、下記のような実験的なコードを実行してみると・・・
fun eagerEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.filter {
        println("iterable.filter : $it")
        it % 2 == 0
    }.map {
        println("iterable.map : $it")
        it * 2
    }.first()
    println(result)
  • コンソールを確認すると下記のような評価結果になります
iterable.filter : 1
iterable.filter : 2
iterable.filter : 3
iterable.filter : 4
iterable.filter : 5
iterable.map : 2
iterable.map : 4
4
  • 前から順番に評価されているのがわかります
  • 先行評価の場合、まず全件に対してfilterし、次に残ったものに対してmapして、最後にfirstしてるという具合です
  • これを遅延評価に変更したい場合はiterableをsequenceにします
fun lazyEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.asSequence().filter {
        println("sequence.filter : $it")
        it % 2 == 0
    }.map {
        println("sequence.map : $it")
        it * 2
    }.first()
    println(result)
}
  • 変更後の結果はこんな感じです
sequence.filter : 1
sequence.filter : 2
sequence.map : 2
4
  • 遅延評価になっているのがわかります
  • 1件ずつ、順番にfilter→map→firstしたら終了といった具合に評価されています
  • Javaを書いていた人にとっては、今まで遅延評価されてきたものが急に先行評価に変わるので、予期せぬ結果を招くという話になるかもしれないです
  • ですので、遅延評価なのか先行評価なのかきちんと理解してCollection操作をしてあげる必要がありそうですね 参考

coroutineの話

  • Javaでは並列実行するように実装をしようとなった場合、CompletableFutureとかParallelStreamとか他にもいくつか標準APIの選択肢があると思います
  • 一方で、Kotlinには 1.3系から正式に使えるようになったcoroutineというものがあります。その話を最後にしようと思ったのですが、2回前に原田さんがブログでまとめてくださってるのでそちらを見てもらえればと・・・ KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

  • あと、詳しい説明は 公式サイトを見て頂ければと思います

まとめ

  • 今回は簡単にKotlinの特徴をまとめてしまいましたが、他にもKotlinとJavaの違いはまだまだあります。
  • この記事を読んで、少しでもKotlinに興味を持ってもらえたら嬉しいです。
  • また補足ですが、Kotlinは同一プロジェクト内でJavaファイルと共存できるので(コンパイルかけると全部classファイルになるので)、今Javaしか書いてない人は少しずつ書いてみるのも手段ですし、IntelliJ IDEAというIDEではJavaのソースコードをKotlinに変換する機能もついているので、変換してみたらどうなるのか見るだけでもイメージが湧くのかなと思います。

Gaugeのsetupとteardownステップを用いて効率的に読みやすいテストを書く

こんにちは!SPEEDA開発チームの工藤です。

大分時間が開いてしまいましたが、Gaugeシリーズの第四回目です。

今回はe2eテスト書く際には必須であろうSet Up/Tear Down Stepsを、Gaugeではどのように実現できるのかをSPEEDA開発チームでの実例も交えてお伝えできればと思います。

過去3回分の記事はこちらから↓

  1. Gauge Test Automation Toolとアジャイル開発
  2. GaugeのConceptを用いてテストシナリオをより仕様書のように記述する
  3. GaugeのParameterを使いこなす

GaugeにおけるSet Up/Tear Down Stepsの実現方法

Gaugeには、Set Up StepsやTear Down Stepsを実現できる手段がいくつか用意されています。

用途に合わせて下記のいずれかを選択して使います。

  • Contexts
  • Tear Down Steps
  • Execution Hooks

上記3つについて順番にお伝えしていきます。

Contexts

GaugeではSet Up StepsをContextsと呼んでいます。 context stepsを使用することで、SpecファイルのScenarioの実行に必要な条件を指定できます。

Specファイル内先頭のScenarioの前にStepを記述すると、そのStepが全てのScenarioの最初に実行されます。(GaugeにおけるSpecやScenarioなどのワードに馴染みのない方は第一回目の記事をご覧ください)

下記例ではScenario 1の前に「ユーザーAでログイン」、「プロジェクトページに遷移する」というStepを記述していて、このStepがScenario 1、2の最初に実行されます。

# プロジェクトの削除

context steps
* ユーザーAでログイン
* プロジェクトページに遷移する

Scenario 1
## 1つのプロジェクトを削除
* プロジェクト"project_a"を削除
* プロジェクト"project_a"が削除されていることを確認

Scenario 2
## 複数のプロジェクトを削除
* プロジェクトリスト上の全てのプロジェクトを削除
* プロジェクトリストが空であることを確認

各Scenarioで必要だがあまり仕様的には重要ではないセットアップ処理などをContextsにまとめることで、Specificationの冗長さをなくすことができます。

SPEEDA開発ではSpecファイルをページ単位で切ることが多いのでテスト対象のページに遷移するステップや、ログイン処理をここに書くことが多いです。

Tear Down Steps

Tear Down StepsはSpecファイル内最後のScenarioの後に記載します。 Scenarioの実行を終えるために必要なStepがあればTear Down Stepsとして定義します。

3つ以上のアンダースコアを記述することでTear Down Stepsを指定することができます。

___
* Tear down step 1
* Tear down step 2
* Tear down step 3

下記の例では、アンダースコアの後に記述されている「ユーザーAでログアウト」と「ユーザーAを削除」がTear Down Stepsになります。このSpecificationが実行されると、下記の順で実行されます。

  1. Contextsの実行
  2. 1つのプロジェクトを削除のScenarioの実行
  3. Tear Down Stepsの実行
  4. Contextsの実行
  5. 複数のプロジェクトを削除のScenarioの実行
  6. Tear Down Stepsの実行
# プロジェクトの削除

* ユーザーAを作成
* ユーザーAでログイン

Scenario 1
## 1つのプロジェクトを削除
* プロジェクト"project_a"を削除
* プロジェクト"project_a"が削除されていることを確認

Scenario 2
## 複数のプロジェクトを削除
* プロジェクトリスト上の全てのプロジェクトを削除
* プロジェクトリストが空であることを確認
___
ここからTear Down Steps
* ユーザーAでログアウト
* ユーザーAを削除

SPEEDA開発では実はあまりTear Down Stepsは使っていません、Tear Downとしては後述するExecution Hooksを使う方が多いです。

Execution hooks

Execution hooksを使うとSuite,Spec,Scenario,Stepの単位で任意のテストコードを実行することができます。

ContextsやTear DownはSpecファイル毎且つScenarioにしか定義できませんが、Execution HooksはSpecファイルを跨いで且つ様々な単位で定義できます。

import com.thoughtworks.gauge.*

class ExecutionHooksExample {
    @BeforeSuite
    fun beforeSuite() {
        // 全てのテスト実施前の最初に一度だけ実行される処理
    }

    @AfterSuite
    fun afterSuite() {
        // 全てのテスト実施後の最後に一度だけ実行される処理
    }

    @BeforeSpec
    fun beforeSpec() {
        // 各Specファイルのテスト実施の先頭に一度だけ実行される処理
    }

    @AfterSpec
    fun afterSpec() {
        // 各Specファイルのテスト実施の最後に一度だけ実行される処理
    }

    @BeforeScenario
    fun beforeScenario() {
        // 各Scenario実施前に実行される処理
    }

    @AfterScenario
    fun afterScenario() {
        // 各Scenario実施後に実行される処理
    }

    @BeforeStep
    fun beforeStep() {
        // 各Step実施前に実行される処理
    }

    @AfterStep
    fun afterStep() {
        // 各Scenario実施後に実施される処理
    }
}

SPEEDAでは下記のような処理はBefore Suiteで実行しています

  • 一度だけ設定ファイルを読み込む
  • Read-Onlyデータの投入

また下記のような処理はAfter Scenarioで実行しています

  • ログアウト
  • WebdriverのClose処理

その他にもDBやモックのセットアップ処理もExecution Hooksを使用して任意のタイミングで実行しています。

Execution hooksを特定のTagが指定されている場合のみ実行されるようにすることも可能です。その場合は下記のように指定します。

// tag1 または tag2がついているScenarioでのみ前処理として下記を実行
@BeforeScenario(tags = {"tag1, tag2"})
fun setupDataBase() {
    // Code for before scenario
}

まとめ

GaugeはExecutable Specificationを謳っていてSpecファイルやScenarioは実行可能な"仕様書"である必要があります。

SPEEDA開発では今回ご紹介した機能を使ってSpecificationファイルの記述を出来るだけ簡潔にして、より仕様書として読みやすくするよう心がけています。

KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

ユーザベースインターンの原田です.大学院で研究しながら京都でユーザベースのインターンをさせて頂いており,今回初めてブログを書かせて頂きます!

題名にある通り,今回KotlinのCoroutineを使用した並列数を指定して関数を実行できるライブラリ(ParallelExecutor)を作成しましたので,そのことについて投稿させて頂きます.

背景

外部のAPIを呼びだす処理を並列で呼びだしたいが,相手側の都合(サーバーへの負荷等)により並列数を制限したい状況が発生しました.しかしCoroutineは大量に起動出来てしまい,通常では並列数に制限をかけることが出来ません.そこでこれを実現する為に,ParallelExecutorを作成することにしました.

本記事の内容

本記事の内容は以下の通りです

  • そもそもCoroutineとは何か

  • Coroutine間で値を転送できるChannelについて

  • ParallelExecutorの説明

    Coroutine

    Coroutineは一言で言うと,軽量なスレッドです.そして以下のような特徴を持っています.

  • 中断が可能な計算インスタンスである
  • 特定のスレッドに束縛されない

ここではまずCoroutineの作成方法を示し,その後でこれらの特徴について説明します.

Coroutineの作成方法

下図はCoroutine作成のイメージです. f:id:harada-777:20191015180809p:plain:w400:left
CoroutineはCoroutine builderで作成することができます.しかし,その際にはCoroutineScope内で作成する必要があります.CoroutineScopeとはCoroutineが実行される仮想的な場所のようなものです.CoroutineはCoroutineScope内でのみ実行可能です. 実際のコードを作成してみます.Coroutineを使用する為に以下の依存を追加して下さい.

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC"
}

以下はサンプルコードです.

fun main() {
    runBlocking {
        val job = launch {
            delay(1000L) // 1秒待つ
            println("World!") // print after delay
        }
        println("Hello,")
        delay(2000L) // プログラムを終了させない為にmain thread で2秒待つ
    }
}

runBlockingは現在のスレッドをブロックしてCoroutineScopeを作り出します.そしてlaunchはCoroutine builderの1つであり,Coroutineを生成することができます.launchはデフォルトでは親のCoroutineScopeで実行するCoroutineを作成します.ここで親のrubBlockingのCoroutineScopeであるmain threadで実行するCoroutineを作成しています.引数でどのCoroutineScopeで実行するかを指定することもできます.

次にCoroutineの特徴について説明します.

中断が可能な計算インスタンス [1]

coroutineが中断可能な計算インスタンスであることについて説明します.ここで計算インスタンスとは処理を記述したコードブロックのことを指しています.よってスレッドは大量に起動できませんが,Coroutineは以下のように10000個など大量に起動しても問題ありません. またCoroutineが中断可能とは,Cortouineの処理を途中で止めて,スレッドを解放することができることを意味します.その中断はsuspend関数と呼ばれる関数で行われます.以下のコードを見てください.

suspend fun apiCall() {
    println("ApiCall")
    delay(1000)
    println("Return")
}
fun main(args: Array<String>) {
    runBlocking {
        println("main1")
        val job = launch {
            apiCall() // coroutineを中断し、スレッドを解放する
        }
        delay(500) //delay1
        println("main2")
    }
}

このdelayはsuspend関数です.呼ばれるとCoroutineを中断しスレッドを解放しする関数です.またsuspend関数はsuspend修飾子を使って自分で宣言すること可能です.ここではcallApiがそれに当たります.このコードは以下の図のように実行されます. f:id:harada-777:20191015180549p:plain
ポイントはsuspend関数を呼びsuspend関数であるcallApiのdelayが呼ばれた後にスレッドを解放している点です.これがCoroutineの特徴でスレッドをブロックすることなく処理を実行できます.jobは処理の集合を表すインスタンスです. このコードの実行結果は以下のようになります.

main1
ApiCall
main2
Return

特定のスレッドに束縛されない

Coroutineは特定のスレッドに束縛されません.つまりCroutineとスレッドは1対1対応ではありません.Croutineはsuspend関数によって中断しスレッドを解放,そしてそのとき空いているスレッドを確保し再開されながら実行を行います.

f:id:harada-777:20191015182617p:plain:w400:left
こうすることでより1つのスレッドを有効に活用することが可能です.

Channelとは

Channelとはキューの一種です.Channelを用いることがCoroutine間で値を転送することが出来ます. f:id:harada-777:20191015181603p:plain:w400:left
channelのsendを呼ぶことで,値をchannelに書き込みchannelの片方でrecieveを呼ぶことでその値を順に呼び出すことができます.実際のコードは以下の通りです.

fun main() {
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for (x in 1..5){
                channel.send(x * x) //値をchannelに書き込む
            }
        }
        repeat(5) { println(channel.receive()) } //値を取り出す
        println("Done!")
    }
}

このコードは以下のように書くこともできます.closeは特別な関数でchannelの終了を表すtokenを送ることができます.読み取り側でこのtokenが読み取られると繰り返しが終了し,全ての要素が読み取られたことを保証できます.

fun main() {
    runBlocking {
        val channel = Channel<Int>()
        launch {
            for (x in 1..5) {
                channel.send(x * x)
            }
            channel.close()
        }
        for (item in channel) {
            println(item)
        }
        println("Done!")
    }
}

ParallelExecutorについて

ここで今回作成したParallelExecutorについて説明します.使用は以下の通りです.

  • 並列数を指定してsuspend関数を並列に実行することができる

  • ParallelExecutorのインスタンスを共有することで,共有した部分で並列数を制御することができる

  • ParallelExecutorには入力として引数にシーケンスと実行したいsuspend関数を渡すことができる

  • 全てのシーケンスの要素は,ParallelExecutorに渡した関数に渡され実行される

  • 結果はChannelにResult型で書き込まれ,ParallelExecutorはそのChannelを返す

  • 途中で例外が発生すると自動的にchannelは閉じられ,channel最後の要素がその例外を持っている

実際のコードはこちらです. https://github.com/uzabase/ParallelExecutor/blob/master/src/main/kotlin/ParallelExecutor.kt

ParallelExecutorではCoroutineの並列数を指定する為にChannelをセマフォとして用いています.[2]セマフォとは共有資源に対するアクセス可能な数を示すものです. f:id:harada-777:20191015181641p:plain:w400:left
ParallelExecutorではセマフォに値を送れたCoroutineのみが処理を可能にしています.Channleのsendはsuspend関数なのでCroutineの処置を中断ができます.従ってCoroutineの並列数を制限することができます. 大きな流れを説明します.① でまずCoroutineがinputSeqの大きさ分起動します.その次に②でsemaphoreに値を送ろうとします.③semaphoreに値を送れたCoroutineは処理の開始を行い,④実際の処理が走ります. ParallelExecutorのが行なっていることのイメージが以下の図です. f:id:harada-777:20191015181807p:plain ④の中身を説明します. まずGlobalScopeでCroutineをinputSeq(入力として与えたシーケンス)の数起動をさせます.GlobalScopeはデフォルトでは用意されているBackground Thread Poolのスレッドを使用してCoroutineScopeを作成します.コードでは以下の部分です.

job = GlobalScope.launch(handler) {
            inputSeq.forEach { input ->
                launch { 
                            ・
                            ・

そして次に自分で用意したSenderThreadPoolをCoroutineScopeとして指定して,callFunction(input)を呼ぶCoroutineを作成します.コードでは以下の部分です. SenderThreadFactoryの定義

class SenderThreadFactory : ThreadFactory {
    private var count = 0
    override fun newThread(r: Runnable): Thread {
        return Thread(r, "sender-thread-" + ++count)
    }
}

SenderThreadFactoryによって作成されたPoolを用いてDispatcherを生成(これを渡すことでCoroutineのCoroutineScopeをこのPoolに指定できる)

private val dispatcher = Executors.newFixedThreadPool(capacity, SenderThreadFactory()).asCoroutineDispatcher()

Coroutineを起動する(withContextは値を返すCoroutineBuilderの一つ)

withContext(dispatcher) {
                        runCatching {
                            callFunction(input)
                        }
                    }

そしてその結果をResultに格納し,resultChに送り,semaphoreの値を1つ取り出します.そうすることで待機しているCoroutineが動き出します.コードでは以下の部分です.

 }.let { result ->
    resultCh.send(result)
    semaphore.receive()
    result.onFailure {
    throw it
}

またcallFunctionで例外が発生した場合は,例外ハンドラに処理が行き,jobのキャンセルが行われ残りの処理が素通りされるようになっています.そしてresultChとsemaphoreを閉じます.以下が例外ハンドラのコードです.

val handler = CoroutineExceptionHandler { _, exception ->
    exception.printStackTrace()
    job?.cancel()
    resultCh.close(exception)
    channel.close()
   }

jobのキャンセルは以下のように実装されています.jobのキャンセルが呼ばれるとisActiveがfalseになります.よって素通りしたい処理をif分で囲っています.

launch {
    if (isActive) {
        semaphore.send(Unit)
        withContext(dispatcher) {
                ・
                ・

まとめ

今回作成したライブラリとCoroutine周りの説明をさせて頂きました. 本ライブラリの実際の使用方法はこちらをご参照下さい. https://github.com/uzabase/ParallelExecutor

参考文献 [1] https://qiita.com/k-kagurazaka@github/items/8595ca60a5c8d31bbe37 [2] https://qiita.com/k-kagurazaka@github/items/0c30cc04dcef306ed3c7

ReactとReactHooksを使って、Flux的なアーキテクチャを実現する

こんにちは。SPEEDA開発チームの冨田です。

昨今のフロントエンドでは、Fluxというアーキテクチャが利用されることが多くなってきています。SPEEDAでもVueを使っている画面がありますが、そこではVuexというVue向けのFluxライブラリで状態管理をしています。

Fluxではデータの流れを一方向にすることで見通しのよい設計が行えるようになります。

今回は、素のReactを使ってデータの流れを単一方向にする設計を紹介します。

今回作ってみるもの

Todoアプリを作ってみましょう。 以下のようなことができる画面を作ります。なお、今回はデータの永続化は考えないものとします(つまり、ウェブページを更新すると、全て消えてしまいます)。

  • Todoを追加することができる
  • 全てのTodoのリストを見ることができる
  • Todoの完了状態を変更することができる

セットアップ

簡単のため、CodeSandboxなどで、React + TypeScriptなプロジェクトを作っておくとよいでしょう。以下は、今回作成したプロジェクトになります。

codesandbox.io

1. GettersとActionsをつくる

Todoアプリを実現するためのカスタムフックを作りましょう。

Gettersはコンポーネントからアクセスできるようにする(露出する)データの集まりです。Actionsはそれを呼び出すことで、なんらかの副作用を起こし、Gettersを更新する関数の集まりです(正確には、内部のStateを変更することで、Gettersが更新される)。これらはFluxで定義されているものと同様の概念だと考えていただいてよいでしょう。

今回はGetters, Actionsとして以下のものを定義しています。

  • todos: Todoのリストを保持する配列
  • add: 文字列を受け取り、Todoのリストに新規のTodoを追加する
  • update: idと真偽値を受け取り、リストの中の該当するTodoの完了状態を変更する
import { useState } from "react";

interface Todo {
  description: string;
  done: boolean;
}

interface State {
  todos: Todo[];
}

interface Getters {
  todos: Array<Todo & { id: number }>;
}

interface Actions {
  add(description: string): void;
  update(id: number, done: boolean): void;
}

export type Store = Getters & Actions;

export function useTodo(): Getters & Actions {
  const [state, setState] = useState<State>({ todos: [] });
  const { todos } = state;

  function add(description: string) {
    setState({
      todos: [...todos, { description, done: false }]
    });
  }

  function update(id: number, done: boolean) {
    setState({
      todos: todos.map((it, i) => (i !== id ? it : { ...it, done }))
    });
  }

  return {
    todos: todos.map((it, id) => ({ ...it, id })),
    add,
    update
  };
}

2. GettersとActionsをどこのコンポーネントからでもアクセスできるようにする

useContextを使います。 useContextは、createContextにより、あらかじめ生成しておいたコンテキストにどこからでもアクセスできるようにするReactHooksです。

例:Appコンポーネント内で、Context.Providervalue propsに渡したオブジェクトがChildElementコンポーネント内のuseContextで得られます。

const Context = createContext({});

const ChildElement = () => {
  const { foo } = useContext(Context);
  return (
    <div>{foo}</div>
  );
}

const App = () => {
  return (
    <Context.Provider value={{ foo: "bar" }}>
      <ChildElement />
    </Context.Provider>
  );
};

上記の仕組みを利用して、GettersとActionsに対してどこからでもアクセスできるようにし、さらにGettersに変更が行われると、再レンダーされるようにします。

createContextでコンテキストを作成します。ここでは引数に{} as anyとやってしまっていますが、Providerでvalueを提供しないときに初期値として得られるものなので、これでよいでしょう。

StoreProviderコンポーネントは、Context.Providerをラップしたものです。コンテキストにuseTodoで生成したGettersとActionsを保存し、子コンポーネントをレンダリングします。 ユーザーに、Contextやカスタムフックを意識させないための工夫です。

useContextもReactのuseContextをラップしたものです。こちらも、Contextを意識させないために作っています。

ここで定義したStoreProviderとuseContextを利用することで、画面のコンポーネントではカスタムフックについて意識することなく、useContextを呼ぶだけでなんか知らんけどGettersとActionsが使える、という状態になります。

import React, { createContext, useContext as useContextOriginal, FC } from "react";
import { useTodo, Store } from "./Store";

interface AppStore {
  todo: Store;
}

const Context = createContext<AppStore>({} as any);
const { Provider } = Context;

export const StoreProvider: FC = ({ children }) => {
  const todo = useTodo();
  return <Provider value={{ todo }}>{children}</Provider>;
};

export function useContext() {
  return useContextOriginal(Context);
}

3. Todoを表示するコンポーネントを作る

ユーザーが入力しているdescriptionについてはこのコンポーネントで管理するようにしています。これも上で作ったGetters, Actionsで管理してもよいのですが、useStateとの共存も示したかったので、今回はここに書きました。

実装自体は、普通のulとinput, buttonを使ったコンポーネントですが、useContextを利用することで、todos, add, updateにアクセスできるようになりました。

追加ボタンが押され、addが呼び出されることで、todosが更新され、再レンダリングが行われます。

チェックボックスが変更されることで、updateが呼び出され、todosが更新され、再レンダリングが行われます。

Getters(todos)は、画面をレンダリングするために必要なもの。Actions(add, update)は呼び出すことで、状態を変更し、再レンダリングを促すもの。データの流れが単一方向になっているのがわかるでしょうか。

import React, { FC, useState } from "react";
import { useContext } from "./Flux";

export const TodoComponent: FC = () => {
  const [currentDescription, setCurrentDescription] = useState("");
  const { todo: { todos, add, update } } = useContext();

  function onAddClick() {
    setCurrentDescription("");
    add(currentDescription);
  }

  return (
    <>
      <ul>
        {todos.map(({ id, description, done }) => (
          <li key={id}>
            {description}
            <input
              type="checkbox"
              checked={done}
              onChange={e => update(id, e.target.checked)}
            />
          </li>
        ))}
      </ul>
      <input
        value={currentDescription}
        onChange={e => setCurrentDescription(e.target.value)}
      />
      <button onClick={onAddClick}>追加</button>
    </>
  );
};

4. アプリコンポーネントを作る

最後に、アプリコンポーネント(一番外側のコンポーネント)を作ります。

2で作ったStoreProviderがここで登場します。 3で作ったTodoComponentをStoreProviderで包むだけです。 これにより、内部で状態が変化したときに、再レンダリングが走ります。

import React from "react";
import { render } from "react-dom";
import { TodoComponent } from "./TodoComponent";
import { StoreProvider } from "./Flux";

const App = () => {
  return (
    <StoreProvider>
      <TodoComponent />
    </StoreProvider>
  );
};

render(<App />, document.getElementById("root"));

5. テストを書く

順番が前後して申し訳ないのですが、最後におまけとして、Getters, Actionsのテストの書き方をご紹介します。

ReactのカスタムフックはReactコンポーネント内でしか呼び出すことができません。 すなわち、テスト用のReactコンポーネント内でカスタムフックを呼び出し、呼び出した結果をテストすることになるでしょう。

react-domが提供するactという関数のコールバックでレンダリングやカスタムフックが生成したGetters, Actionsを取得することができます。

今回はbeforeEachで、テスト用のコンポーネントをマウントしさらにuseTodoが返すGetters, Actionsを変数に保持しています。Specification内では得られたGetterやActionsのテストのみに注力しています。

注意点としてはact内でActionsを呼び出せるのは1回で、何度も呼び出したい場合はactを何度も書く必要があることです。actを抜けると、状態がGetterに反映されるイメージです。

テストケースは3つです。 ひとつめはtodosの初期状態が空配列であること。 ふたつめはaddすると、todosに要素がひとつ追加されること。 みっつめは、addした要素に対しupdateをかけると、その要素のdoneの状態が、指定したとおりに変更されること。

import React from "react";
import { render } from "react-dom";
import { act } from "react-dom/test-utils";
import { useTodo, Store } from "../Store";

describe("#useTodo", () => {
  let container;
  describe("useTodo called.", () => {
    let state: Store;
    beforeEach(() => {
      container = document.createElement("div");
      const App = () => ((state = useTodo()), null);
      act(() => {
        render(<App />, container);
      });
    });
    afterEach(() => (container = null));

    it("should have action and state.", () => {
      expect(state.todos).toEqual([]);
    });

    it("should add todos when 'add' called.", () => {
      act(() => state.add("foo"));
      expect(state.todos).toEqual([{ description: "foo", done: false, id: 0 }]);
    });

    it("should update done status when 'update' called.", () => {
      act(() => state.add("foo"));
      act(() => state.add("bar"));
      act(() => state.update(0, true));
      expect(state.todos).toEqual([
        { description: "foo", done: true, id: 0 },
        { description: "bar", done: false, id: 1 }
      ]);
    });
  });
});

以上のように、素のReactだけでFlux的なアーキテクチャを実現することができました。 よろしければ、ぜひお試しください。

pytest-mock使ってハマったこと

こんにちは。

SPEEDA開発チームの掛川です。

現在、私が参画しているプロジェクトではPythonを使ってサービスの開発を行なっています。 私自身、Pythonを書くのは今回が初めてなのですが、 テストを書く際にハマったことについて記事にしていきたいと思います。

環境といろいろ

・環境
    OS     Mac OS X  10.14.5
    python 3.7.3
・ライブラリ
    pytest 5.1.2
    pytest-mock 1.11.1


私が今まで参画していたプロジェクトでは、モックライブラリはMockKを使用していた(使用言語はKotlinでした)のですが 「MockitoやMockKと同じように表現したい時にpytestではどうやって書いたらいいの?」 と疑問に思いハマったことの中から今回は以下の2つの内容にフォーカスしていきます。

  • classメソッドをモックしたい時
  • 引数に応じて返す値を変えたい時


classメソッドをモックしたい時

今回は請求書を発行するシステムを想定しました。

f:id:kkyki:20191011013258p:plain
請求書のイメージ

まずは、請求書の発行を行うためのusecaseとusecaseのテストを書いてみました。
このusecaseで行いたいことは4つです。

  1. 渡されたuser_idをもとに顧客情報(customer)を取得してくる
  2. 渡されたuser_idをもとに購入情報(Details)を取得してくる
  3. 購入情報(Details)から購入の合計金額を算出する
  4. 請求書(Invoice)の発行に必要な情報をControllerに返す
class InvoiceUseCase:

    @classmethod
    def publish(cls, user_id):
        # 1.顧客情報(customer)の取得
        customer = CustomerGateway.get(user_id)
        # 2.購入情報(Details)を取得
        details = SalesHistoriesGateway.get(user_id)
        # 4. 請求書情報(Invoice)を返却
        return Invoice(
            billing_date=datetime.date(2019, 1, 1),
            customer=customer,
            details=details,
            total=details.total())  # 3. 合計金額を算出
class TestInvoiceSheetUseCase:

    def test_publish(self, mocker):
        # Customerをモックする
        customer = mocker.Mock(Customer)
        customer_gateway_mock = mocker.patch.object(
            CustomerGateway,
            'get',
            return_value=customer)

        # Detailをモックする
        detail1 = mocker.Mock(Detail)
        detail2 = mocker.Mock(Detail)
        details = Details(details=[detail1, detail2])
        sales_histories_gateway_mock = mocker.patch.object(
            SalesHistoriesGateway,
            'get',
            return_value=details)

        expected = Invoice(
            billing_date=datetime.date(2019, 1, 1),
            customer=customer,
            details=details,
            total=details.total())

        assert InvoiceUseCase.publish('user_id') == expected

        # CustomerGatewayのgetメソッドが指定した引数'user_id'で1回呼ばれたことの検証
        customer_gateway_mock.assert_called_once_with('user_id')
        # SalesHistoriesGatewayのgetメソッドが指定した引数'user_id'で1回呼ばれたことの検証
        sales_histories_gateway_mock.assert_called_once_with('user_id')

MockitoやMockKでclassメソッドをモックしたい時には以下のように
when(モックしたいクラスオブジェクト.モックしたいメソッド).thenReturn(返したい値);
every { モックしたいクラスオブジェクト.モックしたいメソッド } return 返した値
と書くことができると思いますが、 pytest-mockでは
mocker.patch.object(モックしたいクラスオブジェクト, 'モックしたいメソッド', return_value=返したい値)
または
mocker.patch('モックしたいクラスオブジェクト.モックしたいメソッド', return_value=返したい値)
と書くことができます。

引数に応じて返す値を変えたい時

上記で書いていたものは全てreturn_value を使っており、 モックしたいメソッドは毎回固定の値を返すように設定していました。
ですが、メソッドに渡された引数に応じて返す値を変えたい時はないでしょうか?
そういう場合は、side_effect を使います。
side_effect を使うと、モックしたいメソッドの代わりに side_effect で定義したメソッドが呼び出され(引数はモックしたいメソッドと合わせる)、そのメソッドが返す値がモックの返す値として使われます。


次に、顧客情報を扱うgatewayとgatewayのテストを書いてみました。
このgatewayで行いたいことは以下です。

  • 複数のuser_idを受け取り、それに対するそれぞれの顧客情報の集合体(Customers)を返す
class CustomerGateway:

    @classmethod
    def get_customers(cls, user_ids):
        return Customers(list(map(
            lambda user_id: CustomerDriver.find_by(user_id), user_ids)))
    def test_get_customers(self, mocker):
        def find_by_id(user_id):
            if user_id == 'user_id1':
                return Customer(
                    id='user_id1',
                    name='Alice',
                    zip_code=77777,
                    address='Anaheim, CA',
                    number=123456789)
            elif user_id == 'user_id2':
                return Customer(
                    id='user_id2',
                    name='Bill',
                    zip_code=88888,
                    address='New York, NY',
                    number=111111111)
            else:
                self.fail('invalid user_id!!')

        mocker.patch.object(
            CustomerDriver,
            'find_by',
            side_effect=find_by_id)

        assert CustomerGateway.get_customers(['user_id1', 'user_id2']) == Customers([
            Customer(
                id='user_id1',
                name='Alice',
                zip_code=77777,
                address='Anaheim, CA',
                number=123456789),
            Customer(
                id='user_id2',
                name='Bill',
                zip_code=88888,
                address='New York, NY',
                number=111111111)
        ])


mockerについて

mockerはPythonの標準ライブラリであるmockライブラリの薄いラッパーで、unittestで提供しているモックパッケージと同じ引数をサポートしており、 テストメソッドに引数として渡すことで使うことができます。
また、mockerの便利なところはpatchしたクラスメソッドを初期化する必要がないところです。 mockerでモックしたインスタンスは実行対象のテストメソッドの実行後に自動的にリセットされます。

mocker.patch.object()mocker.patch() はどちらを使ってもモックすることは可能ですが、両者の違いは
mocker.patch.object() は外部から注入されたインスタンスに対してモックしますが、 mocker.patch() では渡された文字列から内部で対象のクラスとメソッドの参照を取得しモックします。


上で書いたgatewayに、指定されたuser_idを受け取り対応する顧客情報(Customer)を返すメソッドを追加して
mocker.patch.object()mocker.patch() のそれぞれを使ってテストを書いてみました。

class CustomerGateway:

    @classmethod
    def get(cls, user_id):
        return CustomerDriver.find_by(user_id)
<mocker.patch.object()を使った場合>
class TestCustomerGateway:

    def test_get_customer(self, mocker):
        customer = Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)
        # mocker.patch.object(
        #    モックしたいクラスオブジェクト,
        #    'モックしたいメソッド',
        #    return_value=返したい値)
        m = mocker.patch.object(
            CustomerDriver,
            'find_by',
            return_value=customer)

        assert CustomerGateway.get('user_id') == Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)

        m.assert_called_once_with('user_id')
<mocker.patch()を使った場合>
class TestCustomerGateway:

    def test_get_customer(self, mocker):
        customer = Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)
        # mocker.patch(
        #    'モックしたいクラスオブジェクト.モックしたいメソッド',
        #    return_value=返したい値)
        m = mocker.patch(
            'app.main.driver.customer_driver.CustomerDriver.find_by',
            return_value=customer)

        assert CustomerGateway.get('user_id') == Customer(
            id='id001',
            name='Alice',
            zip_code=77777,
            address='SEATTLE USA',
            number=123456789)

        m.assert_called_once_with('user_id')

終わりに

初めてPythonのテストを書いた私がハマったことをつらつらと書いてみましたが、
この記事が私と同じように初めてPythonのテストを書く方のお役に立てれば幸いです。

参考資料

ペアプロと育休の取得しやすさの関係について

こんにちは。SPEEDA開発チームの鈴木です。

昨年一児(娘)の父になりまして、凄い勢いで変化していく様子に喜んだり困ったりしながら過ごしております。
色々できることが増えると嬉しいのですが、それは同時にいたずらの幅が広がることも意味するんですよね。例えばものを引っ張ることを覚えたのは嬉しいのですが、私の髪の毛をひっぱってむしるのはやめていただきたい。そんな感じです。

f:id:kenji-suzuki:20191014235944p:plain:w250
髪をむしる娘の図。言葉は通じない。

今回はそんなうちの子が産まれたときに取得した育休の話をしたいと思います。
育休の話とはいっても育休をハックする話とか育児アプリとかの話ではなく、「育休を取得しやすい(と私は思う)SPEEDA開発チームの環境」についての話をします。
本編がそこそこ長い(スミマセン)ので前置きはここら辺で切り上げることとします。

男性の育休取得率

さて突然ですが、2018年度の日本における男性の育休取得率がどのくらいかご存知でしょうか?

6.16%です。

これは過去最高の取得率だそうです。
ちなみに同年の女性の育休取得率が82.2%だそうなので、過去最高とはいえども残念ながらまだまだ低い数値といえるのではないでしょうか。

このような状況の育休ですが、SPEEDA開発チームでは昨年私を含めた2人が育休を取得しています。
母数が少ないので一概に「育休取得率が高い」とは言えないですが、間違いなく育休を取得しやすい環境であると思いますし、だからこそ私は育休を取得したので、今回は私たちの環境の紹介を兼ねて次のような話をしようと思います。

1. 男性が育休を取得しなかった理由  
2.「1」の理由の原因を考える
3.「2」の原因を解決するにはどうしたらいいか  
4. SPEEDA開発チームにおいてはどうしているか  

男性が育休を取らなかった理由

では、まず一般的にどういう理由で育休が取られていないのか、2018年度のデータを見てみましょう。

f:id:kenji-suzuki:20191003170319p:plain
育休を取得しなかった理由2018

これを見ると、男性が一体どういう理由で育休を取得しなかったかがわかります。
(出展: 三菱UFJリサーチ&コンサルティング「平成29年度仕事と育児の両立に関する実態把握のための調査研究事業」)

Top3はこのようになっています。

1位: 業務が繁忙で職場の人手が不足していた(38.5%)
2位: 職場が育児休業を取得しづらい雰囲気だった(33.7%)
3位: 自分にしかできない仕事や担当している仕事があった(22.1%)

今回はこのうち2位の「職場が育児休業を取得しづらい雰囲気だった」と3位の「自分にしかできない仕事や担当している仕事があった」にフォーカスして話を展開したいと思います(人手不足の話は採用や業務効率化に絡んだ話だと思いますが、そこについては別軸の問題だと思いますので今回は言及しません)。

育休を取らなかった理由の原因を考えてみる

次に、上記2つの理由について私なりに原因を考えてみたいと思います。

職場が育休を取得しづらい雰囲気になっている原因

なぜ育休を取得しづらい雰囲気になってしまっているのでしょうか。
原因を私なりに考えてみましたが、このようなことが考えられるのではないかと思います。

1.普通の休暇すら取りづらい
普通に1日2日の休暇を取ることが難しい環境の場合、ある程度まとまった期間になるであろう育休は余計取りづらく感じることかと思います。
どういう場合に休暇が取りづらくなるのか、2つ思い当たります。
1つ目は、自分が担当している仕事に期限があり、休むことで間に合わなくなったり、後々辛くなったりと自分が困るケースです。
2つ目は、自分が休むと自分の仕事を周囲の誰かが余分に担当することになり、迷惑がかかるケースです。
1つ目のケースで休んだ分を自分でカバーできずに間に合わなくなった場合は、2つ目のように他の誰かがフォローにまわり結局周囲の負荷が増えることが考えられます。
そういうことを考えていくとやはり休みづらくなるのではないでしょうか。
また、こういった懸念は想像に過ぎず実際は自分が休んだところで大した迷惑はかからないのかもしれませんが「安心して休める仕組み」が整っていなければ心理的に休みが取りづらいということはあるでしょう。

2.周りの人が普通の休暇すら取らない
自分だけではなく、周りの人も「1」と同じように自分が困ったり、周囲への迷惑を気にして休みを取得しないような環境では休みづらくなるかと思います。
お互いをお互いで縛り合っている感じです。同調バイアスによる負の連鎖と言えるかもしれません。
休暇と同様に、子供が産まれたのに誰も育休を取得しないような環境では遠慮してしまう(遠慮しなければならない気持ちになる)のではないでしょうか。

3.男性の育休に理解がない
周りの人たちが男性の育休に対して理解がない場合、育休は取得しづらい雰囲気になるでしょう。

どうすれば育休を取得しやすい環境になるか

私が挙げた原因はどれも環境が原因となっていますが、どうしたら育休を取得しやすい雰囲気を作れるでしょうか。
「1」は色々な解決方法があるでしょうが、上述のとおり休んだ場合に自分を含めて誰も困らないような「安心して休める仕組み」があれば解決しそうです。
「2」の解決には「1」の仕組みの存在に加えて実際その仕組みが働いている必要があります。
「3」は「1」の仕組みで部分的に解決しそうです。なぜなら「3」のような人たちが育休に反対する理由は”育休を取得することによって自分や他の人たちに迷惑がかかる”ということにあったりするからです。
「誰かが迷惑するから育休には反対」とは言えても「誰にも迷惑はかからないが育休には反対」とは言えないのではないでしょうか。

このように考えてみると 「安心して休める仕組み」が存在して運用されていることが、育休を取得しづらい雰囲気を解消する方法の一つになりそうです。

安心して休める仕組みとは

休みを取得しづらい理由として「休むと自分が困る」というものがありました。
なぜ自分が休むと困る状況になっているのでしょうか。それは「仕事が個人に対してアサインされている」からではないでしょうか。
そもそも自分がやらないといけない仕事なので、誰かに代わってもらいづらいというわけです。
このような環境では他の人も同様に「自分の仕事」をもっているでしょうから。
では「仕事がチーム対してアサインされている」ならば問題はないのでしょうか。
いいえ。 チームで仕事をしていても、仕事が属人化しないようにしないと今回の文脈では結局困ることになります。
仕事が属人化していると、計画的に休む場合は引き継ぎが発生するでしょうし、突発的に休むなら引き継げない分を自力で調査したりする手間が発生するからです。
ですので「仕事がチームに対してアサイン」されており、属人化も防がれていなければなりません。

SPEEDAの開発チームではどうしているか

SPEEDAの開発チームでは基本的に「仕事はチームに対してアサイン」されます。
そして「ペアプロ」が属人化を防ぐのに役立っています(やっと本題!)。
(属人化を防ぐためだけにペアプロをしているわけではないことを補足しておきます。ペアプロには属人化の軽減以外にも沢山のメリットがあるんです。ただ今回の話では「属人化」の観点にフォーカスします。)

ペアプロについて具体的に

SPEEDAの開発チームは、すべてのチームがほぼ100%ペアプロで作業しています(ペアプロそのものについての詳しい説明は割愛させていただきます) 。
そしてプログラミング以外(例えば採用活動)でもペア作業をします。
更にペアを組むメンバーを一日のうち何度も入れ替えており、様々なメンバーと様々な開発ストーリーに取り組むことになります。
領域もUI/UX含めたフロント周りから、バックエンド、インフラ、CI/CDなどすべてを皆で担当します。
その結果として、チームの中で「フロント部分はAさんしか知らない」「バックエンド部分はBさんしか知らない」という属人化が起きなくなっています。

チームの人数が多く、かつ小規模なストーリーの場合、自分が担当する前にストーリーが終わってたということはありますが、自分しか知らないという状況にはなりません。
またチームの人数が奇数の場合は一人作業※が発生しますが、その場合は一人でも十分なストーリーを選ぶような工夫をしています。 ※一人で作業するというのは普通かもしれませんが私たちの場合ペアが基本なため一人は特別な感じです。
そのため、誰かが休んで困るということが基本的にありません。
なので用事があれば休みますし、周りがそうなので上述のように「周りが休まないから休みづらい」ということがありません。

ペア作業のイメージ

ここで補足としてペア作業についてイメージしやすいように絵で表してみることにします。
あるチームにメンバーとして、Aさん、Bさん、Cさん、Dさんがいるとします。
f:id:kenji-suzuki:20191006232514p:plain

ペアは、AさんとBさん、CさんとDさんという組み合わせとします。
環境はこのような感じです。

f:id:kenji-suzuki:20191014011451p:plain
ペアプロ環境

一つのデスクトップPCにモニター2つとキーボード2つが接続されています。
マウスが見当たらないのは画像の手抜きではなく、Thinkpadのトラックポイント付きキーボードを使っているためです。
各々に対してキーボードが存在するため、ドライバーとナビゲーターの役割がスムーズに交代できます(キーボードが一つしかなかったら、心理的にキーボードを渡してもらったり渡したりというのがやりづらくなるかと思います)。
モニターはミラーリングされていたり左右で分割されていたり、ペアによってやりやすいように変えています。

CさんDさんペアも同じ構成で作業します。
f:id:kenji-suzuki:20191014011751p:plain

作業の流れ

時間は13時、AさんとBさんペアはストーリーXに着手します。CさんとDさんペアはストーリーYに着手します。
f:id:kenji-suzuki:20191006232845p:plain

時間が14時になりました。ここでペアを入れ替えます。1コマの時間(ペア交代までの時間)は、チームで話し合って決めます。この例では1時間で交代するものとします。今度はDさんAさんペア、BさんCさんがペアになりました。横にスライドする形ですね。
f:id:kenji-suzuki:20191006234446p:plain
入れ替えの際は、作業開始時にいまどういう状況なのかの共有が行われることが多いです。
(省いていますが、本当は適宜休憩を入れます(休憩超大事!))
また、OSやディレクトリ構成などは意図的に統一しているため、別のマシンに使ったとき困りません。

時間が15時になりました。またペアを入れ替えます。CさんDさんペアと、AさんBさんペアです。
f:id:kenji-suzuki:20191006235005p:plain
これで全員がストーリーX、ストーリーY両方に少しずつ関わったことになり、特定のメンバーしかわからないということがなくなりました。
複数のストーリーに少しずつ関わるということは、一つのストーリーに100%関われないことも同時に意味し、自分の知らないコードが存在し得るようになります。
こういった問題に対しては、他のメンバーに伝えた方がよさそうなことについて(例えば設計の部分)は都度々々共有したり相談したりしながら進めることで対応しています。
メンバーが増えていくとこういった共有も大変になってきたり、上記のペアの入れ替え方法だとペアを組めない人たち(例えばAさんとCさんはペアになれない)が出てきたりと、ペア作業については話題が尽きないのですが、今回の趣旨はペア作業によって属人化が軽減されているということであるため割愛させていただきます。

チームの入れ替え

チーム内でペアの入れ替えを行っていることは上述しましたが、チームをまたいだメンバーの入れ替えも行われています。
例えばXチームとYチームという2つのチームがあったとします。 このチームに対し、それぞれが担当している仕事はそのままに、メンバーを一部入れ替えるのです。 f:id:kenji-suzuki:20191007004951p:plain

↑を↓のように入れ替える。

f:id:kenji-suzuki:20191007005034p:plain
入れ替えの頻度は決まっておらず、リソース調整の結果であったり、タイミング的なものであったりします。
また、基本的には本人の意思が最大限に尊重され本人が希望するチームに移動することになります。強い理由があって同じチームに長く残るということもあります。
このような入れ替えが行われることにより、SPEEDAの開発チームにおける属人性は極めて少なくなっているといってもよいかと思います。
※ペアプロと同様に属人化を防ぐためだけに入れ替えをしているわけではなく、知見の共有であったりチームに新たな風を起こしたりと他にも色々メリットがあるから入れ替えが行われています。

最後に

私がSPEEDA開発チームにおいて、育休を取得しづらいとは思わなかった理由は、ユーザベースという会社自体も周りの人たちも男性の育休に理解があったということもありますが、ペアプロとチームの入れ替えによって「属人化」が軽減されており「自分が休むと誰かが困る」という心理的負担がなかったことも大きいと思います。
今回は主に「属人化」という点にフォーカスしてペアプロの話をしましたが、ペアプロがもたらす良い作用は他にも沢山あるので、(いきなりは難しいかもしれませんが)興味がわいた方は是非ペアプロを取り入れてみてはいかがでしょうか。
いきなりガッツリではなく、小規模に始めたり、短時間やってみたりするだけでも「良さ」がわかるかもしれません。

Ringアプリケーションで例外をいい感じにハンドリングする方法(Ductでの解説も含む)

こんにちは!こんにちは!SPEEDA開発チームのあやぴーです。 社内のClojureを使ったAPIにおいて、「例外をうまくハンドリングしたいんだけど…」という話が出てきたので、今回はRingアプリケーションにおける例外のハンドリング方法について解説します。また、昨今Ductを使ってアプリケーションを作る機会が増えているので、それについては最後の方に解説をします。

また具体的なコードはGitHubにあるので、参考にしてみてください。

https://github.com/ayato-p/exception-handling

この記事で解説する具体的な内容については以下の通り

  • 例外のハンドリングにRingミドルウェアを使う
  • 例外によってレスポンスを変更する
  • Ductアプリケーションへの適用方法
  • まとめ

例外のハンドリングにRingミドルウェアを使う

Ringアプリケーションの特にRingハンドラー(以下、ハンドラー)部分で例外が出る場合について考えてみます。簡単のために以下のように、例外を投げる処理do-somethingを実行するハンドラーthrow-exception-handlerがあることにします。

(ns demo.core
  (:require [ring.util.response :as response]))

(defn- do-something []
  (throw (ex-info "err" {:exception/type :server-error})))

(defn throw-exception-handler [req]
  (let [res (do-something)]
    (response/response res)))

本来であればdo-somethingが適当な値を返して、それをハンドラーのレスポンスとして返すところですが、この例ではdo-somethingが例外を起こすため、適切なレスポンスを返すことができません(Ringアダプターの実装にもよりますが、多くの場合は500エラーが返ると思います)。このハンドラーの挙動は以下のテストで確認することができます。

(ns demo.core-test
  (:require [clojure.test :as t]
            [demo.core :as sut]
            [ring.mock.request :as mock]))

(t/deftest throw-exception-handler-test
  (t/testing "throw-exception-handlerが例外を返すこと"
    (let [req (mock/request :get "/err")]
      (t/is
       (thrown-with-msg? clojure.lang.ExceptionInfo #"err"
                         (sut/throw-exception-handler req)))

      (let [data (try
                   (sut/throw-exception-handler req)
                   (catch clojure.lang.ExceptionInfo e
                     (ex-data e)))]
        (t/is (= {:exception/type :server-error}
                 data))))))

このようにハンドラーの中で例外が発生する場合、各ハンドラーやその手前のレイヤーで適切にハンドリングして、例外をハンドラーの外側に伝播させないというのも場合によってはやると思います。しかし、予期しない例外や一般的な例外などに対してすべてのハンドラーで対応するのはあまりにも面倒です。そういった例外をハンドルするための、いい感じの機能が欲しいところです。

Javaの場合、JAX-RSのExceptionMapperあたりが今回欲しい機能に該当しそうです。Ringには残念ながら同じようなものはありません。そのため、少しだけ考えてみることにします。上記のテストコードがヒントになりそうです。上のコードのようにthrow-exception-handlerにリクエストマップを渡すようなコードをtry~catchで囲んで、例外を吐き出したときのみ例外のレスポンスマップを返すように書くことを考えれないでしょうか。

(try
  (throw-exception-handler req)
  (catch Exception e
    (-> (response/response "Internal Server Error!!")
        (response/status 500))))

このようにハンドラーを実行して、その結果次第でレスポンスに影響を与える方法を僕らは既に知っているはずです。Ringミドルウェア(以下、ミドルウェア)です。次のようなミドルウェアwrap-exception-handlerを考えてみます。

;; demo.core
(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (-> (response/response "Internal Server Error!!")
            (response/status 500))))))

これはハンドラーを受け取って、新しいハンドラーを返します。新しいハンドラーは元のハンドラーに対して、自身が受け取ったリクエストを適用するだけで基本的には何もしませんが、例外が投げられたときにそれをキャッチして500のステータスコードを返すようにしています。

wrap-exception-handlerは以下のテストコードで機能していることを確認することができます。

;; demo.core-test
(t/deftest wrap-exception-handler-test
  (t/testing
      "例外を投げないハンドラーが実行されたら何もせずに元の結果を返すこと"
    (let [req (mock/request :get "/err")
          handler (constantly {:status 200
                               :body "Hello, world"})
          app (sut/wrap-exception-handler handler)]
      (t/is (= {:status 200
                :body "Hello, world"}
               (app req)))))
  (t/testing
      "例外を投げるハンドラーが実行されたらステータスコード500のレスポンスを返すこと"
    (let [req (mock/request :get "/err")
          handler (fn [_] (throw (Exception. "err")))
          app (sut/wrap-exception-handler handler)]
      (t/is (= {:status 500
                :body "Internal Server Error!!"
                :headers {}}
               (app req))))))

例外によってレスポンスを変更する

例外をハンドルすることができたので、今度は例外の種類によって返すレスポンスを変化させてみます。既に想像がついていると思いますが、try~catchで掴む例外を複数用意すれば簡単にレスポンスを変えることができます。やってみましょう。

以下ではIllegalArgumentExceptionのときに500clojure.lang.ExceptionInfoのときはex-data:exception/typeの値によってステータスコードを変えています。また、対応できていない例外に対しては500を返しつつも、標準出力に"Unhandled Exception:"と流すようにしています。

(t/deftest wrap-exception-handler-test
  ;; "例外を投げないハンドラーが実行されたら何もせずに元の結果を返すこと"

  (t/testing
      "例外を投げるハンドラーが実行されたら適切なエラーコードを返すこと"

    (t/testing "IllegalArgumentExceptionのとき500を返す"
      (let [req (mock/request :get "/err")
            handler (fn [_] (throw (IllegalArgumentException. "err")))
            app (sut/wrap-exception-handler handler)]
        (t/is (= {:status 500
                  :body "Internal Server Error!!"
                  :headers {}}
                 (app req)))))

    (t/testing "対応できていない例外は500を返す"
      (let [req (mock/request :get "/err")
            handler (fn [_] (throw (NullPointerException. "err")))
            app (sut/wrap-exception-handler handler)]
        (t/is (= {:status 500
                  :body "Internal Server Error!!"
                  :headers {}}
                 (app req)))
        (t/is (str/starts-with?
               (with-out-str (app req))
               "Unhandled Exception: java.lang.NullPointerException"))))

    (t/testing "ExceptionInfo"
      (t/testing ":exception/typeが:server-errorのとき500を返す"
        (let [req (mock/request :get "/err")
              handler (fn [_] (throw (ex-info "err" {:exception/type :server-error})))
              app (sut/wrap-exception-handler handler)]
          (t/is (= {:status 500
                    :body "Internal Server Error!!"
                    :headers {}}
                   (app req)))))

      (t/testing ":exception/typeが:not-foundのとき400を返す"
        (let [req (mock/request :get "/err")
              handler (fn [_] (throw (ex-info "err" {:exception/type :not-found})))
              app (sut/wrap-exception-handler handler)]
          (t/is (= {:status 404
                    :body "Not Found!!"
                    :headers {}}
                   (app req))))))))

こうなるようにwrap-exception-handlerを実装すると、以下のようになります。

(defn- internal-server-error-response []
  (-> (response/response "Internal Server Error!!")
      (response/status 500)))

(defn- not-found-response []
  (-> (response/response "Not Found!!")
      (response/status 404)))

(defprotocol ExceptionToResponse
  (->response [e]))

(extend-protocol ExceptionToResponse
  Exception
  (->response [e]
    (println "Unhandled Exception:" (type e))
    (clojure.stacktrace/print-stack-trace e)
    (internal-server-error-response))

  IllegalArgumentException
  (->response [e]
    (internal-server-error-response))

  clojure.lang.ExceptionInfo
  (->response [e]
    (let [{t :exception/type} (ex-data e)]
      (case t
        :server-error
        (internal-server-error-response)
        :not-found
        (not-found-response)))))

(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (->response e)))))

500404のレスポンスだけの関数(internal-server-error-response, not-found-response)を作りました。また実際に例外をふりわけるところはExceptionToResponseというプロトコルに任せて、それぞれの例外ごとに実装をできるようにしています。これによってwrap-exception-handlerでやっていることは非常に明快になりました。

このようにRingミドルウェアを応用することで、例外ごとに任意のレスポンスを返すことができました。

Ductアプリケーションへの適用方法

上記のwrap-exception-handlerを適用するDuctアプリケーションに適用するのは非常に簡単です。まずはプロジェクトを用意するところから。次のコマンドを使ってDuctプロジェクトの雛形を作ります。

$ lein new duct demo --to-dir exception-handling-api -- +api +ataraxy

今回はAtaraxyというデータでルーティングを記述できるライブラリを利用します。これはDuctで既にモジュール化されているため、簡単に使い始めることができます。

次に先程のwrap-exception-handlerを使って、Ductから使えるコンポーネントを用意します。

(ns demo.middleware.exception-handler
  (:require [ring.util.response :as response]
            [integrant.core :as ig]))

;; 中略

(defn wrap-exception-handler [handler]
  (fn exception-handler [req]
    (try
      (handler req)
      (catch Exception e
        (->response e)))))

(defmethod ig/init-key :demo.middleware/exception-handler [_ _]
  wrap-exception-handler)

wrap-exception-handlerの実装については、既に説明したものをそのまま利用します。そして、ミドルウェアのコンポーネント:demo.middleware/exception-handlerでは、単にwrap-exception-handlerを返すようにします。

次に、このミドルウェアを適用するには、config.ednに以下のような記述をします。大事なところは:duct.module/ataraxyのキーに対応するマップです。具体的な記法の説明については、Duct module.ataraxyのREADMEに譲りますが、Ataraxyのシンタックスでいうresult部に対してメタ情報として任意のミドルウェアを指定することで先程のミドルウェアのコンポーネントを適用することができます。

{:duct.profile/base
 {:duct.core/project-ns demo

  ;; Middlewares
  :demo.middleware/exception-handler {}

  ;; Handlers
  :demo.handler.throw-exception/not-found {}
  :demo.handler.throw-exception/server-error {}}

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/ataraxy
 {"/" ^:exception-handler
  {"not-found" [:throw-exception/not-found]
   "server-error" [:throw-exception/server-error]}}

 :duct.module/logging {}
 :duct.module.web/api
 {}}

まとめ

ClojureでWebアプリケーションを作っても、簡単に例外のハンドリングはできるよ!

【kubernetes / Helm】大量のCronJobに悩む貴方に送るプラクティス

はじめに

こんにちは! UZABASE SPEEDA SRE teamの生賀id:skikkh(@skikkh)です。

最近あった嬉しかったことは、自分が翻訳した日本語がkubernetesのCronJob - Kubernetesページに反映されていたことです。

閑話休題、弊社SPEEDAサービスでは大量のバッチジョブがHinemosを起点としてVM上で動作しています。 SREチームではこのようなジョブ群を徐々にサーバから切り離して、コンテナライズを進めています。

そんな大量にあるジョブですが、環境変数だけが異なっているものも多数あり、実行環境 x 環境変数と環境が異なると掛け算式に増えていきます。 これをkubernetesのCronJobでyamlハードコーディングすると容易に1000行を超えてしまい、管理上のコストも含め現実的ではありません。1

そこで、kubernetesパッケージマネージャーを使用することにしました。その選択肢としてはHelmKustomizeなどがあります。 これらを利用することで環境毎に異なる設定値のリソースが作成できたり、複雑な依存関係を持つkubernetesリソース群をChartという一つのフォーマットにまとめることができます。

今回、要件を満たすエントリが見当たらなかった為、利用事例として投稿させていだきます。

目次

Kustomizeの場合

まずはKustomizeで同様の利用を想定した際の構成を見てみましょう。

下記ディレクトリ構成は公式のKustomizeのGithubの、サンプルディレクトリ構成から拝借しました。

Kustomizeのサンプルディレクトリ構成

~/someApp
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── development
    │   ├── cpu_count.yaml
    │   ├── kustomization.yaml
    │   └── replica_count.yaml
    └── production
        ├── cpu_count.yaml
        ├── kustomization.yaml
        └── replica_count.yaml

staging, develop, production環境といった環境差分をoverlaysで表現するのはKustomizeの方がいいかもしれませんが、Kustomizeの基本的なユースケースに照らし合わせてジョブを作成するとなると、次の例ようにCronJob毎にディレクトリを切る必要があり、管理コストが嵩んでしまいます。

Kustomizeで多数CronJobを作成する

例えば24個のAジョブと24個のBジョブ計48個作成する場合、overlays下に48個のディレクトリ構成ができてしまいます。以下がサンプルになります。

~/someApp
├── base
│   ├── cronjob.yaml
│   └── kustomization.yaml
└── overlays
    ├── 0101-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    ├── 0102-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    ├── 0103-job-a
    │   ├── category.yaml
    │   ├── kustomization.yaml
    │   └── table.yaml
    │      ︙ # 増えるだけ用意しないといけなくなる
    └── 0224-job-b
        ├── caterory.yaml
        ├── kustomization.yaml
        └── table.yaml

バッチジョブが20を超えてくるとなるとディレクトリで分割するのは現実的に厳しいと思います。

参考:Kustomize で CronJob を同一テンプレートからスケジュール毎に生成する

以上の理由から、KustomizeではなくHelmを利用することを決めました。

Helmの場合

Helmではアクションと呼ばれる制御構造によってリストをループ処理することができます。したがって、今回のケースではこちらを採用することにしました。

Helm, Tillerのバージョンはv2.14.3を使用しています。

今回の要件として、

  • 同一のテンプレートをベースとして、
  • DBのテーブル毎に含まれる、
  • 複数の地域情報を取り出し処理ができる2

バッチジョブを作成する必要がありました。

これを実現するためには先述したループ処理を行う必要があります。

実現方法としては「ループ処理をネストすれば可能」というのが答えなのですが、少しだけハマりどころがあったので、それも合わせてお話できればと思います。

通常のCronJobをループ処理する場合、values.yamlに回したい変数のリストを作成し、rangeを入れればループ処理が可能です。

単一のリストを利用してCronJobをループ処理する

まずはスケジュール毎にCronJobを作成したい場合を想定してみましょう。 以下の実行例ではスケジュール毎にスケジュールのインデックスをechoで出力するという設定をyamlで行います。動作確認したい場合はhelm testを利用しましょう。

test-schedule-cj.yaml

{{- range $index, $schedule := .Values.global.schedules }}
--- # 複数作成の為に必須
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $index | add1 }} # CronJob毎に一意になるような名前をつける必要がある
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: test
            # テストで検証するのであれば、busyboxイメージでechoを出力します。  
            args:
            - /bin/sh
            - -c
            - date; echo Index count is $(INDEX).
            env:
              name: INDEX
              value: {{ $index | add1 }}
          restartPolicy: OnFailure
{{- end }}

因みにyamlの可読性の為、最初に以下のような代入を行っています。(以降、省略)

test-schedule-cj.yaml

# global
{{- $namespace := .Values.global.namespace }}
{{- $serviceAccount := .Values.global.serviceAccount }}
{{- $imageRepository := .Values.global.imageRepository }}
{{- $imageTag := .Values.globa.imageTag }}
# chart
{{- $chartName := .Chart.Name }}
{{- $chartVersion := .Chart.Version }}
{{- $releaseName := .Release.Name }}
{{- $releaseService := .Release.Service }}

values.yamlの設定は以下のようになります。

vaules.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # image  
  imageRepository: busybox
  imageTag: latest

  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

特に難しい部分はなく、rangeアクションで作成したいCronJobを囲むだけです。 ただ一つ注意しないといけない点として、CronJobの名前は一意になるようにしなければいけません。 その為、全ての名前が一意になるように命名規則を考えてつけましょう。 基本的にはループで回す変数名をつけるようにすれば大丈夫だと思います。3

作成後、以下のhelmコマンドを実行します。

$ helm upgrade --install job01 .

これでテーブルの数だけジョブを回すことができるChartのリリースができますね。

複数のリストを利用してCronJobをループ処理する

一つの変数の条件でループ処理ができたので、複数の変数のリストを使用してループする場合を考えてみましょう。 スケジュールのループ処理内にテーブルのループ処理をネストするだけで作成できます。 記述例としては以下のようになります。

test-schedule-table-cj.yaml

{{- range $index, $schedule := .Values.global.schedules }}
{{- range $table := .Values.global.tables }}
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $index | add1 }}
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: test
            args:
            - /bin/sh
            - -c
            - date; echo Index count is $(INDEX). Table name is $(TABLE_NAME).
            env:
              name: INDEX
              value: {{ $index | add1 }}
              name: TABLE_NAME
              value: {{ $tablel.name }}
          restartPolicy: OnFailure
{{- end }}
{{- end }}

values.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # image  
  imageRepository: busybox
  imageTag: latest

  # schedule info
  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

  # table info
  tables: 
    - name: XxxTest
      category: walk
    - name: YyyTest
      category: walk
    - name: ZzzTest
      category: run

しかし、例のようにスケジュール×テーブルという変数でジョブを回そうとすると失敗してしまいます。 以下が失敗の際に出力されるエラーになります。

UPGRADE FAILED
Error: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/test-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {}
Error: UPGRADE FAILED: render error in "test-loop/templates/est-schedule-table-cj.yaml": template: test-loop/templates/est-schedule-table-cj.yaml:15:28: executing "test-loop/templates/est-schedule-table-cj.yaml" at <.Values.global.tables>: can't evaluate field Values in type interface {}

失敗の原因は.のカレントスコープが「.Values.global.schedules」を向いているためで、.Values.global.regionが習得できません。

回避策としては、ループの処理の際に、「常にグローバルスコープを持つ$を使用する」ことでこのエラーを回避できます。

したがって、以下のdiffの変更点のように最初の記述でループしたい変数を別の変数に代入することでスコープを変えないままネストしたループ処理ができるようになります。 また、CronJobのリソース名も一意にするため、テーブルの名前を更新しておきましょう。

test-schedule-table-cj.yaml

+ {{- $regions := .Values.global.tables }}

- {{- range $index, $schedule := .Values.global.schedules }}
- {{- range $table := .Values.global.tables }}
+ {{- range $index, $schedule := .Values.global.schedules }}
+ {{- range $table := $tables }} 

-   name: test-{{ $index | add1 }} 
+   name: test-{{ $index | add1 }}-{{ $table.name }} # 名前を一意にするため

しかし、これでもまだ十分ではありません。あくまでスケジュールのindexがほしいのではなく、スケジュール(cron)毎に習得される地域の変数をCronJobのmetadataや環境変数に代入したいのです。

スケジュールの時間毎に、異なる地域のバッチジョブを実行する

golangのSprig libraryを利用してリストを取得するようにしています。 このような形にしたのは、複数のバッチジョブを後述するsubchartに記述する際、values.yamlをDRYにするためです。 全てのリストを1つづつ取得する関数がなかったので、次のような形で再現しています。

test-region-table-cj.yaml

{{- $regions := .Values.global.tables }}

{{- range $index, $schedule := $schedules }}
{{- range $table := $tables }}

# スケジュール毎に地域のリストを取得する
{{- $region := slice $regions $index ( $index | add1 ) | first }}

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-{{ $region }}-{{ $table.name }} # indexではなく地域別で名前をつけている
  namespace: {{ $namespace }}
  labels:
    chart: "{{ $chartName }}-{{ $chartVersion }}"
    release: "{{ $releaseName }}"
    heritage: "{{ $releaseService }}"
spec:
  schedule: {{ $schedule }}
  suspend: false
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccount: {{ $serviceAccount }}
          containers:
          - image: "{{ $imageRepository }}:{{ $imageTag }}"
            name: {{ $name }}
            args:
            - /bin/sh
            - -c
            - date; echo This chart has region $(REGION) and table $(TABLE_NAME).
            env:
              - name: TABLE_NAME
                value: "{{ $table.name }}"
              - name: TABLE_CATEGORY
                value: "{{ $table.category }}"
              - name: REGION
                value: {{ $region | quote }}
          restartPolicy: OnFailure
{{- end }}
{{- end }}

values.yamlにはglobal.regionsを、scheduleのリスト長と同一のリストを加えましょう。4

values.yaml

+  regions: [japan, us, uk, china]

これで、地域別のDBのテーブルの情報が取得できるようになりましたね。

サブチャートを作成して、一連のワークフローを一つのChartで再現する

実際には上記のようなバッチジョブが後続に存在しているので、これらをひとまとまりにして扱う必要があります。 最後に、これらバッチジョブをひまとまりにして処理する方法を学びましょう。

複数のjob(Dockerイメージが別)を扱うためにsubchartを採用しました。 ServiceAccount関連のリソースやNetworkPolicyやNamespaceのようなグローバルなリソースは大本のChartにまとめ、各ジョブをsubchartに入れるという形を取ります。

values.yamlの項目で、globalの変数と、そうでないsubchart毎のローカル変数を使いわけることで実現できます。

以下にサンプルのディレクトリ構成を挙げます。

.
├── Chart.yaml
├── charts
│   ├── 0101-job
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── _helpers.tpl
│   │       ├── xxx-cronjob.yaml
│   │       └── tests
│   │           └── test-xxx.yaml
│   ├── 0102-job
│   │   ├── Chart.yaml
│   │   └── templates
│   │       ├── _helpers.tpl
│   │       ├── yyy-cronjob.yaml
│   │       └── tests
│   │           └── test-yyy.yaml
│   └── 0103-job
│       ├── Chart.yaml
│       └── templates
│           ├── _helpers.tpl
│           └── zzz-cronjob.yaml
│           └── tests
│               └── test-zzz.yaml
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── namespace.yaml
│   ├── networkpolicy.yaml
│   ├── secrets
│   │             ︙
│   └── serviceaccount.yaml
└── values.yaml

個別のyaml設定の注意点

各サブチャート毎に分化されたschedulesはグローバルの設定ではなく、個別のジョブ毎のローカルの設定になるため、各subchart内では、.Values.global.schedulesから.Values.schedulesのようにグローバルのスコープをローカルのスコープに変更しておきましょう。

+ {{- $category := .Values.schedules }}
- {{- $tables := .Values.global.schedules }}

フラグで実行するジョブバッチを選択する

後でsubhart毎にテストをするには、個別のsubchartだけを実行する必要があります。 そのため、enabledを各subchartに記述します。

以下のように各CronJobリソースを条件式{{- if .Values.enabled }}で挟んで、

test-subchart-cj.yaml

{{- if .Values.enabled }}
{{- range $index, $schedule := $schedules }}
{{- range $table := $tables }}
---
apiVersion: batch/v1beta1
︙
{{- end}}
{{- end}}
{{- end}}

values.yamlに以下の設定を入れます。

values.yaml

0101-job:
  enabled: true # 全てtrueにしておく
  imageRepository: xxx
︙

実行時に不必要なジョブに対してfalseのフラグを立てることで個別実行が実現できます。

$ helm upgrade --install jobs --set 0101-job.enabled=true --set 0102-job.enabled=false --set 0103-job.enabled=false .

上記は0101-jobだけがChartだけがデプロイされる例になります。

テストに関して1つ注意点があります。 テストを行う毎はテストの実行ジョブの順番が変わるのでご注意下さい。5

完成したvalues.yaml

最終的なvalues.yamlは次のようになります。

values.yaml

global:
  # kubernetes
  namespace: test-ns
  serviceAccount: test-sa

  # table info
  tables: 
    - name: XxxTest
      category: walk
    - name: YyyTest
      category: walk
    - name: ZzzTest
      category: run
     

  regions: [japan, us, uk, china]

  # test config
  tests:

    tables: 
      - name: XxxTest
        category: walk

    regions: [japan, china]

0101-job:
  enabled: true
  imageRepository: xxx
  imageTag: 1.0.0
  schedules: [
    '0 0 * * *',  '10 0 * * *',
    '20 0 * * *', '30 0 * * *'
    ]

0102-job:
  enabled: true
  imageRepository: yyy
  imageTag: 1.0.0
  schedule: '0 1 * * *'

0103-job:
  enabled: true
  imageRepository: zzz
  imageTag: 0.1.0

  schedules: [
    '0 2 * * *', '10 2 * * *',
    '20 2 * * *', '30 2 * * *'
    ]

おわりに

今回ループ処理化したCronJob Aが成功したらCronJob Bを実行すると言ったワークフローは単純なスケジュール(cron)でHelm Chart化しました。

その他ハマりどころとしてはvalues.yamlのsubchart名と一致しないChart名、ディレクトリ名になっているとチャートがデプロイされないという事例がありました。案外、見落とします。 名前を変更した際には確認するようにしましょう。

最後に注意点で、今回のエントリでは実運用で想定するようなTLSの暗号化通信やSecurityContext、Compute Resourcesなど省略しているので、それぞれの環境に合わせて設定していただればと思います。

以上!

仲間募集

ユーザベースのSPEEDA SREチームは、No Challenge, No SRE, No SPEEDA を掲げて業務に取り組んでいます。
「挑戦しなければ、SREではないし、SREがなければ、SPEEDAもない」という意識で、日々ユーザベースのMissionである、「経済情報で、世界をかえる」の実現に向けて邁進しています。

少しでも興味を持ってくださった方はこちらまで


  1. いわゆるwall of YAML

  2. 割り当てたい変数(今回でいうと地域)毎にスケジュール(cron)を生成します

  3. 63文字の制限があります

  4. 本来は順序が逆ですが、説明の便宜上このように書いています

  5. 恐らく前回の実行のリストから+1されています