Jenkinsプラグイン開発ノウハウ2 - 開発とテスト

## Jenkinsプラグイン開発ノウハウシリーズ
1. [Jenkinsプラグイン開発ノウハウ1 - 環境構築](http://qiita.com/sona-tar/items/e6d7ebb5470236a0cc50)
2. [Jenkinsプラグイン開発ノウハウ2 - 開発とテスト](http://qiita.com/sona-tar/items/933e55594bfed757fe8d)
3. [Jenkinsプラグイン開発ノウハウ3 - リリース](http://qiita.com/sona-tar/items/45eae75eb5f91f4ebf55)


## はじめに
[Jenkinsプラグイン開発ノウハウ1 - 環境構築]()で開発環境は整ったので実際に開発する際のポイントをサンプルコードをもとに紹介したいと思います。

## 1. サンプルコード

まずサンプルプラグインの`JobCreateBuilder.java`の仕様を紹介します。

JobCreateBuilderは実行時に渡された設定された文字列のジョブを作成します。またJenkinsの設定でPrefixを設定しておくと、ジョブ名の前にプレフィックスを追加してジョブを作成します。
例えばJenkinsの設定のPrefixに`pre-`、JobCreateBuilderのジョブには`job01`と設定してジョブを実行すると`pre-job01`というジョブが作成されます。またすでに存在するジョブを作成しようとすると失敗します。

### 1.1. ジョブ開始時に起動されるメソッド

次に`JobCreateBuilder`のメインシーケンスを表すメソッドのソースコードを紹介します。

```java:src/main/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder.java
    @Override
    public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener)
            throws InterruptedException, IOException {
        // 実行時に指定された新規に作成するジョブ名を取得
        String newJobName = getCreateJobNameFromParam(build.getEnvironment(listener));
        // ジョブ名が空だったらエラー
        if (newJobName.isEmpty()) {
            listener.getLogger().println("Error : " + Messages.JobCreateBuilder_JobName_Empty());
            return false;
        }
        try {
            // PrefixをJenkinsの設定から取得して、先ほど取得したジョブ名と連結してジョブを作成
            Jenkins.getInstance().createProject(FreeStyleProject.class, getDescriptor().getPrefix() + newJobName);
        }catch (IllegalArgumentException e) {
            // 既に存在するジョブ名だったら場合はジョブの実行を失敗とする
            listener.getLogger().println("Error : " + e.getMessage());
            return false;
        }
        // ジョブの作成が成功したことをJenkinsのコンソールにログとして出力
        listener.getLogger().println(Messages.JobCreateBuilder_Success() + " name : " + newJobName);
        // ジョブの実行が成功
        return true;
    }
```

上記のソースコードをを読むとなんとなく実施している処理のイメージがつくと思いますが、簡単にJenkinsAPIのルールと共に説明します。

0. ジョブ開始時に起動されるメソッドは`perform`メソッドになります。
1. performメソッドは`true`を返すとジョブの実行が成功、`false`を返すと失敗となる。
2. `Jenkins.getInstance()`からJenkisのインスタンスを取得することで多くの操作が可能となる。
今回はプロジェクトを作成するための`createProject`メソッドを実行しています。
詳しくは[Jenkins公式ドキュメント](http://javadoc.jenkins-ci.org/)のJenkinsクラスを確認してみてください。
3. `listener.getLogger().println()`でJenkinsの実行時のコンソールログとして出力可能。


続いて設定や画面に関する処理について、ソースコード、設定ファイル、JenkinsのUIを確認しながら説明していきたいと思います。

### 1.2. すべてのジョブに影響するパラメータ
`global.jelly`を作成しておくと、すべてのジョブに影響するパラメータを用意することが可能になります。

今回で言うとPrefixです。

- 設定画面  
JenkinsのUIでは**Jenkinsの設定 -> Jenkinsの管理 -> JobCreate-Builder**にあります。
![prefix01.png](https://qiita-image-store.s3.amazonaws.com/0/54186/8fdd2613-e40e-87f0-aeca-04380360d236.png)

- 設定ファイル

```java:src/main/resources/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder/global.jelly
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:section title="JobCreate Builder">
    <f:entry title="${%Prefix}" field="prefix" description="Prefix of job name created by JobCreate-Builder" default="">
       <f:textbox default="" />
    </f:entry>
  </f:section>
</j:jelly>
```


今回重要なのは、`<f:entry`で始まる行になります。`title="${%Prefix}"`上記のJnekinsの管理で表示される文字列を示します。ここではPrefixなのにUI上では**接頭辞**と表示されています。これは日本語Locale対応をしているためです。英語Localeの環境ではPrefixと表示されます。

日本語Localeに対応するためには`global_ja.properties`を用意します。

- 日本語Locale設定ファイル

```java:src/main/resources/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder/global_ja.properties
# Prefix=接頭辞
Prefix=\u63a5\u982d\u8f9e
```

Unicodeで記述する必要があるので、[Unicode Escape Sequence 変換ツール](http://www.kwonline.org/u_esc_seq.php)のようなサイトでUnicodeの文字列を用意しましょう。



`global.jelly`の`<f:entry`の話に戻りますが、この中でも重要なのが`field=prefix`です。
これは以下の`getPrefix`メソッドと対応しています。
そのため`perform`メソッドなどから`getPrefix`を実行することでJenkinsの設定の値を取得することが可能になります。

```java:src/main/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder.java
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {

        private String jobNamePrefix;

        public String getPrefix() {
            return jobNamePrefix;
        }

        public boolean configure(StaplerRequest req, JSONObject formData)throws FormException {
            jobNamePrefix = formData.getString("prefix");
            save();
            return super.configure(req, formData);
        }
```

`description`や`default`は想像通りだと思いますので説明は省略します。


### 1.3. ビルド手順の追加を選択した時に表示される文字列
ビルド手順の追加を選択した時に表示される文字列ですが、ソースコードの`DescriptorImpl`クラスの`getDisplayName`メソッドと対応しています。

JekinsのUIでは**先ほど作成したcreate-job -> ジョブの設定 -> ビルド手順の追加 -> ジョブの作成**になります。

![jobcreate01.png](https://qiita-image-store.s3.amazonaws.com/0/54186/f37e8376-cce8-6c05-8aec-c2b82ee92e84.png)

```java:src/main/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder.java
        public String getDisplayName() {
            return Messages.JobCreateBuilder_DisplayName();
        }
```

`Messages`クラスに見覚えがないかと思いますが、これは`Messages.properties`, `Messages_ja.properties`から自動生成されるクラスになります。
日本語Localeの環境では、`Messages_ja.properties`の値が返り、それ以外の環境では`Messages.properties`の値が返ります。

```java:target/generated-sources/localizer/org/jenkinsci/plugins/jobcreatebuilder/Messages.java
public class Messages {
    /**
     * Set a job name.
     * 
     */
    public static String JobCreateBuilder_JobName_Empty() {
        return holder.format("JobCreateBuilder.JobName.Empty");
    }
    省略
```

### 1.4. ジョブ内部のパラメータ
`config.jelly`、`config_ja.properties`を作成すると、**ジョブ内部のパラメータ**を作成することが可能です。

JenkinsのUIでは**先ほど作成したcreate-job -> ジョブの設定 -> ジョブを作成 -> ジョブ名**になります。

![jobName01.png](https://qiita-image-store.s3.amazonaws.com/0/54186/b05a58a9-c1aa-0e3d-4aa5-86fa7bfe4506.png)

```java:src/main/resources/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder/config.jelly
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="${%JobName}" field="jobName">
    <f:textbox />
  </f:entry>
</j:jelly>
```

`global.jelly`とソースコードの対応の考え方は先ほどとほとんど同じですが、今回は`DescriptorImpl`クラスではなく`JobCreateBuilder`のメソッドが対応します。

```java:src/main/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder.java
public class JobCreateBuilder extends Builder {

    private final String jobName;

    @DataBoundConstructor
    public JobCreateBuilder(String jobName) {
        this.jobName = jobName;
    }

    public String getJobName() {
        return jobName;
    }
```

### 1.5 ジョブ内部のパラメータの入力フォームのバリデーション

Jenkinsでは以下のように入力フォームの状態が不正な場合に画面に警告を表示することが可能です。

- 画面
![validation01.png](https://qiita-image-store.s3.amazonaws.com/0/54186/e38068d8-69bf-f293-b994-8fc882d0f4dc.png)

- ソースコード
今回は入力フォームの文字列が0だった場合にエラーを出力しています。

```java:src/main/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilder.java
        public FormValidation doCheckJobName(@QueryParameter String value) {
            if (value.length() == 0)
                return FormValidation.error(Messages.JobCreateBuilder_JobName_Empty());
            return FormValidation.ok();
        }
```
### 1.6. ジョブ内部のパラメータのヘルプ
`help-jobname.html`, `help-jobname_ja.html`を用意しておくと、以下のようにフォームに対応したヘルプを表示できるようになります。

![jobnamehelp01.png](https://qiita-image-store.s3.amazonaws.com/0/54186/6a7fcf52-d86e-26af-3f9e-d2507c9740b8.png)





## 2. テスト
### 2.1. JUnit4でユニットテストの基本

以下のように宣言するとテスト用のJenkinsのインスタンスが生成され、各種JenkinsのAPIが利用できるようになります。
これを実行せずに各種APIを実行しても`NullPointerException`となるので気をつけてください。

```java:src/test/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilderTest.java
public class JobDeleteBuilderTest {
    @Rule
    public JenkinsRule j = new JenkinsRule();

    @Test
    public void testPerform() {
　　　　　　// test code略
    }
    @Test
    public void testDescriptorDoCheckJobName() {
　　　　　　// test code略
    }
```


またJUnit4の仕様になりますが`@Test`アノテーションをつけたメソッドがテストケースとして扱われます。

今回作成した2つのテスト関数ですが、`testPerform`メソッドは実際にジョブを実行するテストを実施し、`testDescriptorDoCheckJobName`ではジョブ名を入力した際のバリデーションのテストを実施しています。


またテストコードの場合には、UI上からジョブの作成やビルドの実行をするわけにはいかないため、APIを実行してジョブの作成などをする必要があります。


### 2.2 サンプルコードのテストのメインシーケンス

続いて`testPerform()`のメインシーケンスをコメントで簡単に説明します。


```java:src/test/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilderTest.java
``
   @Test
    public void testPerform() throws Exception {
        // create a job. Job name is pre-job01
        {
            // Jenkinsの設定
            String newJobNamePrefix = "pre-";
            // ジョブお設定
            String newJobName = "job01";

            // JobCreateBuilderのジョブを作成
            FreeStyleProject createBuilderProject = createJobCreateBuilderProject("create-builder-project01", newJobNamePrefix, newJobName);

            // テスト実行
            // ジョブの実行
            Build build = createBuilderProject.scheduleBuild2(0).get();
            // ジョブの実行結果を取得
            Result result = getResult(build);

            // テスト結果を確認
            // ジョブの実行に成功したかどうか
            assertThat("Check pre-job01 is success", result, is(Result.SUCCESS));

            // 作成したジョブが想定したとおりのものになっているかどうか

            FreeStyleProject newJob = (FreeStyleProject) jenkinsRule.getInstance().getItem(newJobNamePrefix + newJobName);
            assertThat("Check pre-job01 is created", newJob.getDisplayName(), is(newJobNamePrefix + newJobName));

            // 終了処理 作成したジョブを全て削除する。
            createBuilderProject.delete();
            newJob.delete();
        }
    }
```



### 2.3 ジョブの作成
以下は、JobCreateBuilderのジョブを作成するメソッドです。

```java:src/test/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilderTest.java
    private FreeStyleProject createJobCreateBuilderProject(String builderName, String jobPrefix, String jobName) throws IOException {
        // 1. FreeStyleProjectを作成
        FreeStyleProject project = jenkinsRule.createFreeStyleProject(builderName);
        // 2. JobCreateBuilderを作成
        JobCreateBuilder job = new JobCreateBuilder(jobName);
        // 3. Jenkinsの設定を実施
        job.getDescriptor().setPrefix(jobPrefix);
        // 1.で作成したFreeStyleProjectに、2のJobCreateBuilderを登録
        project.getBuildersList().add(job);
}
```


### 2.4. 実行したジョブの結果を取得

実行したジョブの結果を取得する際には注意するポイントが存在します。

この`scheduleBuild2`メソッドは、名前のとおりジョブを実行スケジューラに登録するだけなので、このメソッドがreturnされた時点では、まだジョブの実行が終わってない可能性があります。

そのため直接`build.getResult()`を実行してしまうと実行が終わっている保証がないため、`build.isBuilding()`で実行が終わっていることを定期的にチェックしてから結果を取得します。

今回はジョブの実行が終わった後に実行結果を返す`getResult`メソッドを作成しました。

```java:src/test/java/org/jenkinsci/plugins/jobcreatebuilder/JobCreateBuilderTest.java
    private Result getResult(Build build) throws InterruptedException {
        while(build.isBuilding()) {
            Thread.sleep(10);
        }
        return build.getResult();
    }
```



## 参考
今回はBuildを拡張するプラグインを紹介していますが、Jenkinsはその他の部分もプラグインとして開発可能になっています。[Extension Points - Jenkins Wiki](https://wiki.jenkins-ci.org/display/JENKINS/Extension+points)を確認してみてください。

また公式ドキュメントを読み進めても難しいので、最初は目標とするプラグインを[Jenkins Wiki - Pluings](https://wiki.jenkins-ci.org/display/JENKINS/Plugins)や[Githubのjenkisciグループ](https://github.com/jenkinsci/)から見つけて参考にしてみましょう。
特に[Jenkins Wiki - Pluings](https://wiki.jenkins-ci.org/display/JENKINS/Plugins)はカテゴリごとに分類されているため、自分が作ろうとしているプラグインと似ているものを探しやすいと思います。


あとは自分なりに変更が必要なところは[Jenkins公式ドキュメント](http://javadoc.jenkins-ci.org/)を確認してみてください。
また過去にリリースされたプラグインを参考にしているとdeprecatedになっているAPIを使っているものもあるので、余裕があれば適宜利用しているAPIを公式ドキュメントで確認して最新のAPIに置き換えてみましょう。

## まとめ

以上で、開発やテストが完了しました。

続きは[Jenkinsプラグイン開発ノウハウ3 - リリース](http://qiita.com/sona-tar/items/45eae75eb5f91f4ebf55)になります。