自作の自動テストツールをJenkinsで実行する

##1. はじめに
こちらは[ソフトウェアテストの小ネタ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/software-testing-koneta)の3日目の記事です。

[システムテスト自動化 標準ガイド](https://www.shoeisha.co.jp/book/detail/9784798139210)で自動化すべきでないテストの一つに「物理的なやりとりがあるテスト。例えば、カードリーダーにカードを通す、何かの装置の接続を外す、電源をオフかオンにするなど」が挙げられていますがホントはこの手のテストも自動化したいですよね。

[Lチカで始めるテスト自動化](https://techbookfest.org/product/5661312486998016)は初めて作る自動テストシステムを目標としながらも、このようなテストの自動化を目指してそこそこ機能が充実してきたことや[連載1回目](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)で[Jenkins](https://www.jenkins.io/)での実行にちょっとだけ触れたものの詳細は割愛していたため、Jenkinsでの実行を記事にまとめます。

##2. 目標
Jenkinsのコントローラ(Windows10)とエージェント(Raspberry Pi)でパイプラインを組んで以下のプロジェクトを実行し[^1]、Pass/Fail判定を行います。
本稿では所定の文字列をファイルへ出力することでテストを実行したとみなしJenkinsでの実行にフォーカスします。

1. コントローラ
  - テストランナー([test-runner.py](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d#%E4%BB%98%E9%8C%B2a-test-runnerpy%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9))[^2]とテストスクリプト(CSVファイル)をエージェントへ転送する
2. エージェント
  - テストランナーのrunコマンド[^3]でテストを実行し、所定の文字列をファイルへ出力する
3. コントローラ
  - エージェントからファイルを取得する
  - テストランナーのrunコマンドでファイルに記入されている文字列をテストランナーに読み込む
  - テストランナーのeval_str_eqコマンドで期待値の文字列と比較する

##3. 環境
- Python
  - コントローラ(Windows10 PC & Miniconda3)
      - C:\Users\\****\Miniconda3\python.exe
      - Python 3.6.9
  - エージェント(Raspberry Pi 3B+)
      - Raspbian GNU/Linux 10 (buster)
      - Python 3.7.3
- Jenkins 2.249.3[^4]

##4. 作るもの
1. テスト実行を模したテストスクリプト(pass版)
2. テスト実行を模したテストスクリプト(fail版)
3. 実行結果と期待値を比較するテストスクリプト

###4.1 テスト実行を模したテストスクリプト(pass版)
echoコマンドで文字列passをtest.txtへ出力します。

```sh:execute-pass.csv
run,echo pass>test.txt
```

###4.2 テスト実行を模したテストスクリプト(fail版)
echoコマンドで文字列failをtest.txtへ出力します。

```sh:execute-fail.csv
run,echo fail>test.txt
```

###4.3 実行結果と期待値を比較するテストスクリプト
コントローラ(Windows10)で実行します。typeコマンドで標準出力へ出力したテキストファイルの内容と期待値の文字列passを、改行コードを無視して比較します。

```sh:evaluation.csv
run,type test.txt
eval_str_eq,pass
```

##5. Jenkinsのプロジェクト
パイプラインのプロジェクトを作成します。アイテム名はtest-runner-sampleとしました。

###5.1 passする版

```groovy:PipelineScript
node('master'){
    stash name: 'test'
}
node('RPi00'){
    deleteDir()
    unstash 'test'
    sh 'python3 test-runner.py execute-pass' // <- pass version
    //sh 'python3 test-runner.py evaluation'
    stash name: 'result'
}
node('master'){
    unstash 'result'
    bat 'C:/Users/****/Miniconda3/python.exe test-runner.py evaluation'
}
```

```text:コンソール出力
ユーザー****が実行
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\test-runner-sample
[Pipeline] {
[Pipeline] stash
Stashed 4 file(s)
[Pipeline] }
[Pipeline] // node
[Pipeline] node
Running on RPi00 in /home/pi/jenkins/workspace/test-runner-sample
[Pipeline] {
[Pipeline] deleteDir
[Pipeline] unstash
[Pipeline] sh
+ python3 test-runner.py execute-pass
['run', 'echo pass>test.txt']
CompletedProcess(args='echo pass>test.txt', returncode=0, stdout='', stderr='')
['2020/12/02 22:51:36', 'run', 'echo pass>test.txt', 'OK']
PASS
[Pipeline] stash
Stashed 6 file(s)
[Pipeline] }
[Pipeline] // node
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\test-runner-sample
Terminated
[Pipeline] {
[Pipeline] unstash
[Pipeline] bat

C:\jenkins\workspace\test-runner-sample>C:/Users/****/Miniconda3/python.exe test-runner.py evaluation 
C:\Users\****\Miniconda3\lib\site-packages\numpy\__init__.py:140: UserWarning: mkl-service package failed to import, therefore Intel(R) MKL initialization ensuring its correct out-of-the box operation under condition when Gnu OpenMP had already been loaded by Python process is not assured. Please install mkl-service package, see http://github.com/IntelPython/mkl-service
  from . import _distributor_init
['run', 'type test.txt']
CompletedProcess(args='type test.txt', returncode=0, stdout='pass\n', stderr='')
['2020/12/02 22:51:38', 'run', 'type test.txt', 'OK']
['eval_str_eq', 'pass']
['2020/12/02 22:51:38', 'eval_str_eq', 'pass', 'OK']
PASS
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```

###5.2 failする版
fail版を実行して実際にfailすることを確認します。

```groovy:PipelineScript
node('master'){
    stash name: 'test'
}
node('RPi00'){
    deleteDir()
    unstash 'test'
    sh 'python3 test-runner.py execute-fail' // <- fail version
    //sh 'python3 test-runner.py evaluation'
    stash name: 'result'
}
node('master'){
    unstash 'result'
    bat 'C:/Users/****/Miniconda3/python.exe test-runner.py evaluation'
}
```

```text:コンソール出力
ユーザー****が実行
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\test-runner-sample
[Pipeline] {
[Pipeline] stash
Stashed 4 file(s)
[Pipeline] }
[Pipeline] // node
[Pipeline] node
Running on RPi00 in /home/pi/jenkins/workspace/test-runner-sample
[Pipeline] {
[Pipeline] deleteDir
[Pipeline] unstash
[Pipeline] sh
+ python3 test-runner.py execute-fail
['run', 'echo fail>test.txt']
CompletedProcess(args='echo fail>test.txt', returncode=0, stdout='', stderr='')
['2020/12/02 22:53:29', 'run', 'echo fail>test.txt', 'OK']
PASS
[Pipeline] stash
Stashed 6 file(s)
[Pipeline] }
[Pipeline] // node
Terminated
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\test-runner-sample
[Pipeline] {
[Pipeline] unstash
[Pipeline] bat

C:\jenkins\workspace\test-runner-sample>C:/Users/****/Miniconda3/python.exe test-runner.py evaluation 
C:\Users\****\Miniconda3\lib\site-packages\numpy\__init__.py:140: UserWarning: mkl-service package failed to import, therefore Intel(R) MKL initialization ensuring its correct out-of-the box operation under condition when Gnu OpenMP had already been loaded by Python process is not assured. Please install mkl-service package, see http://github.com/IntelPython/mkl-service
  from . import _distributor_init
['run', 'type test.txt']
CompletedProcess(args='type test.txt', returncode=0, stdout='fail\n', stderr='')
['2020/12/02 22:53:30', 'run', 'type test.txt', 'OK']
['eval_str_eq', 'pass']
['2020/12/02 22:53:30', 'eval_str_eq', 'pass', 'NG']
FAIL
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: script returned exit code 1
Finished: FAILURE
```

##6. おわりに
- 自作の自動テストツールをJenkinsで実行しPass/Fail判定ができました。
- 以下の理由によりPythonはテストツールを作りやすいと思いました。
  - テストに有用なライブラリがいろいろある
  - WindowsやLinuxなど複数のプラットホーム、アーキテクチャで共用しやすい
  - Jenkinsと組合わせて自動テストシステムを組むといった応用を利かせやすい
- この記事が品質技術開発を始めるきっかけとなりましたら幸いです。

##付録A. Lチカで始めるテスト自動化・記事一覧
1. [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)
3. [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
4. [Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)](https://qiita.com/pbjpkas/items/a02c4128baf5a3f2a46c)
5. [Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み](https://qiita.com/pbjpkas/items/9af9e6441e1e20c5909e)
6. [Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定](https://qiita.com/pbjpkas/items/405d55f809da1221b867)
7. [Lチカで始めるテスト自動化(7)タイムスタンプの保存](https://qiita.com/pbjpkas/items/8e3c4330f4a81d30bd53)
8. [Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635)
9. [Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み](https://qiita.com/pbjpkas/items/7a4cb4c59886b1fd2cf9)
10. [Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上](https://qiita.com/pbjpkas/items/c011d7ad6f98bc1458d5)
11. [Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作](https://qiita.com/pbjpkas/items/fa6d2956ea3dd18b3584)
12. [Lチカで始めるテスト自動化(12)書籍化の作業メモ](https://qiita.com/pbjpkas/items/e76a763de7f496d5430c)
13. [Lチカで始めるテスト自動化(13)外部プログラムの呼出し](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d)

- 1～11と13の記事を電子書籍化し技術書典で[頒布](https://techbookfest.org/product/5661312486998016)しています。

##付録B. ソフトウェアテストのアドベントカレンダー参加記事
- [M5StackとPythonで受入テスト自動化の要素技術を試す](https://qiita.com/pbjpkas/items/97b6e596a4009a71f1ab) ([ソフトウェアテスト #2 Advent Calendar 2018 | 13日目](https://qiita.com/advent-calendar/2018/softwaretesting-2))
- [ストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化する](https://qiita.com/pbjpkas/items/d525d7d19095d1e26452) ([ソフトウェアテストの小ネタ Advent Calendar 2019 | 13日目](https://qiita.com/advent-calendar/2019/software-testing-koneta))

[^1]: 従来はMaster、Slave、Jobという用語が用いられていましたが、Jenkinsの[公式用語集](https://www.jenkins.io/doc/book/glossary/)に倣ってController、Agent、Projectとしています。なお、[中国語版の用語集](https://www.jenkins.io/zh/doc/book/glossary/)にはControllerの項目はなくMasterとなっています。
[^2]: テストランナー([test-runner.py](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d#%E4%BB%98%E9%8C%B2a-test-runnerpy%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9))はコントローラとエージェントで同じものを使用します。エージェントにインストールしていないライブラリ(cv2、PIL、pyocr)は今回は使用しないこともありimportしないようコメントアウトしています。
[^3]: runコマンドは外部プログラムを実行するコマンドです。詳しくは[Lチカで始めるテスト自動化(13)外部プログラムの呼出し](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d)をご参照ください。
[^4]: Jenkinsのインストールは「[マイコンのUARTコマンド制御をJenkins、Raspberry Pi、Pythonで行う](https://qiita.com/pbjpkas/items/f2f8eeab76dda95891d6)」の[4.マスター側Jenkins](https://qiita.com/pbjpkas/items/f2f8eeab76dda95891d6#4-%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC%E5%81%B4jenkins)をご参照ください。