ごまだれ日記

プログラミングの技術メモとか

JJUG CCC 2021 Spring参加メモ

JJUG CCC 2021 Springに(ほんのちょっとだけ)参加したので雑多なメモを。

ccc2021spring.java-users.jp

まあ時間がなかったので1セッションしか聴いてないんですけどね。
聴いたのはこちらのセッションでした。
fortee.jp

動画が上がるっぽいので簡単な感想だけ。本旨とはあまり関係ない話なんですけども。
例外(的な状況)をtry-catchを使わずにハンドリングするためにSuccess型やFailure型みたいなのを定義し、inscanceofで検査するみたいなプラクティスを紹介する箇所がありました。
そこで「inscanceofは使ったら負けという価値観の人もいるが、それはオブジェクト指向プログラミングの考え方で、関数型プログラミングの考え方では別に問題ない」という主旨の補足的な説明がありました。(言葉通りではなく主旨)
そこで少しハッとさせられました。私も「instanceofは使ったら負けおじさん」でしたが、それはあくまでもオブジェクト指向プログラミングの主旨にそぐわないというだけで、本質的に問題があるわけではないんだなって。
こういうのは「オブジェクト指向プログラミングとしては正しくない」とかじゃなくて、じゃあ実際に何が問題なんだ?ってところまで突き詰めて考える必要があるのかなと思いました。
最近はオブジェクト指向プログラミングに否定的な言説もちらほら見受けられるようになってきたので、特にそう思います。オブジェクト指向プログラミングというと金科玉条のように語られがちですけど実際にはそんなことはなくて、「オブジェクト指向プログラミングとしては正しくないけど実際には別に問題はないコード」とかも普通にあって、今回その一例を見たような思いでした。
(まあ、このプラクティスも別に最善のやり方として紹介されてたわけではなかったですけど)

とはいえ、そういうことを自分の平凡な頭だけで考えるのは現実的ではないので、オブジェクト指向プログラミングなどの特定の考え方にまずは依拠しつつも、関数型プログラミングなどの別の考え方も別途学んで、それを取り入れていくというのが現実的なんですかねぇ。
まあJavaにも関数型プログラミング的な機能がどんどん取り入れられているわけなので、そんなの今さらな話だろうと言われればぐうの音も出ないのですけれども。
何にせよ、勉強することと手を動かすことは止めてはいけないですね。

Javaのテストにおけるモダンなベストプラクティス

本記事はModern Best Practices for Testing in Javaの日本語訳です。元記事の著者から許可を得て翻訳、公開しています。
翻訳は不慣れなので変なところもあると思いますが、ご容赦ください。

また、Qiitaにも同じ記事を投稿しています。
Javaのテストにおけるモダンなベストプラクティス - Qiita


メンテナンスしやすくて読みやすいテストコードは良いテストカバレッジを確立するために重要で、それにより何かを壊すことを恐れずに新機能の実装やリファクタリングが可能になります。この記事には、私がJavaユニットテストや統合テストを長年に渡って書いて得られた多くのベストプラクティスが含まれています。それにはJUnit5やAssertJ、Testcontainers、Kotlinといったモダンな技術も含みます。中には当たり前と思われるようなこともあるかもしれませんし、あなたがソフトウェア開発やテストについての本で読んだことと相容れないこともあるかもしれません。

TL;DR

  • ヘルパー関数やパラメータ化テスト、AssertJの強力なアサーションを多用し、変数を使いすぎず、関連することだけを検証し、滅多に起こらないようなケースに対してテストを書くことを避けることで、小さくて明確なテストを書きましょう。
  • 全ての関連があるパラメータを明確にし、データを正しく挿入し、継承よりもコンポジションを使うことで、自己完結しているテストを書きましょう。
  • 本番コードの再利用を避け、出力値とハードコードされた値との比較に焦点を当てることで、ダンプテスト*1を書きましょう。
  • 完全な垂直スライドをテストする*2ことに焦点を当て、インメモリデータベースの使用を避けて、本番環境に近いテストを書きましょう。
  • JUnit5とAssertJはとても良い選択です。
  • staticなアクセスを避け、コンストラクタインジェクションを使い、 Clock*3を使い、非同期実行からビジネスロジックを分離することで、テストしやすい実装になるように労力を費やしましょう。

基本

Given, When, Then

テストは、1行の空行で分けられた3つのブロックで構成されるべきです。コードのそれぞれのブロックはできるだけ短くするべきです。ブロックを短くするためにサブ関数を使いましょう。

  • Given(入力): データの生成やモックの設定のようなテストの準備
  • When(実行): テスト対象のメソッドや動作の呼び出し
  • Then(出力): 出力や振る舞いが正しいかどうか検証するためのアサーションの実行
// 良い例
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "Smartphone"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("Smartphone");
}

“actual*” と “expected*” のプレフィックスを使う

// 悪い例
ProductDTO product1 = requestProduct(1);

ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);

同じ値かどうかのアサーションで変数を使うなら、変数名のプレフィックスとして"actual” や “expected” を付けましょう。これによって読みやすくなり、変数の意図が明確になります。その上、期待値と実測値を混同してしまう恐れが減ります。

// 良い例
ProductDTO actualProduct = requestProduct(1);

ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); // 素晴らしくて明確

ランダム性のある値よりも固定値を使う

ランダム性のある値はテストを不安定にし、デバッグが困難になり、エラーメッセージが省略され、コードへのエラーの追跡が困難になるため避けましょう。

// 悪い例
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad

代わりに、全てに対して固定値を使用しましょう。固定値はテストの再現性を高くし、デバッグを容易にし、関連するコードの行への追跡を容易にするエラーメッセージが出力されます。

// 良い例
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");

ヘルパー関数を使用することで、タイピング量を減らすことができます。

小さくて明確なテストを書く

ヘルパー関数を多用する

細かいコードや繰り返し出現するコードをサブ関数に抽出し、それに説明的な名前をつけましょう。それはテストを短く保ち、テストの要点が一目で簡単に把握できるようになるという意味で強力です。

// 悪い例
@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }

    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
// 良い例
@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}
  • データ(オブジェクト)の生成のため( createProductWithCategory() )と、複雑なアサーションのためにヘルパー関数を使ってください。ヘルパー関数には、テストに関係のあるパラメータのみを渡すようにします。それ以外の値については、適切なデフォルト値を使ってください。Kotlinでは、デフォルト引数を使うことで簡単に実現できます。Javaでは、擬似的なデフォルト引数を実現するためにメソッドチェーンとオーバーロードを使う必要があります
  • 可変長引数はテストコードをより簡潔にしてくれます( ìnsertIntoDatabase()
  • ヘルパー関数はシンプルな値をより簡単に生成するためにも使えます。拡張関数を使うことができるKotlinなら、よりやりやすいです。
// 良い例 (Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")
// 良い例 (Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()

このヘルパー関数はKotlinではこのように実装します:

fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())

fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")

変数を使い過ぎない

複数回使われている値を変数に抽出することは、開発者の常用手段です。

// 悪い例
@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );

    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}

不幸にも、これはテストコードを著しく膨張させます。その上、得られるテスト失敗メッセージから、関連するコード行にさかのぼって値を追跡することは難しいです。

KISSの原則 > DRY原則
// 良い例
@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}

テストコードが短く保たれていれば(それは強く推奨されます)、同じ値がどこで使われているのか知るのに何の支障もありません。それに加え、メソッドはさらに短くなり、それゆえに理解が容易です。そして最後に、この場合の失敗メッセージは、コードをさかのぼって追跡することをより簡単にしてくれます。

既存のテストを「ただもう一つ小さなことをテストするだけ」のために拡張しない

// 悪い例
public class ProductControllerTest {
    @Test
    public void happyPath() {
        // 大量のコードがここに...
    }
}

滅多に起こらないケースのテストを既存の(ハッピーパス*4の)テストに追加することは魅惑的です。
しかし、そのテストは大きくて理解が難しいものになります。それは、その大きなテストによってカバーされる全ての関連するテストケースを把握することを困難にします。一般的に、こういったテストは「ハッピーパステスト」と呼ばれます*5
もしこのようなテストが失敗した時、何が壊れたのか正確に理解するのは難しいです。

// 良い例
public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}

代わりに、期待する振る舞いについて全てわかる説明的な名前を持った新しいテストメソッドを作りましょう。はい、書く量は増えますが、関連する振る舞いだけをテストする、目的にぴったり合った明確なテストを作ることができます。繰り返しますが、ヘルパー関数はタイピング量を減らします。そして最後に、説明的な名前を持った、目的にぴったり合ったテストを追加することは、実装された振る舞いを記録する方法としてとても良いです。

テストしたいことだけをアサートする

本当にテストしたいことは何かということについて考えましょう。できるからといって、必要以上にアサートすることを避けましょう。さらに、前のテストにおいて既にテストしたことについて心に留めましょう。通常は、全てのテストにおいて同じことを何度もアサートする必要はありません。これによってテストが短く保たれ、明確に示され、期待する振る舞いについて気をそらされることがありません。

例について考えてみましょう。製品情報を返すHTTPエンドポイントについてテストします。テストスイートは下記のテストを含むべきです:

1 . データベースから取得した全ての値が正しいフォーマットで正しくマッピングされたJSONペイロードとして正しく返されることをアサートする大きな「マッピングテスト」。 equals() が正しく実装されているのであれば、AssertJの isEqualTo() (単一の要素用)または containsOnly() (複数の要素用)を使えば簡単にアサートできます。

String responseJson = requestProducts();

ProductDTO expectedDTO1 = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "evelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
        .containsOnly(expectedDTO1, expectedDTO2);

2 . クエリパラメータの ?category の正しい振る舞いをチェックするテスト。私たちは正しくフィルタリングされるかをテストしたいわけです、全てのプロパティが正しくセットされているかどうかではなく。それは上記のケースで既にテストしています。したがって、返された製品IDだけを比較すれば十分です。

String responseJson = requestProductsByCategory("Office");

assertThat(toDTOs(responseJson))
        .extracting(ProductDTO::getId)
        .containsOnly("1", "2");

3 . 滅多に起こらないケース、または特別なビジネスロジックをチェックするテスト。たとえば、ペイロードの中の特定の値が正しく計算されているかどうか。このケースだと、興味があるのはペイロードのうち特定のJSONフィールドだけです。そのため、テスト対象のロジックのスコープを明確にして文書化するために、関連するフィールドだけをチェックすべきです。繰り返しますが、全てのフィールドを再度アサートする必要はありません、なぜならここでは関係ないからです。

assertThat(actualProduct.getPrice()).isEqualTo(100);

自己完結したテスト

関連するパラメータを隠さない(ヘルパー関数内)

// 悪い例
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

はい、データの生成とアサーションのため、ヘルパー関数を使うべきです。しかし、それらをパラメータ化しなければいけません。テストのために重要で、テストによって制御される必要がある全てに対してパラメータを定義しましょう。ソースを読む人に対して、テスト内容を理解するために関数定義にジャンプさせるようなことを強いてはいけません。経験則: テストメソッドのみを見ることでテストの要点がわかるようにすべきです。

// 良い例
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));

テストメソッドの中で正しくデータを挿入する

テストメソッドの中では全てが正しくある必要があります。再利用可能なデータの挿入コードを @Before メソッドに移動させることは魅惑的ですが、そうするとテストがどうなっているのか完全に理解するためには、ソースを読む人があちこち飛び回らなければならなくなります。繰り返しますが、データを挿入するヘルパー関数はこの繰り返し行うタスクを一行にすることの助けとなります。

継承よりもコンポジションを好む

テストクラスで複雑な継承階層を作ってはいけません。

// 悪い例
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}

このような階層は理解を難しくしますし、あなたは結局現在のテストに必要ないたくさんのものを含むベースのテストクラスを継承することになる可能性が高いです。これはコードを読む人の気を散らし、バグが発生するかもしれません。継承は柔軟ではありません: AllInklusiveBaseTest から継承したものを全てを使うことは不可能ですが、そのスーパークラスAdvancedBaseTestから継承したものは何もないでしょうか? *6 その上、コードを読む人は全体像を理解するために複数のベースクラスの間を飛び回らなければなりません。

「重複は誤った抽象化よりは良い」
RDX in 10 Modern Software Over-Engineering Mistakes

代わりに、コンポジションを使うことを推奨します。それぞれの特定のフィクスチャの作業ごとに小さいコードスニペットとクラスを書きましょう(テストデータベースの起動、スキーマの生成、データの挿入、モックのウェブサーバの起動)。@BeforeAll を付与したメソッドの中か、もしくは生成されたオブジェクトをテストクラスのフィールドに割り当てることでこれらのパーツを再利用しましょう。それで、あなたはこれらのパーツを再利用することで全ての新しいテストクラスを組み立てます。まるでレゴブロックのように。この方法で、全てのテストは自身にぴったり合った、内容を把握するのが簡単でハプニングとは無縁のフィクスチャを持ちます。そのテストクラスは、全ての関連がテストクラスの中で正しいため、自己完結しています。

// 良い例
public class MyTest {
    // 継承の代わりのコンポジション
    private JdbcTemplate template;
    private MockWebServer taxService;

    @BeforeAll
    public void setupDatabaseSchemaAndMockWebServer() throws IOException {
        this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
        this.taxService = new MockWebServer();
        taxService.start();
    }
}

// 別のファイル
public class DatabaseFixture {
    public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
        PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
        db.start();
        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName("org.postgresql.Driver")
                .username(db.getUsername())
                .password(db.getPassword())
                .url(db.getJdbcUrl())
                .build();
        JdbcTemplate template = new JdbcTemplate(dataSource);
        SchemaCreator.createSchema(template);
        return template;
    }
}


繰り返します:

KISSの原則 > DRY原則

ダンプテストは素晴らしい: 出力とハードコードされた値を比較する

本番コードを再利用しない

テストは本番コードをテストすべきです: 本番コードを再利用するのではなく。もしテストの中で本番コードを再利用すると、もはやそのコードはテストされない*7ため、再利用されたコードによるバグを見落とすかもしれません。

// 悪い例
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));

ProductDTO actualDTO = requestProduct(1);

// 本番コードの再利用
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);

代わりに、テストを書く時、入力と出力という観点から考えましょう。そのテストでは入力値をセットして、実際の出力値とハードコードされた値とを比較します。大抵の場合、コードの再利用は必要ありません。

// 良い例
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));

本番コードと同じロジックをテストで書かない

マッピングのコードはテストの中でロジックが再発明される、よくある例です。私たちのテストが、テストの最初に挿入されたエンティティと同じ値を含むことをアサートするのに使われるようなDTO戻り値を返すmapEntityToDto()メソッドを含むとしましょう。この場合、テストコードの中に本番コードと同じロジックを書いてしまう可能性が高いでしょう。それはバグを含むかもしれません。

// 悪い例
ProductEntity inputEntity = new ProductEntity(1, "evelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);

ProductDTO actualDTO = requestProduct(1);

 // mapEntityToDto() は本番コードと同じマッピングロジックを含む
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);

繰り返しますが、解決策は actualDTO と、ハードコードされた値を含む手動で生成した参照オブジェクトを比較することです。それはとてもシンプルで、理解が容易で、エラーが発生しにくいです。

// 良い例
ProductDTO expectedDTO = new ProductDTO("1", "evelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);

もし全ての値の比較はしたくなくて、それゆえに完全な参照オブジェクトを生成したくなければ、サブオブジェクトのみか、関連する値のみを比較することを検討してください。

ロジックを書きすぎない

繰り返しますが、テストとはほとんど入力と出力に関するものです。入力を提供し、実測値と期待値を比較することです。したがって、テストの中でロジックを書きすぎる必要はないし、そうすべきではありません。もし多数のループと条件を伴うロジックを実装するなら、テストは内容を把握するのが難しく、よりエラーが起こりやすくなります。さらに、複雑なアサーションロジックの場合には、AssertJの強力なアサーションがあなたのために重労働をやってくれます。*8

現実に近いテスト

完全な垂直スライドのテストに集中する

一般的に、モックを使ってそれぞれのクラスを個別にテストすることが推奨されます。しかし、それには深刻な欠点があります: あなたは全てのクラスを統合してテストしているわけではなく、内部のクラスごとにテストがあるため、内部のリファクタリングが全てのテストを破壊するでしょう。そして最終的に、あなたは様々なテストを書き、メンテナンスしなければなりません。

f:id:dhirabayashi:20200421181431p:plain
各クラスを分離してモックを使ってテストすることは欠点をもたらします。

代わりに、統合テストに注目することを提案します。「統合テスト」とは、全てのクラスを一緒にして(本番コードのように)、全ての技術レイヤー(HTTP、ビジネスロジック、データベース)を完璧な垂直スライドで通り抜けるテストのことです。この方法だと、実装ではなく振る舞いをテストします。これらのテストは正確で、本番環境に近く、内部のリファクタリングに対して堅牢です。理想的に、一つのテストを書くだけで済みます。
f:id:dhirabayashi:20200421181610p:plain
統合テストに注目することを推奨します(= 現実のオブジェクトを一緒に書き、一度で全てをテストする)

このトピックについては、言うことがもっとたくさんあります。詳細は私のブログ記事の「Focus on Integration Tests Instead of Mock-Based Tests」をチェックしてください。

テストのためにインメモリデータベースを使用しない

f:id:dhirabayashi:20200421181757p:plain
インメモリデータベースを使うと、本番環境と違うデータベースに対してテストすることになります

テストのためにインメモリデータベースを使うこと(H2,HSQLDB,Fongo)は信頼性とテストのスコープを減らします。インメモリデータベースと本番環境で使われるデータベースとは異なる振る舞いをし、異なる結果を返すかもしれません。そのため、未熟なインメモリデータベースを基にしたテストは、本番環境のアプリケーションの正しい振る舞いをする保証がありません。その上、あなたは確かな(データベース固有の)機能を使用(またはテスト)することができない状況に簡単にぶつかります。なぜなら、インメモリデータベースはサポートしていないか、もしくは異なる動作をするからです。これについての詳細は、「 Don’t use In-Memory Databases for Tests 」の記事をチェックしてください。

解決策は実際のデータベースに対するテストを実行することです。幸運にも、Testcontainersというライブラリが、テストコードの中でコンテナを直接管理するための素晴らしいJavaAPIを提供しています。実行速度を速めるには、ここを見てください

Java/JVM

-noverify -XX:TieredStopAtLevel=1 を使う

常に-noverify -XX:TieredStopAtLevel=1 JVMオプションを実行設定に追加しましょう。それによりテストが実行される前のJVMの起動時間が1〜2秒節約されます。これはIDE経由でテストを頻繁に実行する、テストの初期開発時に特に役に立ちます。

更新: Java 13から、 -noverify は非推奨です。*9
Tip: IntelliJ IDEAでは「JUnit」起動設定のテンプレートにこの引数を追加することができるため、新しい実行設定ごとに引数を追加する必要はありません。
f:id:dhirabayashi:20200421182437p:plain

AssertJを使う

AssertJは流れるような*10型安全なAPIと、非常にバラエティに富んだアサーション、説明的なエラーメッセージを持った非常に強力で成熟したアサーションライブラリです。
あなたがしたいアサーションの全てがここにあります。これによりテストコードを短く保ちながら、ループや条件を持つ複雑なアサーションロジックを書かずに済みます。いくつかの例を示します:

assertThat(actualProduct)
        .isEqualToIgnoringGivenFields(expectedProduct, "id");

assertThat(actualProductList).containsExactly(
        createProductDTO("1", "Smartphone", 250.00),
        createProductDTO("1", "Smartphone", 250.00)
);

assertThat(actualProductList)
        .usingElementComparatorIgnoringFields("id")
        .containsExactly(expectedProduct1, expectedProduct2);

assertThat(actualProductList)
        .extracting(Product::getId)
        .containsExactly("1", "2");

assertThat(actualProductList)
        .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));

assertThat(actualProductList)
        .filteredOn(product -> product.getCategory().equals("Smartphone"))
        .allSatisfy(product -> assertThat(product.isLiked()).isTrue());

assertTrue()assertFalse() を避ける

単純なassertTrue()assertFalse() によるアサーションは不可解なエラーメッセージを出力するため避けましょう:

// 悪い例
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);
expected: <true> but was: <false>

代わりに、特にカスタマイズしなくても*11、良いエラーメッセージを出力するAssertJのアサーションを使いましょう。

// 良い例
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);
Expecting:
 <[Product[id=1, name='Samsung Galaxy']]>
to contain:
 <[Product[id=2, name='iPhone']]>
but could not find:
 <[Product[id=2, name='iPhone']]>

もし本当にbooleanに対してチェックしなければならないのであれば、エラーメッセージを改善するためにAssertJのas()の使用を検討してください。

JUnit5を使う

JUnit5は(ユニット)テストのための最先端技術です。活発に開発され、多くの強力な機能(parameterized tests, grouping, conditional tests, lifecycle controlのような)を提供しています。

パラメータ化テストを使う

パラメータ化テストでは一つのテストを異なる値で複数回実行することができます。この方法では、テストコードを追加することなしに複数のケースを簡単にテストすることができます。そういったテストを書くための素晴らしい手段をJUnit5は提供します@ValueSource@EnumSource@CsvSource、そして@MethodSourceです。

// 良い例
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
    client.perform(get("/products").param("token", invalidToken))
            .andExpect(status().is(400))
}

@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
    // ...
}

私はこれらを広範囲に使うことを強く推奨します。なぜなら、最小限の努力でより多くのケースをテストできるからです。

最後に、パラメータで期待値もコントロールできる、より発展的なパラメータ化テストシナリオのために使用できる@CsvSource@MethodSourceのことを強調したいと思います。

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}

@MethodSourceは、全ての関連するテストパラメータと期待値を含む専用のテストオブジェクトと組み合わせて使うと強力です。残念ながら、Javaでは、これらのデータ構造(POJO)を書くことが面倒です。それが、下記でこの機能の例を示すのにKotlinのデータクラスを使う理由です。

data class TestData(
    val input: String?,
    val expected: Token?
)

@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
}

private fun validTokenProvider() = Stream.of(
    TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
    TestData(input = "151175_13521", expected = Token(151175, "13521")),
    TestData(input = "151144375_id", expected = Token(151144375, "id")),
    TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
    TestData(input = null, expected = null)
)

テストをグループ化する

JUnit5の @Nested はテストメソッドをグループ化するのに便利です。理にかなったグループは特定のタイプのテスト(InputIsXYErrorCasesのような)、またはテストの配下のそれぞれのメソッドを一つのグループにすることができます。(GetDesignUpdateDesign

public class DesignControllerTest {
    @Nested
    class GetDesigns {
        @Test
        void allFieldsAreIncluded() {}
        @Test
        void limitParameter() {}
        @Test
        void filterParameter() {}
    }
    @Nested
    class DeleteDesign {
        @Test
        void designIsRemovedFromDb() {}
        @Test
        void return404OnInvalidIdParameter() {}
        @Test
        void return401IfNotAuthorized() {}
    }
}

f:id:dhirabayashi:20200421183516p:plain
JUnit5の@Nestedでグループ化

@DisplayNameまたはKotlinのバッククオートによる読みやすいテスト名

Javaでは、読みやすいテストの説明を書くのにJUnit5の@DisplayNameを使います。

public class DisplayNameTest {
    @Test
    @DisplayName("Design is removed from database")
    void designIsRemoved() {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404() {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401() {}
}

f:id:dhirabayashi:20200421183700p:plain
JUnit5の@DisplayNameを使った読みやすいテストメソッド名

Kotlinでは、バッククオートの中にメソッド名を書くことができ、半角スペースを含むこともできます。これで冗長性を避けつつ読みやすくすることができます。

@Test
fun `design is removed from db`() {}

リモートサービスをモック化する

HTTPクライアントをテストするためには、リモートサービスをモック化する必要があります。私はそのためにOkHttpのWebMockServerを使うことを好みます。

MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
        .addHeader("Content-Type", "application/json")
        .setBody("{\"name\": \"Smartphone\"}"));

ProductDTO productDTO = client.retrieveProduct("1");

assertThat(productDTO.getName()).isEqualTo("Smartphone");

非同期コードのアサーションのためにAwaitilityを使う

Awaitilityは非同期コードのテストのためのライブラリです。最終的に失敗するまで、どれくらいの頻度でアサーションを行うのか簡単に定義できます。

private static final ConditionFactory WAIT = await()
        .atMost(Duration.ofSeconds(6))
        .pollInterval(Duration.ofSeconds(1))
        .pollDelay(Duration.ofSeconds(1));

@Test
public void waitAndPoll(){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
        assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
    });
}

この方法だと、不安定なThread.sleep()をテストの中で使うことを避けられます。

しかし、同期コードをテストすることのほうがはるかに簡単です。それが非同期実行と実際のロジックを分けるべき理由です。

ブートストラップDIは必要ない(Spring)

(Spring)DIフレームワークのブートストラップではテストが開始するまでに何秒かかかります。特にテストの初期開発の期間では、それによってフィードバックサイクルが遅くなります。

私が普段統合テストでDIを使わないのはそれが理由です。私は必要なオブジェクトを手動でnewを呼んでインスタンス化し、それらをまとめます。コンストラクタインジェクションを使っているなら、非常に簡単です。ほとんどの時間、あなたは自分が書いたビジネスロジックのテストをしたい。そのためにDIは必要ありません。一つの例として、統合テストについての私の投稿をチェックしてください。

一方で、Spring Boot 2.2では怠惰なBean初期化を簡単に使用できる機能が導入される予定で、DIベースのテストが著しくスピードアップするはずです。*12

実装をテスト可能にする

staticアクセスを使わない。決して。これからも。

staticアクセスはアンチパターンです。第一に、それは依存性と副作用を難解にし、コード全体が理解しにくくなり、エラーが発生しやすくなります。第二に、staticアクセスはテスト容易性を害します。あなたはもはやオブジェクトを交換できません。しかしテストでは、あなたはモックまたは異なる設定を持った実際のオブジェクト(テストデータベースを指しているDAOのような)を使いたいはずです。

そのためstaticアクセスするコードの代わりに、staticではないメソッドにそのコードを書き、クラスをインスタンス化して、必要なオブジェクトのコンストラクタにそのオブジェクトを渡します。

// 悪い例
public class ProductController {
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = ProductDAO.getProducts();
        return mapToDTOs(products);
    }
}
// 良い例
public class ProductController {
    private ProductDAO dao;
    public ProductController(ProductDAO dao) {
        this.dao = dao;
    }
    public List<ProductDTO> getProducts() {
        List<ProductEntity> products = dao.getProducts();
        return mapToDTOs(products);
    }
}

幸運にも、SpringのようなDIフレームワークがstaticアクセスを避ける簡単な方法を提供しています。私たちのために全てのオブジェクトの生成と配置を処理するからです。

パラメータ化

クラスの全ての関連する部分を、テストによってコントロール可能にしましょう。これは、この側面からコンストラクタのパラメータを作ることで実現できます。

たとえば、DAOのクエリ数の上限が1000になっているとします。この上限をテストするには、テストの中でデータベースエントリーを1001個生成することが求められるでしょう。この上限値をコンストラクタパラメータ化することで、その上限値が設定可能になります。本番環境では、このパラメータは1000です。テストでは、2を設定することができます。その上限機能のテストのために必要なテストエントリー数はたった3つだけで済みます。

コンストラクタインジェクションを使う

テスト容易性が低くなるため、フィールドインジェクションは邪悪です。
あなたはテストの中でのDI環境のブートストラップ、またはハッキーなリフレクションマジックを使わなければなりません。
そのため、コンストラクタインジェクションは好ましい方法です。なぜなら、テストの中で依存するオブジェクトのコントロールが簡単になるからです。

Javaでは、少し定型文が必要です。

// 良い例
public class ProductController {

    private ProductDAO dao;
    private TaxClient client;

    public CustomerResource(ProductDAO dao, TaxClient client) {
        this.dao = dao;
        this.client = client;
    }
}

Kotlinでは、同じ内容がもっと簡潔になります。

// 良い例
class ProductController(
    private val dao: ProductDAO,
    private val client: TaxClient
){
}

Instant.now()またはnew Date()を使わない

本番コードでInstant.now()またはnew Date()を呼び出すことで現在のタイムスタンプを取得するようなことをしてはいけません。あなたがその振る舞いをテストしたいのであれば。

// 悪い例
public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update()
            .set("dateModified", now);
        Query query = Query()
            .addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}

問題点は、その生成されたタイムスタンプをテストによってコントロールすることができないことです。テストの実行ごとに常に異なる値となるため、正確な値をアサートできません。代わりに、JavaClockクラスを使いましょう。

// 良い例
public class ProductDAO {
    private Clock clock; 

    public ProductDAO(Clock clock) {
        this.clock = clock;
    }

    public void updateProductState(String productId, State state) {
        Instant now = clock.instant();
        // ...
    }
}

テストの中で、あなたは今clockのモックを生成できるようになり、それをProductDAOに渡して、そのclockのモックが固定のタイムスタンプを返すように設定できます。updateProductState()を呼んだ後、定義されたタイムスタンプがデータベースに挿入されたかどうかをアサートします。

非同期実行と実際のロジックを分ける

非同期コードをテストするのはトリッキーです。Awaitilityのようなライブラリは助けとなりますが、それはまだ面倒で、テストはまだ不安定です。もし可能なら、(多くの場合は同期的な)ビジネスロジックを非同期実行から分割することは理にかなっています。

たとえば、ビジネスロジックProductControllerの中に配置することにより、簡単な同期実行でそれをテストすることができます。非同期的で並列的なロジックはProductSchedulerに集約され、分離してテストできます。

// 良い例
public class ProductScheduler {

    private ProductController controller;

    @Scheduled
    public void start() {
        CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
        CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
        String usResult = usFuture.get();
        String germanyResult = germanyFuture.get();
    }
}

Kotlin

私のBest Practices for Unit Testing in Kotlinについての投稿は、Kotlinでテストを書くための多くのKotlin固有の推奨事項を含んでいます。

*1:[訳注] どういうテストなのかよくわからなかったのですが、どうやら期待値をハードコードするテストのことのように思えます。

*2:[訳注] 統合テストのことのようです。

*3:[訳注] java.time.Clockクラスのことです。

*4:[訳注] 正常系みたいな意味のようです。

*5:[訳注] ここでは一つのテストの大きさと内容理解の困難さを問題視しているわけですが、調べてみても「ハッピーパステスト」という言葉にはそのような意味はなさそうで、どういう意味なのか正直よくわかりません。ただ、この一文の意味がわからなくてもあまり支障はないのでそのままにしています。

*6: [訳注] 意味がよくわからなかったのですが、AllInklusiveBaseTestから継承したメンバのうちAllInklusiveBaseTest由来のものなのか、AdvancedBaseTest由来のものなのか区別がつきにくい、つまり継承階層が深いと何がどこから継承されているのかわかりにくいということを言っているのだと推察します。もっとも、誤訳によって変な文になっているという可能性もありますが…

*7:[訳注] ここでは期待値の生成のために本番コードを使っています。期待値の生成と実測値の生成の両方で本番コードを使っているため、実質的には何もテストしていないのと同じことになると考えられます。

*8:[訳注] AssertJがやってくれるので、あなたが頑張ってロジックを書く必要はないということです。

*9:[訳注] この「更新」は元記事で「Update」となっているのを忠実に訳したもので、元記事にもともとあったものです。今お読みになっている翻訳版の更新ではありません。

*10:[訳注] fluentな。メソッドチェーンで書けるみたいな意味です。

*11:原文では「out-of-the-box」で、初期設定のままでも使えるみたいな意味です。後述の通り、AssertJではas()メソッドを使ってエラーメッセージをカスタマイズできますが、それをしなくても良いエラーメッセージを出しますよ、ということだと思います。

*12:[訳注] 翻訳時点ではリリース済みです。おそらくこれです。

OSS Gate東京ワークショップ2019-12-14に参加してJenkinsにコントリビュートした話

タイトルは嘘ではないですが若干釣りです。

掲題の通りOSS Gateのワークショップに参加してきたのですが、とても良かったので参加報告です。
oss-gate.doorkeeper.jp

詳細は公式サイトを見ていただくほうがいいですが、簡単に言えばOSSへのコントリビュートを体験できるという魅力的なイベントです。
疑似体験ではなく、なんと実在するOSSに対して実際にコントリビュートします。
oss-gate.github.io

OSSに何らかの形でコントリビュートしたいとは常々思っていたのですが、なんとなく敷居が高いと感じてなかなか行動に移せずにいました。
そう思っていたところでこのイベントを見つけて、恐る恐る参加してみました。

ポジションについて

大雑把に分けるとビギナー、サポーターというポジションがあります。
ビギナーはOSSのコントリビュートの経験がないか、もしくは自信がない人で、イベントの中でコントリビュートを体験するのはビギナーです。私はビギナーとして参加しました。
サポーターはその名の通り、ビギナーを補佐する役です。今回はビギナー1人に対して(ほぼ)サポーター1人でした。(その回によって違うようですが)

大雑把な作業の流れ

作業時は、できるだけ細かくメモすることを求められました。あとで振り返りを行うためです。
作業メモは、GitHubのワークショップ用のリポジトリにIssueを立ててそのコメントとして書きました。
私の作業メモを晒してみます。
github.com

対象のOSSを決める

普段使っているOSSを洗い出し、その中から一番興味のあるものを選ぶという形でした。私はJenkinsにしました。
また、重要なこととして、それが本当にOSSなのか調べる必要があります。
ライセンスを調べて、そのライセンスが下記の一覧の中にあればOSSであると言えるそうです。
opensource.org

ライセンスは公式サイトか、GiHubのLISENCE.txtから調べます。Wikipediaにも書いてあったりはしますが、原典で調べる必要があります。

Jenkinsを動かす

これも、あくまでも公式サイトを見ながら作業する必要があります。公式サイトのドキュメントに何か問題があれば、それがコントリビュートチャンスになるからです。結果的には、私はそこでコントリビュートしました。
対象のドキュメントですが、作業していた時点では、Jenkinsの動作要件として「Java 8」と書いてありました。(今は修正済みなので下記はInternet Archiveです)
web.archive.org

私のマシンにはJava 13しか入ってなかったので、それで動くかどうかとりあえず試してみました。
結果としてはダメで、下記のようなエラーメッセージが出ました。

Jenkins requires Java versions [8, 11] but you are running with Java 13 from

Java 8かJava 11 で動くというように読めます。なので、Java 11で試したところ、今度はエラーが出ずに動作しました。

フィードバック

ドキュメントには動作要件としてJava 8としか書かれていませんでしたが、Java 11でも動作すると読めるエラーメッセージが出力されており、実際Java 11でも動いたので、ドキュメントに不備があると言えます。なので、そこを修正してコントリビュートしようということになりました。

まず、どうすればコントリビュートできるのかを公式サイトで調べます。
詳細な手順は下記に書いてありました。
github.com

私の拙い英語力でも、とりあえずフォークしてローカルにクローンして修正、コミット、プッシュ、プルリクでよさそうだなということはわかりました。
Issue Trackerもあったのですが、軽微な修正なのでいきなりプルリク投げても大丈夫だろうとサポーターの方にアドバイスをもらいました。

本当は修正後にビルドできるか確認してからコミットすべきなのですが、今回の修正は既存のセクションに何文字か足すだけの軽微な修正であり、ビルドが壊れるとはあまり考えられないので、ビルド確認はスキップしました。
また、ビルドにDockerが必要で、お恥ずかしながら(?)私のマシンにはDockerが入ってないので、ビルドしてたら時間がなくなりそうだったというのもありますが…

そんな感じで投げたプルリクはこれです。
github.com

説明はプルリクのメッセージに書けばいいので、コミットメッセージやプルリクのタイトルは簡素でよいとアドバイスを受けて、そのようにしました。
プルリクのメッセージは当然英語で書かなければなりません。これまた私の拙い英語力で(Google翻訳の力を借りながら)絞り出した英文を書きました。たぶん怪しい英語になっていると思いますが、一応伝わったようで、マージしてもらえました。本番サイトにも反映されました。やったね!
jenkins.io

感想

軽微な修正とはいえ、普段使っているJenkinsにコントリビュートできて本当にうれしいです。
ビルド部分をスキップしたとはいえ、OSSを決めてからプルリクを投げるまでの一連の流れを体験できて、意外とハードルは高くないんだなと感じました。1回やったので2回目もできるかな、と少し自信がつきました。
あと、コントリビュートしたくてもネタがないと思っていたのですが、Jenkinsのように多くの人に使われているOSSでもコントリビュートチャンスはあったわけなので、探せばネタはいくらでも見つかりそうだなとも思いました。今回私も含めてビギナーは4人いて、全員何らかのフィードバックできたとのことだったので、なおさらですね。

そうなると、あとはやるだけです。「楽しかった」だけで終わらせてしまうと参加した意味があまりないので、今後も何かしらコントリビュートを続けていきたいと思います。積極的にネタを探すか、そこまではせずに問題を見つけたらフィードバックに努めるというスタンスにするか、次回のワークショップではサポーターとして参加してみるか、どうするかは考え中ですが。

とりあえず、とてもいいイベントなので皆さんも参加してみませんか?次回は一月です。
oss-gate.doorkeeper.jp

最後に、お世話になった運営者の方や進行役の方、サポーターの方々にお礼を言いたいです。ありがとうございました。

JJUG CCC 2019 Fallに参加してきた

ccc2019fall.java-users.jp
JJUG CCC 2019 Fallに参加してきたので感想を書きます。
今回も午後からです。

Javaで学ぶネットワークプログラミングの基礎

主にネットワークの基礎知識を解説する発表でした。
個人的には勉強したことのある内容ですが、だいぶ忘れてたので良い復習になりました。

後半はJavaによる例があり、Socketクラスを使ったHTTPクライアント、
ServerSocketクラスを使ったHTTPサーバの実装の例がありました。
業務でこれらのクラスを直接扱うことはないだろうと思いますが、プロトコルの勉強をするなら良さそうですね。
面白いと思いました。

長く続くサービスがモダンであり続けるには

slides.com
内容が多岐にわたるのであまり頭に入っていない面もあるのですが、
とりあえず依存ライブラリが最新版かどうかチェックできるというgradle-versions-pluginは気になりました。
あとJIRAのチケットの自動作成も。弊社ではJIRAではなくRedmineを使ってますが、
後で調べたらRedmineでもREST APIを提供しているようなので、同様のことはできそうです。

AngularとSpring Bootで作るSPA + RESTful Web Serviceアプリケーション - 開発ツールやプロジェクト構成も含めて45分で丸わかり -

www.slideshare.net
名前はよく聞くけどよく知らないSpring Bootについて知りたくて聴講してみました。
とりあえず概要はわかったと思います。
JavaでSPAを作りたい時はこちらのスライドを見ながらやると捗りそうですね。

多言語対応の仮想マシンGraalVMが照らす未来

GraalVMもたまに聞くけどよく知らないワードの一つだったので聴講しました。
GraalVMはこちらのセッションだけでなく、他のセッションでも出てきてたそうで、今ホットな話題のようですね。
残念なことに、いつもの通り後半に差し掛かる頃には疲れててあまり頭に入ってこなかったです……
まあ、とりあえず概要はわかったと思います。(二度目)
ネイティブイメージ化すれば速いとか安直に考えてましたが、起動は速いけど実行速度は必ずしも速いわけではないようです。
思ったよりも奥が深そうですね。
あと、パフォーマンスの話は本質ではなく、多言語対応のほうが本質という話を冒頭でされてました。
とりあえず、GraalVMの今後の動向はウォッチしたほうがよさそうです。

その他

今回も最後のコマと懇親会はスキップしました。
あと、アンカンファレンスにも参加してみたいと前回言ったな、あれは嘘だ。

PyCon JP 2019に参加してきた

pycon.jp

にわかPython使いですが、掲題の通りPyCon JP 2019に参加してきたので、遅ればせながら感想を書きます。
PyCon JPは今回が初参加です。

1日目

Pythonでの開発を効率的に進めるためのツール設定

www.slideshare.net

普段Pythonを書いてないものですから、静的解析ツール、コードフォーマッター、UTフレームワークなどにどのようなものがあるか知りませんでした。
そういうことが知りたいなーと思って聴講したのですが、まさにこちらのセッションで一通り知ることができました。本格的にPythonを書くなら必須の知識となりそうですね。
flake8は静的コード解析ツール、blackはコードフォーマッターで、mypyはtype hintのチェッカー、pytestはUTフレームワーク、そしてtoxは複数バージョンのPythonでテストを実行するためのvirtualenvやテストタスクの管理ツールです。
だいたいpipでインストールできて楽なのがPythonの良いところですね。

ちょっと遅れてしまったのと、レシーバーがないと聴けないセッションだったのに気づかずにあたふたしてしまったことで
冒頭は聴けなかったのですが、スライドを後から見直すことができて助かりました。

Djangoで実践ドメイン駆動設計による実装

現時点ではスライドが上がっていないようですが、動画はありました。
動画が上がるならわざわざ現地に行かなくてもいいのではないだろうか?
www.youtube.com
ドメイン駆動設計自体について「言葉は聞いたことあるなあ」という程度の認識だったのですが、
Djangoでの例の前に、ドメイン駆動設計自体についても説明があったので助かりました。
まあ理解できたかというと微妙ですが…発表ではなく私の理解力の問題で。

「エリック・エヴァンスのドメイン駆動設計」という定番の本があり、私もKindle版を持っている(読んだとは言っていない)のですが、
こちらの本は抽象的で難解とのことでした。ちょっとガックリ…

こちらのサイトがわかりやすいとのことなので、時間を見つけて読みます。
nrslib.com

Python による日本語自然言語処理 〜系列ラベリングによる実世界テキスト分析〜

speakerdeck.com
MeCabやKuromojiは知っていましたが、Pythonにはnagisaという類似のツールがあるということでした。nagisaはもちろんpipでインストールできます。
ただ、前2つが形態素解析を行うツールであるのに対し、nagisaは原型を求める処理がないところが形態素解析とはちょっと違うそうです。

手元でnagisaをインストールして動かしながらやりましょう、ということだったのですが、あいにくノートPCを持っていなかったのでできませんでした。
やっぱりこういったカンファレンスではノートPCは持参したほうがいいんですかねぇ。

ちなみに家で試してみたところ、「すもももももももものうち」はうまく解析できませんでした。kuromojiだと正しく解析してくれるのですが。ちょっと残念。まあ、何か設定やらオプションやらを変えたらできる可能性もありますが、そこまでは検証していません。

Pythonを使ったAPIサーバー開発を始める際に整備したCIとテスト機構

speakerdeck.com
本格的に開発するなら、CIも必須ですよね。普段の業務ではJenkinsを使っているのでよくわかります。
CIツールを使って、最初の「Pythonでの開発を効率的に進めるためのツール設定」で紹介されたようなツールを動かすわけですね。
(まあ、実行タイミングとしてはCIだけじゃないのですが)

Pythonでは具体的になんというツールを使うのかな、というのが知りたくて聴講しました。
CIツールの選択肢はいくつかあるようでしたが、こちらの発表ではCircel CIを使った事例が紹介されました。それがPython開発においてはスタンダードなのか?ということまではわかりませんでした。今思えば質問すればよかったですね…

CIツールの話だけでなく、そもそもどのようにUTを書くのかという内容もありました。
たとえば「外界」との接続が発生するコードのテストについて、私は全部モックでやればいいじゃんと思っていたのですが、
DB接続が発生するコードについては、SQLが本当に正しいのかのチェックなどのためにあえてテスト用DBを立ててテストする手もあるという話でした。
なるほど!

ちなみに、最初の発表で出てきたflake8、mypy、pytestなどはこちらの発表でも出てきました。

ListはIteratorですか?

docs.google.com
Pythonの組み込み型の継承関係、および型を意識したコーディングのやり方についての発表です。
タイトルの問いの答えはNoで、ListはIteratorではないがIterableであるということでした。どうでもいいですが、JavaのListでも同じですね。
関数の引数として、ListではなくIterableを受け取るようにすれば便利だねという話なのですが、Pythonは動的型付けなので、通常は静的な型のチェックはできません。
そこでtype hintが登場するわけですが、type hintは実行時は無視されるため、チェックツールが必要になります。そういうわけで、(私から見れば)3度目のmypy登場です。とてもよく使われるツールなのですね。

2日目

unittestで始めるユニットテスト入門

docs.google.com
Pythonの標準ライブラリとして組み込まれているunittestについての紹介でした。
UTフレームワークが標準で使えるのはさすがPythonといったところでしょうか。JavaでいうならJUnitが標準で入っているようなものですから。まあRubyとかGoでもそうだった気がするので、この場合Javaがアレなんじゃないかという気もしますが。
まあそれはともかく、使い方を事細かく説明されていたのでよかったです。あと、unittestではモックも使えるんですね。JUnitより高機能かもしれません。

それと、pytestについての紹介と、unittestからの移行方法についても発表がありました。pytestもまた(私から見れば)3度目の登場です。
unittestはけっこう高機能だと思いましたが、pytestはもっと高機能なようです。結局、可能であれば最初からpytestを使ったほうがいいんですかね?

Anaconda環境運用TIPS 〜Anacondaの環境構築について知る・質問に答えられるようになる〜

GitPitch Slide Deck
condaコマンドとpipは可能な限り共存しないほうがいい話と、仮想環境便利という話でした。
前者はcondaコマンドを優先して使い、condaコマンドでインストールできないライブラリについてだけpipでインストールすべきということでした。
逆にcondaを封印してpipで統一するのでもいい気がしたのですが、どうなんでしょうね。これも質問すればよかったかも。

pytestによるCIレボリューション

speakerdeck.com
またしてもpytest、Circle CIです。とても人気なんだなということがよくわかりました。
類似の内容の発表が既にあったというのと、あと2日目の最後ということで、疲労もあって内容をはっきりとは覚えてないです。(すみません)
スライドを見返してみますと、VS CodeやPyCharmでのpytestの実行方法、Circle CIとSlackの連携などが説明されており、
実務に即した良い発表だったと思います。

その他

ランチセッション、ポスターセッション、LTなどもあったのですが、あまり参加できませんでした。

まとめ

Pythonについては興味があって多少は勉強してきたのですが、実務でどのようなツールを使うのかは全然知りませんでした。
しかし、今回のセッションのおかげで実務で具体的にどんなツールが使われているのか知ることができました。
3回も出てきたmypy、4回も出てきたpytestはバッチリ覚えました。

特に印象的だったのはmypyです。mypyはtype hintのチェッカーですから、mypyを使うということは、動的型付けであるPythonであえて型を明示するということですからね。
私は普段業務でJavaを書いていますから、静的型付けのありがたみはよくわかっているつもりです。逆に動的型付けの言語ではどうやって品質を担保するのだろうと思っていたのですが、結局型を明示するんかい!
他の動的型付けの言語はともかく、少なくともPythonではそういう方向に進んでいるんですね。とても興味深いです。

そんなわけで、2日間でたくさんの収穫がありました。今後私がPythonを実務で使うことになるかどうかはわかりませんが、もしそうなったら今回学んだことは直接的に役に立ちそうです。ありがとうございました。

JJUG CCC 2019 Springに参加してきた

www.java-users.jp
掲題の通り、JJUG CCC 2019 Springに参加してきたので感想を書きます。
今回も午後からの参加です。毎回ですが。

Catch up Java 12 and Java 13

www.slideshare.net

Java12、およびJava13での変更点についての発表でした。
内容が多岐にわたるため、私の残念な頭ではそれほど内容を覚えられませんでした。
まあ、ありがたいことにスライドが公開されているわけなので、
無理に覚えなくても必要に応じて見返せばよいですね。

とりあえず、コード上で変化が表れるRaw String LiteralやPattern matchingは気になりますね。

ドメイン管理サービスにおけるJava活用のご紹介

現時点でスライドは上がっていないようです。
そもそもドメイン管理サービスというものについてよく知らなかったのですが、
その辺りから説明していただいたので助かりました。
とりあえず「レジストリ」「レジストラ」といったキーワードは覚えました。

入門: 末尾呼び出し最適化

speakerdeck.com

関数型言語を学べば必ず出てくる末尾再帰の最適化の話です。
末尾再帰がどのようなもので、何がうれしいのか程度ならもともと把握していましたが、
スタックフレームの中身とか、なぜ末尾再帰だと最適化が可能なのかといったもう少し掘り下げた話が聴けました。

あと、「継続」という概念について初めて知りました。
継続 - Wikipedia
なるほどわからん。要勉強です…

1400万ユーザーのWebサービスを15年運用して考える、Javaである理由

speakerdeck.com

Javaといえばなかなか歴史のある言語ということもあってdisられることも少なくないですが、
そんな中でJavaを使っている理由は何か、という話です。

歴史的な経緯もあったようですが、それ以外の「言語のユーザーが多い」「静的型付けでコンパイラが強力である」「例外を明確に処理できる」というのは概ね同意です。
検査例外はウザいことも多いので、その点はちょっと気になりますが…(発表では検査例外とは明言してなかったと思いますが、内容から検査例外のことだろうと理解しました)
まあ私はある程度大きな規模での開発はJavaでしかやったことないので、偉そうにあーだこーだ言える立場ではないのですが。
そういう意味だと、Java以外で開発する仕事もやってみたいなあ。

Project Loom - 限定継続と軽量スレッド

www.slideshare.net

Fiberという軽量スレッドの話です。OSに依存しない、JVMで管理されるスレッドということで、
メモリ使用量が少なく、タスクスイッチコストも小さく、さらに重要な点として従来のスレッドと同じように使えるということでした。
従来のスレッドのかなりの部分をFiberに置き換え可能とのことで、キャッチアップは不可避ですね。

ただ、いつ使えるようになるのかは不明で、聴講者の中にいた有識者によると3年後くらいではとのことでした。

それと、「限定継続」というキーワードが出てきました。先ほど出てきた「継続」と関連する話のようです。こちらもキャッチアップしなければ…

その他

最後のコマと懇親会はスキップしました。
次回はアンカンファレンスにも参加してみたいです。テーマにもよりますけど。

JJUG CCC 2018 Fallに参加してきた

www.java-users.jp

ちょっと時間が経ってしまいましたが、JJUG CCC 2018 Fallに参加してきましたので簡単に感想を書きます。
午後からの参加だったので、拝聴したセッションは多くはないですが。

JDK付属ツールにパッチを出しまくったワケ

www.slideshare.net

私自身は考えてみたらJDK付属ツールを普段あまり使わないので内容を理解できたとは言い難いのですが、
pushしたらテストが失敗して一旦リジェクトされてしまったとか、リリースノート(英語)を書いたらほとんど全面的に修正されたとか、
リアルな話が聴けて面白かったです。
あと、英語が苦手だとしてもみんな優しくて理解してくれようとするという話が印象に残っています。

既存アプリケーションでKotlinを導入してみた

docs.google.com

既存アプリケーション云々はあまり関係なくて、単純にKotlinの便利な記法を紹介するという感じでした。
Javaのコードと比較する形で紹介されていたのでわかりやすかったです。
戻り値を複数返せる機能の注意点について、順番を間違える可能性があるという話でハッとしました。
型推論があるが故の不幸ですね…そんなこともあるんだと気づかされました。

Dataクラスも便利ですよね。JavaでもLombokを使えば同じようなことは実現できますが、個人的にはLombokでちょっと痛い目にあっているので、あまり使いたくない…
やはり言語自体がそういう機能を持っているのは大きいです。

ただ、個人的には既存アプリケーションに導入するほどのメリットがあるかどうかは若干疑問です。新規アプリケーションならアリだと思いますが。

Learn JDK11 with JShell

qiita.com

JShellを使ってJava10やJava11で導入された新機能や新しいAPIなどのデモを行うセッションでした。
String#repeat()などの地味なメソッド追加の話がなぜか印象に残っています。こういうのを試すのにJShellはうってつけですね。

マイクロソフト牛尾さん渡米直前記念」  外資系企業で働くエンジニアの生産性向上物語

興味深い話が色々ありましたが、無理やりまとめれば「アウトプット重要」「英語重要」という感じです。
英語に関しては、英語の学習メソッドは確立されているので、できない人はただやっていないだけだというなかなか手厳しいご指摘もありました。
英語に関しては色々と思うところもあるのですが、何を書いても言い訳になるので書くのはやめておきます。

せめてアウトプットについては少しでもやろうと思って、遅ればせながら今ブログを書いている次第です。

まとめ

JJUG CCCには何度も参加しているのですが、ブログに書くのは初めてです。
復習になるのでブログにまとめるのも悪くないなと思いました。

それにしても、わりと遅めの時間からの参加だったのですが、けっこう疲れました。
早い時間のセッションを聴けないのは残念ですが、私にはこれくらいがちょうどいいのかもしれません。