【技術記事】Apache Beam with Cloud Dataflow(over 2.0.0系)入門~基本部分~ParDoまで~

Apache Beam with Cloud Dataflow(over 2.0.0系)入門~基本部分~ParDoまで~

何回に分けてApache Beam with dataflowについて書こうと思う。
今回は、基本的なこと~TransformのParDoまでを書く。
最後にローカルで動作するコードも書く。

Apache BeamとCloud Dataflow

Apache Beamとは

バッチ処理とストリーミング処理のためのプログラミングモデル

Implement batch and streaming data processing jobs that run on any execution engine.

(意訳) あらゆる実行エンジンで動作するバッチとストリーミングデータ処理ジョブの実装
引用元 : Apache Beam

Cloud Dataflowとは

GCPApache Beamの実行環境

Apache BeamとCloud Dataflowの関係

Apache Beamで実装し、Cloud Dataflowで実行する」といった感じ。
ちなみに、実行環境はGoogle Cloud Dataflowのみではない。
詳しくは以下を参照
Apache Beam Capability Matrix

Batchとstreaming

Apache Beamでは、Batch処理とstreaming処理を同じような実装で扱えるようにしようとしている。ここでそもそものBatchとstreamingの定義を考えてみたい。

Batch

バッチ処理とは、一定期間(もしくは一定量)データを集め、まとめて一括処理を行う処理方式。または、複数の手順からなる処理において、あらかじめ一連の手順を登録しておき、自動的に連続処理を行う処理方式。

引用元 : バッチ処理(一括処理)とは - IT用語辞典

=> つまり、範囲が存在する(bounded)なもの。

Streaming

無限に発生し続けるデータを処理するよう設計されたデータ処理モデル

引用元 : 最近のストリーム処理事情振り返り
詳しくは以下を参照
最近のストリーム処理事情振り返り
【要約】The world beyond batch: Streaming 101 - Qiita

=> つまり、範囲が存在しない(unbounded)なもの。

重要な4つの用語

Pipeline

データ処理タスク全体をカプセル化する。(inputデータの読み取り=>データの変換、=>outputデータの書き込み 全体)
Beam Driverのプログラムを作成するときには、このPipelineクラスの作成が必須。

PCollection

Pipelineが動作する分散データセットを表す。このPipelineにセットするデータは、有限のデータでも無限のデータでも良い。誤解を恐れずに言えば、JavaのCollectionクラスの一種のようなものであると考えれば良いかもしれない。
基本的に、inputデータからこのPCollectionというデータセットを生成して、加工処理して、新しいPCollectionを生成して、outputに出力するというのが大まかな流れ。

Transform

わかりやすかったので、公式からの引用

A Transform represents a data processing operation, or a step, in your pipeline. Every Transform takes one or more PCollection objects as input, performs a processing function that you provide on the elements of that PCollection, and produces one or more output PCollection objects.

(意訳) Transformは、パイプラインのデータ処理操作またはステップを表す。全ての Transformは、1つ以上のPCollectionオブジェクトを入力として受け取り、その要素に対して提供する処理関数を実行し、PCollection1つ以上の出力PCollectionオブジェクトを生成する。
引用元 : Beam Programming Guide

I/O Source and Sink

Source :外部データからの読み込みを表す(input)
Sink:外部データへの書き込みを表す(output)

上記の用語の説明は以下を参考にさせていただいている。
Beam Programming Guide

Apache Beamの大まかな流れ

1 Pipelineを作成(この際にPipeline Runnerに依存する部分を含む実行時のオプションを指定)
2 Source APIを使用して外部データの読み込み 、それを元にPcollectionを生成
3 Transformを実施
4 Sink APIを使用して外部ソースにデータを書き込み
5 指定したPipeline Runner で実行

簡単な図にすると以下のような感じ
1 Pipelineをcreate => 2 Source APIでinput => 3 Transform => 4 Sink APIでoutput 5 Run

雛形を作る

mavenの場合

以下の公式の手順に従って行う
Java と Apache Maven を使用したクイックスタート | Cloud Dataflow のドキュメント | Google Cloud Platform

ただし、Maven Archetype Pluginを使用して、サンプルを含んだ Maven プロジェクトを作成するときは、以下で行う(-DarchetypeVersion=2.0.0にする)

mavenarchetype:generateついて詳しく知りたい場合はいかがわかりやすい
Maven を使った Java project 作成方法 - Qiita
Maven Archetype Plugin – archetype:generate
2. Maven 入門 (2) | TECHSCORE(テックスコア)

mavenリポジトリ
Maven Repository: com.google.cloud.dataflow

gradleの場合

[IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow - Qiita]
(http://qiita.com/Sekky0905/items/40546ba36113a37a2dd3)を参照

Pipelineのoptionについて

Pipeline runner(つまり、Cloud Dataflowなどの実行環境)の設定をPipelineのoptionで設定する。これを設定する際には、コード上からプログラムによって設定するよりもコマンドラインでの引数によって設定する方が好ましい。というのも、Apache Beamの良いところの1つに、基本的にApache Beamで一回実装してしまえば、どのRunner(Apache Beamに対応する実行環境)であっても実行することができるPortabilityがあり、コード上でRunnerの設定をしてしまうと、コードによってRunnerが固定されてしまいこのPortabilityが発揮されなくなってしまうからである。(コマンドラインの引数でRunnerの設定を指定できるようにすれば、実行時に実行環境を選ぶことができ、1つのコードで様々なRunnerで実行する時に使い回すことができる)

Transform

Transformとは、Pipelineに対する操作。
PipeLineをinputして新しいPipelineをoutputする。
Input PCollection => Transform => Output PCollection

コード的には以下のように書く

[Output PCollection] = [Input PCollection].apply([Transform])

Beam Programming Guide よりコード引用

メソッドチェーンのようにもつなげることができる(厳密にはメソッドチェーンとは違うようだが…)

[Final Output PCollection] = [Initial Input PCollection].apply([First Transform])
.apply([Second Transform])
.apply([Third Transform])

Beam Programming Guide よりコード引用

Apache Beamでは、汎用的な処理フレームワークを提供していて、開発者は処理ロジックを関数オブジェクトで定義する。
この関数オブジェクトはuser codeと呼ばれる。

Apache Beamでは以下の5つのCore TransformというTransformを提供している。
今回は、ParDo のみ説明を行う
* ParDo
* Using GroupByKey
* Using Combine
* Using Flatten and Partition

ParDo

一般的な並列処理をする時に使用する。
Map/Shuffle/Reduce で言うところの Mapper のようなもの。
inputとしてPCollectionが入力され、処理を行いinputされたCollectionを加工してoutputとして0~複数の新しいPCollectionを出力する。

こんな感じです。

// まずは外部ソースから読み込んで、PCollectionを生成する
PCollection<String> inputData = // ここで外部ソースから読み込みを行う;

// static classとして関数オブジェクトを定義 
// DoFnの左側のinputの型を、右側にoutputの型を型パラメータとして定義する
// 必ず、DoFnをextendsする 
static class FilterEvenFn extends DoFn<String, Integer> {
  // 実際の処理ロジックをアノテーションで宣言する
  @ProcessElement
  // 実際の処理ロジックは、processElementメソッドに記述する
  // 引数のProcessContextを利用してinputやoutputを行う 
  public void processElement(ProcessContext c) {
    // ProcessContextからinput elementを取得
    int num = Integer.parseInt(c.element());
    // input elementを使用した処理
    if (num % 2 == 0) {
        // ProcessContextを使用して出力
        c.output(String.valueOf(num));
    }
  }
}
// 
PCollection<Integer> evenData = inputData.apply(
    ParDo
    .of(new FilterEvenFn()));  

データの読み込み

INPUT_FILE_PATHの場所で読み込むリソースを指定する
Runnerの環境に依存している場合は、それに従う(例えば、Google Cloud PlatformのGoogle Cloud Storageなど )

PCollection<String> newPCollection = pipeline.apply(TextIO.read().from(INPUT_FILE_PATH));

データの書き込み

Runnerの環境に依存している場合は、それに従う(例えば、Google Cloud PlatformのBigQueryなど )

pCollection.apply(TextIO.write().to(OUTPUT_FILE_PATH));

Pipelineの実行

PipeLine optionで指定したRunnerでPipelineを実行する

pipeline.run();

実際に簡単なコードを書いてみた

今回はGoogle Cloud Platformのようなクラウド上の実行環境ではなく、自分のローカル環境で動作するような簡単なコードを書いてみた。

以下のようなファイルを偶数のみ抽出する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

個々の処理はコードにコメントとして記述している。

package com.company;

import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.values.PCollection;


/**
 * メイン
 */
public class Main {
    // DoFnを実装したクラス
    // DoFnの横の<T,T>でinputとoutputの方の定義を行う
    static class FilterEvenFn extends DoFn<String, String> {
        // 実際の処理ロジックにはこのアノテーションをつける
        @ProcessElement
        // 実際の処理ロジックは、processElementメソッドに記述する
        // 引数のProcessContextを利用してinputやoutputを行う
        public void processElement(ProcessContext c) {
            // ProcessContextからinput elementを取得
            int num = Integer.parseInt(c.element());
            // input elementを使用した処理
            if (num % 2 == 0) {
                // ProcessContextを使用して出力
                c.output(String.valueOf(num));
            }
        }
    }

    // インプットデータのパス
    private static final String INPUT_FILE_PATH = "./dataflow_number_test.csv";
    // アウトデータのパス
    private static final String OUTPUT_FILE_PATH = "./sample.csv";

    public static void main(String[] args) {
        // まずPipelineに設定するOptionを作成する
        // 今回は、ローカルで起動するため、DirectRunnerを指定する
        // ローカルモードでは、DirectRunnerがすでにデフォルトになっているため、ランナーを設定する必要はない
        PipelineOptions options = PipelineOptionsFactory.create();

        // Optionを元にPipelineを生成する
        Pipeline pipeline = Pipeline.create(options);

        // inout dataを読み込んで、そこからPCollection(パイプライン内の一連のデータ)を作成する
        PCollection<String> inputData = pipeline.apply(TextIO.read().from(INPUT_FILE_PATH));

        // 処理
        PCollection<String> evenData = inputData.apply(ParDo.of(new FilterEvenFn()));
        // 書き込む
        evenData.apply(TextIO.write().to(OUTPUT_FILE_PATH));

        // run : PipeLine optionで指定したRunnerで実行
        // waitUntilFinish : PipeLineが終了するまで待って、最終的な状態を返す
        pipeline.run().waitUntilFinish();
    }
}

実行結果

以下のファイルが生成される
sample.csv-00000-of-00004
sample.csv-00001-of-00004
sample.csv-00002-of-00004
sample.csv-00003-of-00004

それぞれのファイルの中身は以下。(分散されて並列で処理されているので、バラバラ)

sample.csv-00000-of-00004

4
12

sample.csv-00001-of-00004

8

sample.csv-00002-of-00004

2
10
14

sample.csv-00003-of-00004

6

このファイルをBigQueryに突っ込むなりして分析したりするといいかもしれない。

参考に名せていただいたサイト

バッチ処理(一括処理)とは - IT用語辞典

Apache Beam

最近のストリーム処理事情振り返り

【要約】The world beyond batch: Streaming 101 - Qiita

【要約】The world beyond batch: Streaming 102 - Qiita

Beam Programming Guide

Java と Apache Maven を使用したクイックスタート | Cloud Dataflow のドキュメント | Google Cloud Platform

Maven を使った Java project 作成方法 - Qiita Maven Archetype Plugin – archetype:generate 2. Maven 入門 (2) | TECHSCORE(テックスコア)

Maven Repository: com.google.cloud.dataflow

パイプラインの実行パラメータを指定する  |  Cloud Dataflow のドキュメント  |  Google Cloud Platform

※ Qiitaでも同一の投稿を行っている
Apache Beam with dataflow入門~基本部分~ParDoまで~ - Qiita

【技術記事】IntelliJとGradleで始めるApache Beam with Google Cloud Dataflow

IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow

基本的にドキュメントでは、Mavenでのクイックスタートしか書いていなかったので、Apache BeamをGradleとIntelliJで始める方法をメモする。

今回は、Pipelineに対するOptionを指定することなどは考えず、とりあえずローカル環境で動かせるようにするための設定である。
Pipelineに対するOptionを指定するなど、諸々のことを考慮したものを今後追加記述する可能性もある。

方法

1 intelliJ IDEAでCreate New Project

1.png

2 GradleとJavaを選択

2.png

3 groupIdとartifactIdを指定

3.png

groupId:プロジェクトのルートパッケージ名
artifactId : プロジェクト名

4 諸々の設定

設定を以下のようにする

4.png

5 project nameとproject locationを設定

表示されたままでよければ進む

6 以下のbuild.gradleに変更する

group 'Hello_cloud_dataFlow'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'com.google.cloud.dataflow', name: 'google-cloud-dataflow-java-sdk-all', version: '2.0.0'
    testCompile group: 'junit', name: 'junit', version: '4.11'
}

7 ビルドを待つ

build.gradleを上記に変更してしばらく待つと、IntelliJが勝手にcradleをbuildしてくれるので、Apache Beamを使用できるようになる。

Mavenリポジトリ

掲載したbuild.gradleのような感じで、以下のMavenリポジトリから、引っ張ってこられる
Maven Repository: com.google.cloud.dataflow

参考にさせていただいたサイト

Gradle初心者によるGradle事始め - Qiita

Maven Repository: com.google.cloud.dataflow スクリーンショット 2017-07-11 22.28.43.png

※ Qiitaでも同一の投稿を行っている IntelliJとGradleで始めるApache Beam 2.0.x with Google Cloud Dataflow - Qiita

【技術記事】Javaでユーグリッドの互除法を実装してみた

ユーグリッドの互除法とは

2つの自然数の最大公約数を求めるアルゴリズム

簡単に表すと

a % b = r
b % r = s
r % s = 0

といった形で、割る数を次の割られる数、余りを次の割る数にして再帰的にそれを繰り返して行く。
余りが0になった時の割る数(上の例だとs)が自然数aとbの最大公約数になる。

参考
ユークリッドの互除法の証明と不定方程式 | 高校数学の美しい物語

実装してみた

package com.company;

import java.io.*;

class Main {
    public static void main(String[] args) {
        try {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            String[] str = bufferedReader.readLine().split(" ");
            int x = Integer.parseInt(str[0]);
            int y = Integer.parseInt(str[1]);

            System.out.println(getCommonDivisor(x, y));

        } catch (Exception e) {
            System.out.println(e);
        }
    }

    private static int getCommonDivisor(int x, int y) {
        int biggerNum = Math.max(x, y);
        int smallerNum = Math.min(x, y);

        // 大きい方から小さい方を割った余を求める
        int surplus = biggerNum % smallerNum;

        // 割り切れていれば、それを返す
        if (surplus == 0) {
            return smallerNum;
        }
        // 割り切れなければ再帰的に自信を呼び出す
        surplus = getCommonDivisor(smallerNum, surplus);

        return surplus;
    }
}

試してみる

// 入力
390 273 

// 出力
39

追記(2017年6月23日)

上記のコードは、もともと入力などについては深く考えず、ただアルゴリズムを実装してみたというものだったので、例外処理などを考慮しておらず、簡単に落ちます。

Qiita の方で、その点をご指摘いただきました。
そこで、いただいたコメントを反映したコードを新たに記述しました。

今回は、以下のような条件でコードを記述しています。

ユークリッドの互除法の実装なら負の数は入り口ではじく。

数字以外のものの入力がされても、落ちない

また、自然数に0を含める流派と自然数に0を含めない流派が存在しますが、今回は、「自然数に0を含めない流派」を採用します。

例外処理を行ったコード

package com.company;

import java.io.*;

class Main {
    private static int x = -1;
    private static int y = -1;
    private static final String caution = "自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)";

    public static void main(String[] args) {
        System.out.println(caution);
        readInput();
        System.out.println(doEuclideanAlgorithm(x, y));
    }

    private static void readInput() {
        try {
            while (x <= 0 || y <= 0) {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
                String[] str = bufferedReader.readLine().split(" ");
                x = Integer.parseInt(str[0]);
                y = Integer.parseInt(str[1]);
                if (x <= 0 || y <= 0) {
                    System.out.println("入力が不適切です。" + caution);
                }
            }
        } catch (Exception e) {
            System.out.println("入力が不適切です。" + caution);
            readInput();
        }
    }

    private static int doEuclideanAlgorithm(int x, int y) {
        int biggerNum = Math.max(x, y);
        int smallerNum = Math.min(x, y);

        // 大きい方から小さい方を割った余を求める
        int surplus = biggerNum % smallerNum;

        // 割り切れていれば、それを返す
        if (surplus == 0) {
            return smallerNum;
        }
        // 割り切れなければ再帰的に自信を呼び出す
        surplus = doEuclideanAlgorithm(smallerNum, surplus);

        return surplus;
    }
}

試してみた

自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
a a
入力が不適切です。自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
390 0
入力が不適切です。自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
0 273 
入力が不適切です。自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
-390 273
入力が不適切です。自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
390 -273
入力が不適切です。自然数を半角空白区切りで2つ入力してください(ただし、本プログラムでは自然に0は含めないものとする)
390 273 
39

参考にさせていただいたサイト

ユークリッドの互除法の証明と不定方程式 | 高校数学の美しい物語

※ Qiitaでも同一の投稿をしている
Javaでユーグリッドの互除法を実装してみた - Qiita

公式ドキュメントで新しい技術を習得する方法

公式ドキュメントで新しい技術を習得する方法

何かの技術を新たに学ぶ時、特に最新の技術を学ぶ場合や日本でのユーザーが少ない技術の場合、その技術に関する書籍や記事がまだ存在しなかったり、少なかったりすることがある。その場合、公式ドキュメントを読む必要がある。

また、そうでなくても、なるべく一次ソースである公式のドキュメントを読みたい。

関連記事 新人プログラマのうちに身に付けたい習慣、考え方(この半年で学んだことと反省) - Qiita

現在勤めている会社で扱っている技術柄、また会社の先輩から公式のドキュメントを読むことを強く勧められていたり、自分自身が新しいもの好きなため、これまで公式ドキュメントで新しい技術を習得する機会が多々あった。その中で、確立してきた方法を記述したい。

英語に慣れておく

新しい技術だと日本語の公式ドキュメントが存在していないかったり、一部しか日本語化されていないことも多い。その場合、英語で公式ドキュメントを読む必要がある。最初は、英語で読むのはしんどいかもしれないが、辞書を使いながら読んでいけば、いずれ慣れる。

1回2回さらっと読む。

まずは、1回2回さらっと読む。(英語の場合は辞書を使いながらでも)
その際、細かいところは気にしなくても良い。全体を軽く理解するという感じだ。

日本語の周辺情報を探す

冒頭で以下のように記述したが、何かしら日本語で情報が存在していることもある。

何かの技術を新たに学ぶ時、特に最新の技術を学ぶ時や日本でのユーザーが少ない技術の場合、その技術に関する書籍や記事がまだ存在しなかったり、少なかったりすることがある。

日本語の情報が存在する場合、理解の補助として周辺の情報を日本語で読んでおくのは有効な手立てである。

サンプルコードをコピペしてIDEにはる

ドキュメントには、サンプルコードが記述されていることが多いと思う。
そのサンプルコードをコピペする。この際、使用するのはできるだけリッチなIDEが良いと思う。( Javaだったら、IntelliJ IDEA 、Goだったら Gogland など)

コピペしたサンプルコードを解読する

コピペしたサンプルコードを解読していく。
具体的には、1行ずつ何の処理を行っているか、全てコメントでコード内に記述していく。
また、IDEのジャンプ機能を使用して、コード中に使用されているメソッドまでジャンプしてそのメソッドのコード内のドキュメント(Javaだったら、JavaDoc、Goだったら、GoDocなど)を読む。余裕があれば、メソッドのコード自体を読む。

コードを動く形にしてみる

サンプルコードには、コードの一部のみが記述されていて、そのままでは動かないものも多い。そこで、動くように自分で足りない部分を記述してみる。

コードを改造して、自分なりのコードを書いてみる

サンプルコードが動くようになったら、今度は自分で考えて、その技術を使用して、何らかの小さなコードを書いて遊んでみる。

もう1、2回ドキュメント全体を読む

今度は、詳細な部分までしっかり理解しながら読む。すでにコード内のドキュメントを読んだり、処理をコメントにしたりしているので、ドキュメントがすんなり頭の中に入ってくる。
この際に日本語でメモを取っておく。

ブログやQiitaの記事にする。

先ほど、ドキュメントを読んだ際に書いておいたメモや、サンプルコードや自分で書いたコード(詳細な処理のコメントが載っているはずである。)をまとめて、それをブログやQiitaなどの記事としてアウトプットする。
これには、3つのいいことがあると考えている。

  1. アウトプットすることで、理解が明確になる
    => 他人に見られる記事を書くので、曖昧な理解だとボコボコにされる可能性がある。
    よって、ちゃんとした理解をするようになるはず。

  2. 見返しやすい
    => たくさん勉強していると、前に習得した技術の詳細を忘れることがある。その際にあらかじめ自分が記述した分かりやすい記事があれば、それを見返して思い出すのに時間があまりかからないはずだ。

  3. 他人からの評価を得やすい
    => ちょっと打算的な話になるが、ある技術に関してのちゃんとした記事を書いていれば、その技術に関しての知識がある人だと他人から思われるはずだ。例えば社内で、自分が習得した新しい技術を使ったプロジェクトにアサインされたいと思った時に、その記事はPMに対するアピールの材料になるかもしれない。

実際の例

以下に自分が上記の方法を通して記述したQiitaの記事を貼っておく。
! 英語だけのドキュメントもあるが、日本語のドキュメントが混在しているものもある。

GoでBigQueryクライアントを実装してBigQueryからデータを取得する - Qiita

Go言語でSendGrid Web API v3を使って、メールを送信する - Qiita

【自然言語処理】Google Natural Language API Client LibrariesをGoで実装してみる - Qiita

関連記事
新人プログラマのうちに身に付けたい習慣、考え方(この半年で学んだことと反省) - Qiita

難しいと感じる技術本を読む時に自分がやっていること - Qiita

※ Qiitaでも同一の投稿を行っている
公式ドキュメントで新しい技術を習得する方法 - Qiita

【技術記事】Angular4(Angular2~)のユニットテスト【Angularのユニットテストの基本とComponentの簡単なテスト編】

Angular4(Angular2~)のユニットテスト【Angularのユニットテストの基本とComponentの簡単なテスト編】

Angularのユニットテストを勉強中なので、何回かに分けて記事にする。
今回の記事は、以下の公式ドキュメントの4つのセクションをかなり参考にさせていただいている。
Introduction to Angular testing The first karma test Test a component Test a component with an external template

今回は、Angularのテストの大まかな流れと、簡単なComponentのテストを記事の対象とする。

使うテストツール

Angularのテストでは、JavaScript界のいくつかのテストツールを使用する。それらのツールを理解していないと挫折するので、まずは、Angular自体のテストに先立って、それらのツールを学ぶ必要がある。以下を参考にするとわかりやすい。

jasmine

JavaScript 向けテスティングフレームワーク

jasmine公式 https://jasmine.github.io/2.0/introduction.html
jasmineのわかりやすい記事
http://qiita.com/opengl-8080/items/cf3acafda9756f4b04c9 http://qiita.com/hmsk/items/8f6965968692186b1ea1

karma

ブラウザでユニットテストを実行するためのテストランナー

karmaのわかりやすい記事
http://qiita.com/howdy39/items/b9d704e7f84053924da3 karma公式
https://karma-runner.github.io/1.0/index.html

被テストComponent

app.component.ts

import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'テストだよ';

  /**
   * h1の文章を変更する
   */
  changeH1Element() {
    this.title = 'クリックされたぜ!';
  }
}

app.component.html

<h1>
  {{title}}
</h1>

<p class="button" (click)="changeH1Element()">ここをクリックして</p>

タイトルがあって、「ここをクリックして」というところを押すと、タイトルが変更される。
なお、今回の目的はAngularのユニットテストの学習のため、CSSなどによる装飾等は一切行わない。

テストクラス

app.component.spec.ts

import {ComponentFixture, TestBed, async, ComponentFixtureAutoDetect, fakeAsync, tick} from '@angular/core/testing';
import {By}              from '@angular/platform-browser';
import {DebugElement}    from '@angular/core';
import {AppComponent} from './app.component';


// describeでテストSuiteを作成
describe('AppComponentのテスト', () => {
  // テストの中のAppComponent
  let comp: AppComponent;
  // ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。
  let fixture: ComponentFixture<AppComponent>;
  // ComponentのDOM elementのhandle
  let de: DebugElement;
  let el: HTMLElement;


  // 各Spec(個々のテスト)が開始される前に行う処理を設定する。
  // 非同期処理
  // Componentのインスタンスを生成する前に、Angular template compilerが外部ファイルを読み込む
  beforeEach(async(() => {
    // テストしたいクラスのためのモジュール環境をconfigureTestingModuleメソッドで設定する。
    // メタデータの登録
    TestBed.configureTestingModule({
      // テストされるComponentを登録
      declarations: [
        AppComponent
      ],
      providers: [
        {provide: ComponentFixtureAutoDetect, useValue: true}
      ]
    }).compileComponents(); // 外部ファイルをコンパイル

  }));

  // 同期処理
  // Componentのインスタンスを生成
  beforeEach(() => {
    // ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。
    fixture = TestBed.createComponent(AppComponent);
    // テストされるComponentのインスタンス
    comp = fixture.componentInstance; 

    // queryは、fixtureのDOM全体の中から、引数で与えたelementを満たす最初のDom elementとマッチしたDebugElementを返す
    // Byでは、cssのselectorを生成している。
    de = fixture.debugElement.query(By.css('h1'));

    el = de.nativeElement;
  });


  // itでSpecを作成
  it('AppComponentのインスタンスが生成できているかどうか', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    // trueかどうか
    expect(app).toBeTruthy();
  }));
  it('何もしない場合のタイトルがAppComponentのtitleと同じかどうか', async(() => {
    expect(el.textContent).toContain(comp.title);
  }));
  it('何もしない場合のタイトルがAppComponentのtitleと同じかどうか(上のテストと同じことをしている)', async(() => {
    expect(el.textContent).toContain('テストだよ');
  }));

  it('detectChangesが1回起きた時、h1の値が変更されるかどうか', async(() => {
    comp.title = 'クリックされたぜ!';
    fixture.detectChanges();
    expect(el.textContent).toContain(comp.title);
  }));

  it('changeH1Elementが呼び出されたら、titleが変更される', async(()=> {
    comp.changeH1Element();
    expect(comp.title).toBe('クリックされたぜ!');
  }));
});

各々の処理で行っていることは、コード中にコメントとして記述してある。

Angularのテストの大まかな構成

基本的には、jasmineの構成に則っているようである。
1. beforeEachで、各テストSpecの処理の前に行う処理を記述する。(場合によってはafterEachで、各テストSpecの処理の後に行う処理を記述する。)
2. descibeでSuite(テストの塊のようなもの)を作成し、その中でitでSpec(個々のテストメソッド)を複数記述する。このitの中で、様々なテストを行う。

Angularのテストで使用するクラスとメソッド

TestBed

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html

Configures and initializes environment for unit testing and provides methods for creating components and services in unit tests. 引用元:https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html

(意訳) ユニットテスト用の設定と環境の初期化を行う。
また、ユニットテスト内のcomponentとserviceを生成するためのメソッドを作成する。

It creates an Angular testing module—an @NgModule class—that you configure with the configureTestingModule method to produce the module environment for the class you want to test. 引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23q-spec-file-location

(意訳) TestBedは、Angularのテスティングモジュール(@NgModuleクラス)を生成する。
@NgModuleクラスはテストしたいクラスのためのモジュール環境をconfigureTestingModuleメソッドで設定する。

=> テストの環境初期化と設定を行う。

TestBed.configureTestingModule

テストしたいクラスのためのモジュール環境を設定する。
メタデータの登録を行う。@NgModuleのテスト版のようなもの

beforeEach (jasmineのメソッド)

各Spec(個々のテスト)が開始される前に行う処理を設定する。

TestBed.createComponentとComponentFixture

TestBed.createComponent 公式APIリファレンス
https://angular.io/docs/js/latest/api/core/testing/index/TestBed-class.html
ComponentFixture 公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html

The createComponent method returns a ComponentFixture, a handle on the test environment surrounding the created component. The fixture provides access to the component instance itself and to the DebugElement, which is a handle on the component's DOM element.
引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

(意訳) createComponent(TestBed.createComponent)は、生成されたcomponentのテスト環境のハンドルであるComponentFixtureクラスを返す。ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。

=> ComponentFixture = テストしたいComponentのインスタンスとそのComponentのDebugElementへのアクセスを提供するもの。
しつこいようだが、これによってテスト対象のComponentのインスタンスとDOM elementを操作できるようになる。

TestBed.createComponentは、TestBedインスタンスの設定を閉じるので、createComponentを使用した後にTestBedの設定系のメソッドを使用してはいけないようである。

DebugElement.query

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/index/DebugElement-class.html

queryは、fixtureのDOM全体の中から、引数で与えたelementを満たす最初のDom elementとマッチしたDebugElementを返す。
fixture.debugElement.queryのDebugElementと戻り値のDebugElementは異なる。

queryAllの場合は、引数で与えたelementを満たす全てのDom elementの配列を返す。

参考 https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture
https://angular.io/docs/js/latest/api/core/index/DebugElement-class.html#!%23query-anchor

By

AngularのUtilityで、 predicates(述語と単純に訳していいのか不明)を生成する。

公式APIリファレンス https://angular.io/docs/ts/latest/api/platform-browser/index/By-class.html

detectChanges

componentのchange detection cycleを行う。

公式APIリファレンス https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html#!%23detectChanges-anchor detectChangesについては、以下がわかりやすい。
http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

以下の記事の言葉をお借りすれば、

change Detectionとはモデルの変更を検知し、UIに反映することである

とのこと
引用元 http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

fixture.detectChanges()を呼ぶことで、テストからAngilarにchange detection が行われたことを伝える。

Angularで外部のtemplateを読み込む

外部のtemplateやcssファイルを使用するときは、ちょっとした処理が必要なようだ。

But the Angular template compiler must read the external files from the file system before it can create a component instance. That's an asynchronous activity.

引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

(意訳)
Angularでは、Componentのインスタンスを生成する前に、Angular template compilerが外部ファイルを読み込む必要があり、これは非同期な活動である。

=>
つまり、Componentのインスタンスを生成する前にAngular template compilerが外部ファイルを読み込めるような処理をbeforeEachの中で行う必要なあるわけである。

TestBed.compileComponents

公式APIリファレンス

まず、テスティングモジュールで設定されたComponentを非同期でコンパイルする。
上記のcompileComponentsが完了したら、外部のファイルや、cssファイルはinlinedされていて、
TestBed.createComponentは、同期的にComponentのインスタンスを生成することができる。

参考: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

templateやcssを外部ファイルにするときは、必ず「Componentのインスタンスを生成する」前に「外部ファイルを読み込む」ように非同期、同期をうまく使う。(コードの記述はこの順番でなくても構わない)

テスト実行画面

以下のコマンドをコマンドライン上で打つとテストランナーであるkarmaによってブラウザが立ち上がり、テスト結果がブラウザ上に表示される。

ng test

ng-karma.png

参考にさせていただいたサイト

公式 https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

jasmine公式
https://jasmine.github.io/2.0/introduction.html jasmineわかりやすい記事
http://qiita.com/opengl-8080/items/cf3acafda9756f4b04c9 http://qiita.com/hmsk/items/8f6965968692186b1ea1

karmaのわかりやすい記事
http://qiita.com/howdy39/items/b9d704e7f84053924da3

karma公式
https://karma-runner.github.io/1.0/index.html

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html https://angular.io/docs/js/latest/api/core/testing/index/TestBed-class.html https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html https://angular.io/docs/ts/latest/api/core/index/DebugElement-class.html https://angular.io/docs/ts/latest/api/platform-browser/index/By-class.html https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html#!%23detectChanges-anchor

detectChangesについて
http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

※ Qiitaでも同一記事を投稿している。

qiita.com

【技術記事】Go言語で今月から標準入力で受けたyyyy-mmまでの各月の月初と月末を一覧表示する簡単なコマンドラインアプリを作ってみた

Go言語で今月から標準入力で受けたyyyy-mmまでの各月の月初と月末を一覧表示する簡単なコマンドラインアプリを作ってみた

Goで標準入力と time packageを用いて、日付に関する簡単なコマンドラインアプリを作ってみた。
月初や月末を求めるようなことは日々の業務でも割と出くわすのではないだろうか。

「ここんところこうした方が良いだろ」というアドバイスがあれば、コメントをいただけると幸いです。

流れ

main

  1. 標準入力を受け取る
  2. 標準入力で受け取った値が不適切だったら、再度入力させる処理を行う
  3. 標準入力から受け取った文字列をyyyyとmmに分割
  4. 標準入力から受け取った文字列をyyyyとmmを引数にShowSpecificTermListを呼び出し

ShowSpecificTermList

  1. 現在の時刻を取得
  2. 現在の時刻を元に基準となるDateを作成
  3. 標準入力から受け取った文字列をyyyyとmmを元に終了の条件となるDateを設定
  4. for文で月初は、基準となるDateからループごとに-1月する
  5. for文で月末は、基準となるDateからループごとに-1月&&-1日する
  6. 終了条件の日付以前の日にちまで遡ったら、breakしてループを抜ける

月末で、基準日(日付が1日)から1日を引いていることがポイント

実装してみた

package main

import (
    "fmt"
    "log"
    "regexp"
    "strconv"
    "strings"
    "time"
)

func main() {
    // 遡る日付をyyyy-mmで入力を受け付ける
    fmt.Println("いつまで遡りますか?\nyyyy-mmで入力してください。(現在と同じかそれよりも前を指定してください。)")

    var input string

    var limitYear int

    var limitMonth int

    // 適切な入力がなされない限りループ
    for {
        // 標準入力をスキャン
        fmt.Scan(&input)

        // 正規表現のチェック
        if b, err := regexp.MatchString(`^(\d{4})-(0[1-9]|1[0-2])$`, input); !b || err != nil {
            fmt.Println("入力いただいた文字列が不適切です。 yyyy-mmで入力してください。")
        } else {
            // 入力された文字列を"-"で分割
            s := strings.Split(input, "-")
            // 遡る西暦の限界値
            limitYear, err = strconv.Atoi(s[0])
            if err != nil {
                log.Printf("can not strconv.Atoi :%v \n", err)
            }

            // 遡る月の限界値
            limitMonth, err = strconv.Atoi(s[1])
            if err != nil {
                log.Printf("can not strconv.Atoi :%v \n", err)
            }

            // 入力で受け付けた日付をDateに変換
            inputDate := time.Date(limitYear, time.Month(limitMonth), 0, 0, 0, 0, 0, time.UTC)

            // 現在よりも未来の日付にならないようにする
            if !inputDate.After(time.Now()) {
                // 適切な値が入力されたら、ループを抜ける
                break
            }

            fmt.Println("現在と同じかそれよりも前を指定してください。")
        }
    }

    ShowSpecificTermList(limitYear, limitMonth)
}

// 現在の時刻から指定した日付まで遡った月初と月末の日付の一覧を表示する
func ShowSpecificTermList(limitYear, limitMonth int) {
    // 現在の時刻を取得
    now := time.Now()
    // 基準となる日付を設定
    criterionDate := time.Date(now.Year(), now.Month(), 1, 12, 0, 0, 0, time.UTC)

    // 終了の条件となる日付を設定
    finDate := time.Date(limitYear, time.Month(limitMonth), 30, 15, 0, 0, 0, time.UTC)

    i := 0
    for {
        // 月初を設定
        beginningOfTheMonth := criterionDate.AddDate(0, -i, 0)
        // 月末を設定
        endOfTheMonth := criterionDate.AddDate(0, -i+1, -1)

        fmt.Printf("月初:%v", beginningOfTheMonth)
        fmt.Printf("月末:%v\n", endOfTheMonth)

        // 終了条件の日付以前の日にちまで遡ったら、breakしてループを抜ける
        if beginningOfTheMonth.Before(finDate) {
            fmt.Println("終了")
            break
        }

        // インクリメント
        i++
    }
}

実行結果

いつまで遡りますか?
yyyy-mmで入力してください。(現在と同じかそれよりも前を指定してください。)
2020-03 // <= 入力
現在と同じかそれよりも前を指定してください
2016-01 // <= 入力
月初:2017-06-01 12:00:00 +0000 UTC月末:2017-06-30 12:00:00 +0000 UTC
月初:2017-05-01 12:00:00 +0000 UTC月末:2017-05-31 12:00:00 +0000 UTC
月初:2017-04-01 12:00:00 +0000 UTC月末:2017-04-30 12:00:00 +0000 UTC
月初:2017-03-01 12:00:00 +0000 UTC月末:2017-03-31 12:00:00 +0000 UTC
月初:2017-02-01 12:00:00 +0000 UTC月末:2017-02-28 12:00:00 +0000 UTC
月初:2017-01-01 12:00:00 +0000 UTC月末:2017-01-31 12:00:00 +0000 UTC
月初:2016-12-01 12:00:00 +0000 UTC月末:2016-12-31 12:00:00 +0000 UTC
月初:2016-11-01 12:00:00 +0000 UTC月末:2016-11-30 12:00:00 +0000 UTC
月初:2016-10-01 12:00:00 +0000 UTC月末:2016-10-31 12:00:00 +0000 UTC
月初:2016-09-01 12:00:00 +0000 UTC月末:2016-09-30 12:00:00 +0000 UTC
月初:2016-08-01 12:00:00 +0000 UTC月末:2016-08-31 12:00:00 +0000 UTC
月初:2016-07-01 12:00:00 +0000 UTC月末:2016-07-31 12:00:00 +0000 UTC
月初:2016-06-01 12:00:00 +0000 UTC月末:2016-06-30 12:00:00 +0000 UTC
月初:2016-05-01 12:00:00 +0000 UTC月末:2016-05-31 12:00:00 +0000 UTC
月初:2016-04-01 12:00:00 +0000 UTC月末:2016-04-30 12:00:00 +0000 UTC
月初:2016-03-01 12:00:00 +0000 UTC月末:2016-03-31 12:00:00 +0000 UTC
月初:2016-02-01 12:00:00 +0000 UTC月末:2016-02-29 12:00:00 +0000 UTC
月初:2016-01-01 12:00:00 +0000 UTC月末:2016-01-31 12:00:00 +0000 UTC
終了

time.AddDate(years int, months int, days int) Timeの使い方

time.AddDateでは、引数にyearsを与えれば「年」が、monthを与えれば「月」が、daysを与えれば「日」が加算されたTimeを返してくれる。
今回は、これをちょっと応用して、monthやdaysの引数にマイナスの値を与えることで、過去のTimeを作成している。

 参考にさせていただいたサイト

time package
regexp package qiita.com

※Qiitaにも同一の投稿を行っている qiita.com

【技術記事】SendGridでメールを送信する時に改行が上手くいかない時の対処法

SendGridでメールを送信する時に改行が上手くいかない時の対処法

SendGridを介してメールを送信する時に、Contentをplain/textにしていると意図した改行がなされないとことがある。その時の対処法を記す。
対処法と言っても、非常に簡単な設定を行うだけである。

生じる問題

例えば、以下のような改行コードを入れた文字列の場合に生じる(SendGridのAPIを叩くためのJSONの一部を抜粋)

Content: []Contents{{
       Type:  "text/plain”,       
       Value: "アイウエオ\nカキクケコ\nサシスセソ",}},

この場合、メールで送信されてくる中身は、以下のようなものを期待している

expected_mail_screen.png

しかし、デフォルトの設定のままだと以下のような内容が送信されてくる

actual_mail_screen.png

問題が生じる原因

SendGridの Plain Content: Convert your plain text emails to HTML .という設定が、OFFになっているために生じる。(Settings > Mail Settings > Plain ContentのACTIVE)
これは、その設定の説明の通り、plain text のメールをSendGridがHTMLに変換してくれるというものである。
これがあるために、改行コードが無視されたHTML形式に変換されメールが送信されるので、先ほどのような問題が生じる。

plain_text_off.png

解決策

Plain Content: Convert your plain text emails to HTML (Settings > Mail Settings > Plain ContentのACTIVE)の設定を「ON」にすれば良い。
若干ややこしいが、「OFF」にしていると、plain text のメールをSendGridがHTMLに自動で変換されるようになってしまう。

plain_text_on.png

関連記事

Go言語でSendGrid Web API v3を使って、メールを送信する

GCE上のGoのコードからSendGridを使用してメールを送信

  • Qiitaでも同一の投稿をしている

qiita.com