20年物のC言語で作られたシステムのテスト工程を改善しようとした話

はじめに

ちょっと前に20年物のC言語で作られたシステムのテストを色々改善しようとしてみたので、この時に得たちょっとした知見を書いていこうと思います。

※注意
記事を書くために自分のパソコンで当時を思い出しながら環境を作っているので、実際、実務でやった環境やバージョンとは違います。
また、この記事にはいくつかコードがでてきますが、すべて記事を書くために考えた疑似的な例にすぎません。

単体テスト用のテストコードの作成

20年も動いているシステムだと、もはや誰にも意味はわからんが、既存の挙動を変えてはいけない箇所がいくつもあります。

そういう箇所に手を入れざるを得ないときに、有効な方法として以下のような方法があります。

まず、既存のコードに対するテストコードを記載します。そして全て合格することを確認してから、少しづつ機能を拡張していきます。
これにより、新規機能追加が既存の機能を壊していないことを確認しながら機能追加の作業を進めることが可能になります。

いわゆるテストファーストになりますが、こういう長い歴史をもつ保守的な環境で、XPで言っていたようなことをやると色々と歪みが出ます。
それについてはこの章の最後に記載します。

xUnitの導入

assert等で自力で頑張ってもいいですが、CUnitなどのxUnitのテストフレームワークを使用した方がいいです。

※今回はCUnitしか検証していませんが、最近のモノだと以下のようなC言語用のユニットテストのライブラリがあるようです。

Cutter(2019-09-13にリリースされた1.2.7が最新)
https://cutter.osdn.jp/index.html.ja

UNITY(2017/11にv2.4.3をリリース。コミット履歴は2019/10のものもある)
http://www.throwtheswitch.org/unity
https://github.com/ThrowTheSwitch/Unity

また、CPPUnitなどのC++用のxUnitのフレームワークからもテストが可能ですが、今回は外しています。

何故xUnitのテストフレームワークが必要か

有名どころのテストフレームワークを使用した方が、実現例とかが世の中で出回っていてトラブル時に助かります。
また、有名どころのテストフレームワークを使用するとJenkinsとの連携が楽になります。

たとえばJenkinsのxUnit Pluginは有名どころのxUnitで出力されたXMLを集計する機能を有しており、簡単に自動テストの結果を集計することが可能になっています。

CUnit

CUnitはC言語用のxUnitのフレームワークでgccやvisual cから使用することが可能です。

visual studio2019でビルドする方法

VS2008とか2005の時代のソリューションしかないのでいかんせ古くてワーニングが出まくります。

  1. CUnit-2.1-3/VC9/CUnit.slnを開く
  2. CUnit-2.1-3/CUnit/Headers/CUnit.h.inを基にCUnit.hを作成します。違いは以下の通りです。
    image.png
  3. libcunit->その他のプロジェクトの順番でビルドしていきます。
    ※一度にビルドしてエラーが出る場合は、プロジェクトの依存関係を見直してください。また、プロジェクトの同時ビルド数を1にすると正常にビルドできるようになります。

Visual Stduio2019でプロジェクトの同時ビルド数の確認方法
メニューの「ツール」→「オプション」でオプションダイアログを開き「プロジェクトおよびソリューション」→「ビルド/実行」で該当の画面が表示されます。
image.png

makeコマンドでビルドする方法

以下でWindows10 + Ubuntu 18.04の環境下でmakeコマンドを利用してビルドする例を示します。
基本的に以下の記事と同じ方法でビルドできますがバージョンは2.1-3にしています。

CUnitでCプログラムの単体テストをする
https://qiita.com/muniere/items/ff9a984ed7e51eee7112

wget https://jaist.dl.sourceforge.net/project/cunit/CUnit/2.1-3/CUnit-2.1-3.tar.bz2
tar xvf CUnit-2.1-3.tar.bz2
cd CUnit-2.1-3
aclocal
autoconf
automake
./configure
make
make install
トラブルシュート
automakeやautoconfコマンドがない

以下を実行してインストールする。

sudo apt-get install autoconf              
configureとか実行すると「configure: error: cannot find install-sh, install.sh, or shtool in "." "./.." "./../.."」というエラーがでる。

下記のコマンドを実行してautoconfからやり直す

autoreconf -i
configure時に「error: Libtool library used but 'LIBTOOL' is undefined」が発生した

以下のコマンドでlibtoolをインストールする。

sudo apt-get install libtool
実行時に「error while loading shared libraries: libcunit.so.1: cannot open shared object file: No such file or directory」が出る場合

環境変数LD_LIBRARY_PATHを設定する。

export LD_LIBRARY_PATH=/usr/local/lib

永続化したい場合は.bashrcにでも記載しておく

実行例

コンソールで実行する場合は以下のようになります。

# include <CUnit/CUnit.h>

int max(int x, int y) {
    if (x>y) {
        return x;
    } else {
        return y;
    }
}
int min(int x, int y) {
    /** bug  **/
    if (x>y) {
        return x;
    } else {
        return y;
    }
}

void test_max_001(void) {
    CU_ASSERT_EQUAL(max(5, 4) , 5);
}
void test_max_002(void) {
    CU_ASSERT_EQUAL(max(4, 5) , 5);
}
void test_max_003(void) {
    CU_ASSERT_EQUAL(max(5, 5) , 5);
}
void test_min_001(void) {
    CU_ASSERT_EQUAL(min(5, 4) , 4);
}
void test_min_002(void) {
    CU_ASSERT_EQUAL(min(4, 5) , 4);
}
void test_min_003(void) {
    CU_ASSERT_EQUAL(min(5, 5) , 5);
}

int main() {
    CU_pSuite max_suite, min_suite;

    CU_initialize_registry();
    max_suite = CU_add_suite("max", NULL, NULL);
    CU_add_test(max_suite, "test_001", test_max_001);
    CU_add_test(max_suite, "test_002", test_max_002);
    CU_add_test(max_suite, "test_003", test_max_003);

    min_suite = CU_add_suite("min", NULL, NULL);
    CU_add_test(min_suite, "test_001", test_min_001);
    CU_add_test(min_suite, "test_002", test_min_002);
    CU_add_test(min_suite, "test_003", test_min_003);
    CU_console_run_tests();
    CU_cleanup_registry();

    return(0);
}

他のASSERT用の関数については下記を参照してください。
http://cunit.sourceforge.net/doc/writing_tests.html

CUnitを実行するソースをコンパイルする際はlibcunit.aにリンクしておいてください。

gcc unit.c  -Wall -L/usr/local/lib -lcunit -o unit

コンソールで実行した場合は以下のようになります。
image.png

Jenkinsとの連携方法

CUnitの結果をJenkinsに取り込む方法について説明します。

テストコードでXMLを出力するようにする

Jenkinsに渡すXMLファイルを作成するようにコードを変更します。
XMLファイルに出力する場合、CU_console_run_testsの代わりにCU_automated_run_testsとCU_list_tests_to_fileを使用します。

/**略**/
    /** コンソール出力
    CU_console_run_tests();
    */
    /** XML の出力 **/
    CU_set_output_filename("./unit");
    CU_automated_run_tests();
    CU_list_tests_to_file();
    CU_cleanup_registry();
/**略**/

上記のファイルを変更してコンパイルして実行するとコンソールに出力する代わりに下記のXMLファイルが作成されます。

  • unit-Listing.xml
  • unit-Results.xml

CUnitの実行方法の詳細については下記を参照してください。
http://cunit.sourceforge.net/doc/running_tests.html#overview

Jenkinsでテスト結果のXMLを解析するようにする。

プラグインの管理でxUnit Pluginをインストールします。
image.png

ジョブの設定でビルド後の処理に「Publish xUnit test result report」を追加してCUnit-2.1を選択します。

image.png

Patternにはunit-Results.xmlを指定してください。

ビルドを実行すると以下のような結果が作成されます。
image.png

image.png

テスト用のスタブの作成

テストを自動化する場合、xUnitのテストフレームワークだけを用意して自動化をしようとすると、導入に失敗するケースが多いです。
単純なユーティリティー関数しかテストできずに廃れていくか、あるいは、依存するライブラリの整合性をとるために複雑なテストコードを記述する羽目になって破綻するかのどちらかです。

たとえば以下のようにライブラリ1に依存するライブラリ2があるとします。
image.png

ライブラリ2をテストする場合、ライブラリ1のテスト用のスタブを作成し、ライブラリ2のテストの都合のいいデータを返すようにします。
そうすることで、仮にライブラリ1がネットワークやデータベースに依存していたとしても、実際にはそれらを使用せずにテストに都合のいい状態にすることができます。
image.png

単純なスタブの作成例

単純な例でライブラリ1に依存しているライブラリ2のテストを書く方法について考えてみましょう。

ライブラリ1のサンプル

ライブラリ1は以下のような実装とします。
引数を2つ受け取り、それを加算した結果を返すだけのaddという関数を用意します。

static1.h

# ifndef STATIC1_H
# define STATIC1_H
extern int add(int x, int y);
# endif

static1.c

# include "static1.h"

int add(int x, int y) {
    return x + y;
}
ライブラリ2のサンプル

ライブラリ2は以下のような実装とします。
ライブラリ2ではproc関数でライブラリ1のadd関数を利用して処理をしています。

static2.h

# ifndef STATIC2_H
# define STATIC2_H

# include "static1.h"

extern int proc(int x, int y, int z);

# endif

static2.h

# include "static2.h"

int proc(int x, int y, int z) {
    int a = add(x,y);
    return add(a, z);
}
ライブラリ1のスタブ

本物のコードの代わりに、スタブを作成します。
このスタブの処理はadd関数が実行されるたびに以下の処理を行います。
・呼び出された回数の記録
・テストコードで事前に登録されたコールバック関数の実行

stub_static.h

# ifndef STUB_static1_H
# define STUB_static1_H
# include "static1.h"

typedef int (*pfunc_add_def) ( int x , int y );

typedef struct {

    int count_add;
    pfunc_add_def pfunc_add;
} stub_data_def_static1;
extern stub_data_def_static1 stub_data_static1;
# endif

stub_static.c

# include "stub_static1.h"
stub_data_def_static1 stub_data_static1;

int add ( int x , int y ) {
    ++stub_data_static1.count_add;

    return stub_data_static1.pfunc_add( x , y );

}
テストコードの例

add関数のスタブから実行されるコールバック関数をテストコードに定義して、proc関数を実行する例を以下に示します。

# include <CUnit/CUnit.h>
# include "static2.h"
# include "stub_static1.h"

int test001_stub_add(int x, int y) {
    /** add()のスタブ。CUNITでテストするならxとyを **/
    if (stub_data_static1.count_add == 1) {
        /** 1回目の呼び出し **/
        CU_ASSERT_EQUAL(x , 10);
        CU_ASSERT_EQUAL(y , 5);
        return 15;
    }
    if (stub_data_static1.count_add == 2) {
        CU_ASSERT_EQUAL(x , 15);
        CU_ASSERT_EQUAL(y , 4);
        return 19;
    }
    CU_FAIL("addの呼び出し回数不正");
    return -1;
}

void test001() {
    memset(&stub_data_static1, 0, sizeof(stub_data_static1));
    stub_data_static1.pfunc_add = test001_stub_add;
    int ret = proc(10, 5,4);

    /** 呼び出し回数の確認 **/
    CU_ASSERT_EQUAL(stub_data_static1.count_add , 2);

    CU_ASSERT_EQUAL(ret , 19);
}

int main() {
    CU_pSuite suite;

    CU_initialize_registry();
    suite = CU_add_suite("stubsample", NULL, NULL);
    CU_add_test(suite, "test001", test001);
    CU_console_run_tests();
    CU_cleanup_registry();

    return(0);
}

スタブ用関数が複数回呼び出される場合は、呼び出し回数を元に、スタブが受け取る予定の引数の値をチェックし、スタブが返すべき値を決定します。

スタブの自動作成

前項でスタブを利用した簡単なテストコードの例を紹介しました。
次に問題となるのが、スタブの作成コストです。

手で作成していくことも不可能ではないですが、ライブラリ1の関数が大量にある場合や、更新頻度が多い場合、手で修正していくのは辛いものがあります。
そこで、スタブを自動生成する方法について考えてみましょう。

基本的には単純な変換になります。
そのため、どんなプログラミング言語でもスタブの自動生成できますが、楽をしたいなら、テンプレートライブラリとC言語のパーサーが使えるプログラミング言語がよいでしょう。

Pythonでスタブを自動生成してみる

C言語のパーサーライブラリ

pycparserを使用することでC/C++のソースコードの解析が行えます。
https://github.com/eliben/pycparser

注意事項として、このライブラリはプリプロセッサで処理済みのコードに対してのみしか動作しません。そのため、実際、解析対象とするソースコードは以下のようにプリプロセッサを実行した結果を用いてください。

gcc -E static1.h

今回は以下のようなプリコンパイル済みのヘッダーファイルを解析するものとします。

解析対象のヘッダファイル

extern int add(int x, int y);
typedef struct {
 int x;
 int y;
} in_data_t;
typedef struct {
 int add;
 int minus;
} out_data_t;
extern void calc(in_data_t* i, out_data_t *o);
extern int max(int data[], int length);

ヘッダファイルを解析するサンプル

import sys
from pycparser import c_parser, c_ast, parse_file

# https://github.com/eliben/pycparser/blob/master/examples/cdecl.py
def _explain_type(decl):
    """ Recursively explains a type decl node
    """
    typ = type(decl)

    if typ == c_ast.TypeDecl:
        quals = ' '.join(decl.quals) + ' ' if decl.quals else ''
        return quals + _explain_type(decl.type)
    elif typ == c_ast.Typename or typ == c_ast.Decl:
        return _explain_type(decl.type)
    elif typ == c_ast.IdentifierType:
        return ' '.join(decl.names)
    elif typ == c_ast.PtrDecl:
        quals = ' '.join(decl.quals) + ' ' if decl.quals else ''
        return quals + _explain_type(decl.type) + '*'
    elif typ == c_ast.ArrayDecl:
        arr = 'array'
        if decl.dim: 
            arr = '[%s]' % decl.dim.value
        else:
            arr = '[]'

        return _explain_type(decl.type) + arr

    elif typ == c_ast.FuncDecl:
        if decl.args:
            params = [_explain_type(param) for param in decl.args.params]
            args = ', '.join(params)
        else:
            args = ''

        return ('function(%s) returning ' % (args) +
                _explain_type(decl.type))

    elif typ == c_ast.Struct:
        decls = [_explain_decl_node(mem_decl) for mem_decl in decl.decls]
        members = ', '.join(decls)

        return ('struct%s ' % (' ' + decl.name if decl.name else '') +
                ('containing {%s}' % members if members else ''))

def show_func_defs(filename):
    # Note that cpp is used. Provide a path to your own cpp or
    # make sure one exists in PATH.
    ast = parse_file(filename, use_cpp=False,
                     cpp_args=r'-Iutils/fake_libc_include')
    if not isinstance(ast, c_ast.FileAST):
        return

    for ext in ast.ext:
        if type(ext.type) == c_ast.FuncDecl:
            print(f"function name: {ext.name} -----------------------")
            args = ''
            print("parameters----------------------------------------")
            if ext.type.args:
                for arg in ext.type.args:
                    print(f"{_explain_type(arg)} {arg.name}")
            print("return----------------------------------------")
            print(_explain_type(ext.type.type))

if __name__ == "__main__":
    if len(sys.argv) > 1:
        filename  = sys.argv[1]
    else:
        exit(-1)

    show_func_defs(filename)

解析処理の簡単な流れとしては、まずparse_fileを実行結果としてFileASTが取得できます。
そこのextプロパティを解析することで、ファイルに定義している関数の情報を取得できます。
上記のプログラムを実行してヘッダファイル中の関数定義を調べた結果が以下のように出力されます。

C:\dev\python3\cparse>python test.py static1.h
function name: add -----------------------
parameters----------------------------------------
int x
int y
return----------------------------------------
int
function name: calc -----------------------
parameters----------------------------------------
in_data_t* i
out_data_t* o
return----------------------------------------
void
function name: max -----------------------
parameters----------------------------------------
int[] data
int length
return----------------------------------------
int

このようにpycparserを使用することでC言語のソースファイルの解析が容易に行えることが確認できました。

テンプレートライブラリ

スタブ用のヘッダファイルとソースファイルを作成する際に、テンプレートライブラリを使用すると作成が楽になります。
Pythonでのテンプレートライブラリについては下記を参考にしてください。

Pythonで久しぶりにHTMLを出力したくなったのでテンプレートについて調べる
https://needtec.sakura.ne.jp/wod07672/?p=9162

今回はJinja2を利用しています。

スタブ作成用コード

C言語のパーサーとテンプレートライブラリを使用して、スタブ作成ツールを実装した例が以下のようになります。

create_stub.py

import sys
import os
from pycparser import c_parser, c_ast, parse_file
from jinja2 import Template, Environment, FileSystemLoader

stub_header_tpl = """
# ifndef STUB_{{name}}_H
# define STUB_{{name}}_H
# include "{{name}}.h"
{% for func in function_list %}
typedef {{func.return_type}} (*pfunc_{{func.name}}_def) ({% for arg in func.args %}{% if loop.index != 1 %},{% endif %} {{arg.typedef}} {{arg.name}}{{arg.array}} {% endfor %});
{% endfor %}

typedef struct {
{% for func in function_list %}
    int count_{{func.name}};
    pfunc_{{func.name}}_def pfunc_{{func.name}};
{% endfor %}
} stub_data_def_{{name}};
extern stub_data_def_{{name}} stub_data_{{name}};
# endif
"""

stub_source_tpl = """
# include "stub_{{name}}.h"
stub_data_def_{{name}} stub_data_{{name}};

{% for func in function_list %}
{{func.return_type}} {{func.name}} ({% for arg in func.args %}{% if loop.index != 1 %},{% endif %} {{arg.typedef}} {{arg.name}}{{arg.array}} {% endfor %}) {
    ++stub_data_{{name}}.count_{{func.name}};
    {% if func.return_type != "void" %}
    return stub_data_{{name}}.pfunc_{{func.name}}({% for arg in func.args %}{% if loop.index != 1 %},{% endif %} {{arg.name}} {% endfor %});
    {% else %}
    stub_data_{{name}}.pfunc_{{func.name}}({% for arg in func.args %}{% if loop.index != 1 %},{% endif %} {{arg.name}} {% endfor %});
    {% endif %}
}
{% endfor %}
"""

class ParameterData():
    def __init__(self, name, typedef):
        self.__name = name
        self.__typedef = typedef
        self.__array = ""
        if '[' in typedef:
            self.__array = "[" + typedef[typedef.index("[")+1:typedef.rindex("]")] + "]"
            self.__typedef = typedef[0: typedef.index("[")]

    @property
    def name(self):
        return self.__name

    @property
    def typedef(self):
        return self.__typedef

    @property
    def array(self):
        return self.__array

class FunctionData:
    def __init__(self, name, return_type):
        self.__name = name
        self.__return_type = return_type
        self.__args = []

    @property
    def name(self):
        return self.__name

    @property
    def return_type(self):
        return self.__return_type

    @property
    def args(self):
        return self.__args

# https://github.com/eliben/pycparser/blob/master/examples/cdecl.py
def _explain_type(decl):
    """ Recursively explains a type decl node
    """
    typ = type(decl)

    if typ == c_ast.TypeDecl:
        quals = ' '.join(decl.quals) + ' ' if decl.quals else ''
        return quals + _explain_type(decl.type)
    elif typ == c_ast.Typename or typ == c_ast.Decl:
        return _explain_type(decl.type)
    elif typ == c_ast.IdentifierType:
        return ' '.join(decl.names)
    elif typ == c_ast.PtrDecl:
        quals = ' '.join(decl.quals) + ' ' if decl.quals else ''
        return quals + _explain_type(decl.type) + '*'
    elif typ == c_ast.ArrayDecl:
        arr = 'array'
        if decl.dim: 
            arr = '[%s]' % decl.dim.value
        else:
            arr = '[]'

        return _explain_type(decl.type) + arr

    elif typ == c_ast.FuncDecl:
        if decl.args:
            params = [_explain_type(param) for param in decl.args.params]
            args = ', '.join(params)
        else:
            args = ''

        return ('function(%s) returning ' % (args) +
                _explain_type(decl.type))

    elif typ == c_ast.Struct:
        decls = [_explain_decl_node(mem_decl) for mem_decl in decl.decls]
        members = ', '.join(decls)

        return ('struct%s ' % (' ' + decl.name if decl.name else '') +
                ('containing {%s}' % members if members else ''))

def analyze_func(filename):
    ast = parse_file(filename, use_cpp=False,
                     cpp_args=r'-Iutils/fake_libc_include')
    if not isinstance(ast, c_ast.FileAST):
        return []
    function_list = []
    for ext in ast.ext:
        if type(ext.type) != c_ast.FuncDecl:
            continue
        func = FunctionData(ext.name,  _explain_type(ext.type.type))
        if ext.type.args:
            for arg in ext.type.args:
                param = ParameterData(arg.name, _explain_type(arg))
                func.args.append(param)
        function_list.append(func)
    return function_list

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("python create_stub.py プリプロセス実行済みヘッダ 出力フォルダ")
        exit(-1)
    filename  = sys.argv[1]
    dst_folder  = sys.argv[2]
    function_list = analyze_func(filename)
    if len(function_list) == 0:
        print("関数を見つけることができませんでした")
        exit(-1)
    data = {
        'name' : os.path.splitext(filename)[0],
        'function_list' : function_list
    }
    # スタブのヘッダーファイルの作成
    with open(f"{dst_folder}/stub_{data['name']}.h", mode='w', encoding='utf8') as f:
        f.write(Template(stub_header_tpl).render(data))

    # スタブのソースファイルの作成
    with open(f"{dst_folder}/stub_{data['name']}.c", mode='w', encoding='utf8') as f:
        f.write(Template(stub_source_tpl).render(data))

このツールを利用して作成したスタブは以下の通りです。

stub_static1.h


# ifndef STUB_static1_H
# define STUB_static1_H
# include "static1.h"

typedef int (*pfunc_add_def) ( int x , int y );

typedef void (*pfunc_calc_def) ( in_data_t* i , out_data_t* o );

typedef int (*pfunc_max_def) ( int data[] , int length );

typedef struct {

    int count_add;
    pfunc_add_def pfunc_add;

    int count_calc;
    pfunc_calc_def pfunc_calc;

    int count_max;
    pfunc_max_def pfunc_max;

} stub_data_def_static1;
extern stub_data_def_static1 stub_data_static1;
# endif

stub_static.c


# include "stub_static1.h"
stub_data_def_static1 stub_data_static1;

int add ( int x , int y ) {
    ++stub_data_static1.count_add;

    return stub_data_static1.pfunc_add( x , y );

}

void calc ( in_data_t* i , out_data_t* o ) {
    ++stub_data_static1.count_calc;

    stub_data_static1.pfunc_calc( i , o );

}

int max ( int data[] , int length ) {
    ++stub_data_static1.count_max;

    return stub_data_static1.pfunc_max( data , length );

}

C言語のパーサーが利用できないとき

Pythonが使えなかったりC言語のパーサーが利用できなかったりする状況もよくあります。
そういう場合は、doxygenを利用します。

Doxygenはソースファイルを解析して関数の情報をXMLに出力することができます。このXMLをもとにスタブ用のファイルを作成することも可能です。CやC++をやっている現場の多くはdoxygenを使用しているケースが多いので、おそらくPython使うよりは導入の障壁が低いと考えられます。※

※なお、私はDoxyge+VBSでスタブを作っていました。

その他スタブやモックの作成方法について

実際には採用しませんでしたが、以下のような方法もあります。

CMock
https://github.com/ThrowTheSwitch/CMock
Rubyを使用してモック用のコードを作成する模様。当該環境でRubyが使えるならありかもしれません。

C言語のテストでスタブ関数を使うためのアイデア
https://qiita.com/HAMADA_Hiroshi/items/e1dd3257573ea466d169
「既存ソースに手を入れずに同一ソース内の関数をスタブにしてユニットテストを実施する方法」をまとめたもので、基本的な考えはマクロを駆使して既存関数を別名に置き換えるということをやっています。
幸い、同一ソース内の関数を呼ぶようなテストを書く必要がなかったので実際には採用していません。

網羅率の計測方法

テストコードを記述した場合、そのテストコードが網羅している割合を計測することで、もれなくテストを実行しているかを判断することが可能になります。
幸いなことにgccにはgcovという網羅率を計測するツールがついています。

10 gcov—a Test Coverage Program
https://gcc.gnu.org/onlinedocs/gcc/Gcov.html

gcov の使い方
https://mametter.hatenablog.com/entry/20090721/p1

カバレッジの計測例

先ほど使用したスタブの例でカバレッジを計測してみます。今回はわざと未到達なルートを作るため、ライブラリ2のstatic2.cを以下のように修正します。

static2.c

# include "static2.h"

int proc(int x, int y, int z) {
    int a = add(x,y);
    return add(a, z);
}

int proc2() {
    /** 未到達 **/
    return 0;
}

その後、カバレッジを計測するために-coverageオプションを付与してコンパイルをします。

gcc -c -Wall -Wextra static2.c -coverage
ar r libstatic2.a static2.o 
gcc -c -Wall -Wextra stub_static1.c
ar r libstub_static1.a stub_static1.o
gcc test.c -coverage  -Wall -Wextra -L. -lstatic2 -lstub_static1   -L/usr/local/lib -lcunit -o test

上記のコマンドを実行すると以下のファイルが作成されます。

  • static2.gcno
  • test.gcno
  • test

その後、testを実行することでさらに以下のファイルが作成されます。

  • static2.gcda
  • test.gcda

テスト対象のstatic2.cのカバレッジを調べるには以下のコマンドを実行します。

gcov static2.gcda 

このコマンドを実行すると下記のメッセージが表示されます。

File 'static2.c'
Lines executed:60.00% of 5
Creating 'static2.c.gcov'

ここで網羅率が60%であることが確認でき、さらにどこが網羅されていないかを調べるにはstatic2.c.gcovを表示します。
今回の例ではproc2が通っていないことが確認できます。
image.png

その他自動テストについて

効果について

以下の点で効果的と判断します。

・現状の機能の挙動を変えずに機能を拡張する際のリスクが抑えられる
・テストコードを記載することで、今まで闇の中だった機能を明確にできる。
・テストコードを書くと既存コードの中に眠り続けてきた不具合を検出できる
  →直すかどうかは別問題。発生頻度や発生時の影響度と修正コストの兼ね合い。

ただ、幾つの点で課題があるのでそれについては次項で考えていきましょう。

SQLの試験はどうするの?

レガシーな環境だと、ソースコードにSQLを書いていて、実際それをデータベースにつなげて動かさなければテストにならないことがあります。
この場合、開発者毎に使用するDBの領域を区切って単体テストを行うのが望ましいです。

しかしながら、大きな組織だとDBを専門に扱う部門があったりして、安易にデータベースをいじれなかったりします。
またローカル環境にデータベース自体を入れるとした場合、貧弱なPCがネックになったり、仮にそれを突破しても、個人の環境でデータベースの管理を任せた場合、開発者によっては古い環境を使い続けたりするリスクもあったりします。

こういう時は自動テストが終わったらロールバックするという意識の低い方法で対応することができます。

①自動テスト開始
②DBのトランザクションを開始する
③テスト用のデータをDBに格納
④自動テストを行う
⑤自動テストの結果でDBがどう変わったか確認
⑥DBのロールバック

この方法は完璧な案ではありませんが、実現が容易な案です。

大量の組み合わせテスト

構造体の配列で入力値と期待値を書いておいておけば大量のテストが行えます。
よく使う手としてはExcelで入力と出力の一覧を書いておいて、VBAのマクロでC言語の構造体の配列に変換してテストコードに書き込むやり方があります。

保守的なウォーターフォールな環境でテストファーストを行う歪み

保守的なウォーターフォールな環境で採用されている方法だと、実装が終わった後に、単体テストのテスト仕様書やチェックリストを作成して、テストを実施することになります。
しかし、テストファースト的なことをやると、実装完=単体テスト完になるので、ここで歪みが発生してしまいます。

計画が従来の線に乗らない

従来の方式だと実装、単体テスト用チェックリスト作成、単体テストの実施の3つの工程にわけてスケジュールの線を記述します。
そして、高度に管理された完全で完璧な組織だと、それぞれの工程の時間をきっちり計測します。

テストファースト的なことをやると、実装から単体テストがり分けることのできないものになるので、個別の作業時間が計測不能となり、適当に分けて報告することになります。

単体テスト仕様書

従来の方法で作成していた単体テスト仕様書やチェックリストの取り扱いをどうするか決める必要があります。
テストコードのコードレビューをもってOKとし、単体テストの成果物としてはテストコードとするという判断ができればいいのですが、レガシーな環境だと書類が大事です。
この場合、テストコードからテスト仕様書やチェックリストといったものを作成する必要があります。

1つ目の対応策としては、テストコードの内容を手でチェックリストに転記していくという方法です。
問題点は、テストコードとテスト仕様書の同期がとれなくなっていくことです。
また、この方法は自動でテストを実行する利点をなくす非常にまずい対応ですが、時間さえかければ誰でもできるのでよく採用されます。

2つ目の方法は、テストコードのコメントからテスト仕様書を起こす方法です。
テストコードのコメントにテストの内容を記載しておいてDoxygen等で抜き出した結果をチェックリストにまとめます。
この場合、コメントと実際の処理の食い違いが発生するリスクがあります。

3つ目の方法は、テストコード中に指定するテスト名にチェックリストに出力しても違和感のない日本語を書いてしまう方法です。

    CU_add_test(max_suite, "max(a,b)を実行してa=5,b=4の場合、aの値が取得されることを確認する", test_max_001);

上記のように記載してテストを実行するとテスト結果のXMLには以下のように出力されます。
image.png

あとは、XMLをチェックリストの出力してしまえば完了です。
この方法の問題点は、CU_ASSERT_EQUALなどで行うチェック内容はXMLに出力されないので、チェックリストの粒度が大きくなってしまう点です。

4つ目の方法としては、CU_ASSERT_EQUALなどをラップする関数を作成して、その関数内で、チェックリストに出力しやすい形で検査内容をファイル出力してしまう方法です。

いずれの方法をとるにせよ、書面にこだわられると面倒臭いので注意しましょう。
 →書面にこだわる文化のところでは、そういう作業分もちゃんと見積もりに乗せるようにしましょう。

単体テストでの障害票の取り扱い

実装を終了した時点で単体テストが終わっているので、単体テストでの障害を上げるのは不可能になっています。
とはいえ、高度に管理された完全で完璧な組織だと単体テストの障害件数を重視する場合があります。
この場合は、実装中に出た問題を、それっぽいバグとして適当にでっちあげることになります。

本来あるべき論としては説得して新しいルールを作ることですが、文化がちがうので無理だと思います。

テスト件数の桁が変わるので従来の指標が役に立たなくなる

自動テストを導入すると組み合わせ系のテストを機械的に大量に処理できるようになります。
結果、従来の方法で行ったテストの件数より自動で行ったテストの件数が多くなります。(それこそ桁違い)

いままで精々数百のチェックリストの項目数だったものが、普通に千とか万とかになります。
「テストを大量にやってなにが悪いのかね」という話なんですが、高度に管理された完全で完璧な組織だと異常値扱いされるので、粒度を適当に調整して、報告用のそれっぽい件数に調整したりする必要があったりします。

なお、まっとうな対応をするのであるならば、自動テストは別集計として新しい指標を作っていくのが筋でしょう。

結合テスト以降の障害が従来より減るので異常値扱いされる

単体テストを網羅的にやると、結合テストでもなかなか不具合がでなくなります。

これはこれで問題になります。
たとえば、従来、何十件も障害が発生していたところ、不具合件数がほぼでなくなったとしたとしましょう。
数値しか見てない場合、結合テストの観点が間違っているんじゃないだろうかという疑念を抱く人がいてもしょうがないでしょう。

テストコード書きたがらない人が多い

以下でも書きましたが、そういうもんです。あきらめましょう。

意識が高くないVisualStudioを使用した単体テストの自動化
https://needtec.sakura.ne.jp/wod07672/?p=9213

自動テストの効果を工数や品質で利を説いたり、金額に換算して危機感あおったり、教育用の資料を用意することはできますが、正直、そんな話をいくら積んだところで通じない文化には全く通じません。

ただし、どんな組織においても、志のある人間というのが少数ながらいるモノなので、そういう人間に期待しましょう。

静的解析

前項で自動テストの話をしました。
これは結局、テストコードを書いた範囲を実際に動かしてチェックしているにすぎません。
この項では、実際にテストコードを書いて動かすことなく、怪しげな実装箇所を見つける方法について説明します。

静的解析ツールで怪しい箇所を見つける

静的解析ツールを使用することで怪しげな実装をしている箇所を検出可能です。
お高いツールを使うと効果的な発見をしてくれますが、オープンソースでも十分効果的です。

C言語の場合だとCppCheckを使用することができます。
http://cppcheck.sourceforge.net/

cppcheckはWindowsからGUIで動かすこともできますし、linuxでCUIベースに動かすこともできます。

コマンドラインから使用する方法

Windows10 + Ubuntu 18.04の環境下でcppcheckを使用する方法を説明します。

まず下記のコマンドでインストールします。

sudo apt-get install cppcheck

インストールが完了したら以下のようなコマンドを実行することでcppcheckが実行されます。

$ cppcheck --enable=all .
Checking cov.c ...
1/7 files checked 14% done
Checking main.c ...
2/7 files checked 28% done
Checking static1.c ...
3/7 files checked 42% done
Checking static2.c ...
4/7 files checked 57% done
Checking stub_static1.c ...
5/7 files checked 71% done
Checking test.c ...
6/7 files checked 85% done
Checking unit.c ...
7/7 files checked 100% done
[cov.c:7]: (style) The function 'bar' is never used.
[static1.c:7]: (style) The function 'calc' is never used.
[static2.c:8]: (style) The function 'proc2' is never used.
(information) Cppcheck cannot find all the include files (use --check-config for details)

WindowsのGUIでcppcheckを使用してみる

(1)以下のページからInstallerをダウンロードしてください。
http://cppcheck.sourceforge.net/

(2)インストール後、CppCheckを起動すると以下のような画面が開きます。
image.png

(3)メニューから「ファイル」→「新規プロジェクト」を選択します。
image.png

(4)プロジェクトファイルを保存する任意のパスを設定して「保存」を押下します。
image.png

(5)「プロジェクトファイル」ダイアログが開くので「追加」ボタンを押下してC言語のソースファイルがあるパスを追加してください。
image.png

image.png

image.png

(6)「プロジェクトファイル」ダイアログでOKを押下すると、ビルドディレクトリを作成するか聞かれるので「Yes」を選択します。
image.png

(7)指定したディレクトリ以下のCファイルが列挙されます。
image.png

(8)列挙されたファイルのツリー構造を展開すると警告の詳細が表示されます。
image.png

Jenkinsとの連携

JenkinsのCppcheck Plulginを使用することでCppCheckの結果をJenkinsで集計させることが可能です。
https://plugins.jenkins.io/cppcheck

(1)JenkinsのプラグインマネージャーでCppCheckを選択してインストールします。
image.png

(2)cppcheckを以下のように実行してxmlで結果を取得します。

cppcheck --enable=all --xml --xml-version=2 ~/libsample/. 2>cppcheck.xml

(3)ビルド後の処理にPublsh Cppcheck resultsを追加します。
image.png

image.png

(4)ビルドを実行すると、結果にCppCheckの項目が表示されます。
image.png

image.png

メトリクスを計測する

ソースコードのステップ数や、循環的複雑度はソースコードの品質を予測するための一つの指標になります。

循環的複雑度(Cyclomatic complexity)
https://ja.wikipedia.org/wiki/%E5%BE%AA%E7%92%B0%E7%9A%84%E8%A4%87%E9%9B%91%E5%BA%A6

循環的複雑度は簡単にいうと、ループや分岐が多いほどこの複雑度は高くなります。
複雑度が高ければ高いほどバグを含む可能性が高くなるので、この指標をもとにテストを重点的に行うべき場所にあたりをつけることができます。

Source Monitor

Source MonitorはC ++、C、C#、VB.NET、Java、Delphi、Visual Basic(VB6)で書かれたソースコードのメトリクスを計測することが可能なフリーソフトです。

(1)下記のページからダウンロードして任意のフォルダに展開してください。
http://www.campwoodsw.com/sourcemonitor.html

(2)展開されたSourceMonitor.exeを実行すると以下のような画面が表示されるので「Start SourceMonitor」を選択してください。
image.png

(3)メトリクスを集計するプロジェクトを作成するのでメニューから[File]->[New Project]を選択します。
image.png

(4)「Select Language」ダイアログが開くので「C」を選択して「次へ」を押下します。
image.png

(5)プロジェクトファイルのファイル名と格納するフォルダを指定して「次へ」を押下します。
image.png

(6)解析対象のソースコードを含むフォルダを指定して「次へ」を押下します。
image.png

(7)オプションを選択し「次へ」を押下します。
image.png

オプション 説明
Use Modified Complexity Metrix デフォルトの複雑度メトリックは、各switchステートメント内のcaseをカウントして計算していますが、チェックをONにすることで各switchブロックは1回だけカウントされるようになり、個々のcaseステートメントはModified Complexityに寄与しなくなります。
Do not count blank line 行数を数える際に、空行を無視します。
Ignore Continuous Header and Footer Comments CVSやVisual SourceSafeなどのバージョン管理ユーティリティによって自動的に生成されたファイル履歴などの情報を含む連続したコメント行を無視したい場合にチェックをつけます。

(8)「次へ」を押下します。
image.png

(9)UTF8のファイルを含むなら、「Allow parsing of UTF-8 files」にチェックを付けて「次へ」を押下します。
image.png

(10)「完了」を押下します。
image.png

(11)計測対象のファイルの一覧が表示されるので、確認後、OKを押下します。
image.png

(12)計測結果が表示されます。
image.png

リストをクリックするとファイルの一覧が表示されます。
image.png
image.png

リスト中のファイルのアイテムをクリックすると詳細が表示されます。
image.png

リスト中のファイルのアイテムを右クリックしてコンテキストメニューを表示して「View Source File」を選択することでファイルの内容が確認できます。
image.png
image.png

よく利用する数値

名前 説明
Lines ソースファイル内の物理行の数。プロジェクト作成に「Do not count blank line」をチェックした場合は空白行を含まない。ざっくりした規模をみるのに使用する
Max Complexity 循環的複雑度。プロジェクト作成時の「Use Modified Complexity Metrix」のチェックの有無によりswitch句のcaseの取り扱いが変わる。この数値が多いほどテストしずらい。
Max Depth ネストの深さの最大。ネストが深いコードも複雑になる傾向があるので注視する
% Comment コメントの割合。0%とか逆に大きすぎる場合、チェックした方がいい。

類似コードを探す

以下で紹介したPMD-CPDを使用することでコピペされたコードを検出することが可能です。

僕にそのコピペを直せというのか
https://needtec.sakura.ne.jp/wod07672/?p=9264

開発時と違い、類似のコードを探し出して共通化することが目的で使用はしていません。
すでに稼働してしまったシステムは、開発中と違い、コードがコピペされていようが、共通化などのリファクタリングを安易にすることはできません。

こういう状況でコードクローンを検出する目的は、例えば障害対応や機能追加が発生して、あるソースコードを修正する時、他に修正が必要な類似箇所があるかを調べるために使用します。

例えば、障害を出すと当然、「別の箇所は大丈夫?」とか問われることになります。
この際、「コードクローン検出ツールを使用して類似箇所を網羅的に調べて問題ないことを確認しました」とか、それっぽい事を言ってごまかすことができます。

静的解析ツール関係まとめ

実際にコードを動かさずに、コードの評価を行い修正すべき箇所を見つけることができます。

保守的な環境でテストコードを導入するコストは滅茶苦茶かかりますが、静的解析ツールの採用は低コストで行えるので、多くの組織で活用できると思います。

テスト環境まわりの話

テスト時の環境についての話をいくつかさせていただきます。
なお、この章から結合テストの話も出てくるので「C言語で作られたシステム」のタイトル詐欺になりつつあります。

単体テスト環境の分離

別システムが関係したりしている結合テスト時ならばともかく、単体テスト時にも下記のような構成でテストを行う現場があります。
image.png

この構成のまずい点は以下の通りです。
・お互いのテストが影響を与えて、本来合格するケースが不合格になったり、その逆のケースがあったりすることが頻発する
・単体テストという不安定な状況のため頻繁にテスト環境を入れ替える必要があり、そのたびに他の開発者の作業が止まる。

つまりテスト容易性が極めて悪くなります。このため、単体テストあたりまでは以下のように開発者毎にテスト環境を用意するのが望ましいです。

image.png

開発者毎にテスト環境をあたえるため、ひと昔前はLinuxで動くプログラムを無理やりWindowsのVisualC++でビルドして動かしたりしてました。
しかしながら、昨今はVMWareやVirtualBoxで仮想環境を構築できるのでそれを利用すべきでしょう。

なお、開発者に与えられたPCのメモリが4GBだったときは、仮想環境で云々はあきらめることになると思います。

テスト環境へのリリースの自動化の話

複数人が触るテスト環境にリリースする際は、なるべく自動で行えるようにしたほうがいいです。
リリース時の影響を最小限におさえるために、テスト環境が止まっている時間を短くしたり、だれも使っていないときにリリースする必要があります。これはテスト環境へのリリース作業を自動化しておくことで対応可能になります。

また、テスト環境へのリリースを記録として残すことで、どの時点で、どのリビジョンの資材が使われていたかを追跡可能になります。
Jenkinsとかの特定のツールにこだわる必要はなく、シェルスクリプトやバッチファイルでいいので自動でできるようにするといいでしょう。

なお、以下の記事のテクニックを使うことでWindowsから資材をWinSCPでアップしたあとにTeraTerm経由でスクリプトを実行して結果をメールで送信するということも可能になると思います。

WinSCPを自動化しても別にかまわんのだろ
https://needtec.sakura.ne.jp/wod07672/?p=9178

TeraTermのマクロをためしてみる
https://needtec.sakura.ne.jp/wod07672/?p=9177

RedmineをあきらめたオレたちのPowerShellでのOutlookの自動操作
https://needtec.sakura.ne.jp/wod07672/?p=9188

結合テスト時の大量データ作成の自動化

結合テスト時に、大量のデータを作成が必要な場合があります。
この際、結合テストの観点で「使用するデータはシステムで作成できるものとする」とあると、データベースに対してSQLでデータを流し込むという方法ができません。

システムが用意している画面を操作して大量のデータを登録するには、以下の「Windowsの画面操作を自動化しよう」で紹介しているテクニックが使用できます。

自称IT企業があまりにITを使わずに嫌になって野に下った俺が紹介するWindowsの自動化の方法
https://needtec.sakura.ne.jp/wod07672/?p=9163

ただし、採用するツールや操作対象のアプリケーションの特性上、入力エラー等をすり抜けるケースがあるので、そこは気をつけて採用する必要があります。

結合テストの画面操作を自動化できない?

以下の理由から対象を絞ったほうがいいと思います。

・採用するツールや操作対象のアプリケーションの特性上、自動操作が手動操作とまったく一緒にならないケースがある。
・画面操作の自動化は基本的に安定しない
・画面操作の自動操作は作成コストがかかる。

私がやるなら、スモークテストのための画面操作テストや、大量の繰り返し処理(大量データの作成や負荷試験)に絞って行います。

敗北の記録

理想的だが無駄だった話

世の中には理想的ではあるが、実際問題として導入できない無駄な話がいくつかあります。
ここでは、そういった無駄だった話を紹介したいと思います。

テスト管理ツールの導入の話

image.png

image.png

テスト工程の管理をするツール、TestLinkについて
https://needtec.sakura.ne.jp/wod07672/?p=9198

…ってなことを10年来いろんなとこで提案してますが、基本、SIerのような環境では厳しいと思います。

こういうツールに興味がある組織はすでに使っていますし、興味のない組織はテスト管理自体に本当に興味ないので、勝算の薄いツールの導入に色々なコストをかけるくらいなら、やるべきことは他にあると思います。

バグトラッキングシステムの導入の話

バグトラキングシステムを導入することでバグの履歴を管理できたり、だれがどれだけバグを抱えているかの状況判断ができたり、ソースコードとの紐づけなどが管理できるようになります。

残念ながら、ある種の状況では、そういうあるべき論に時間かけるより、「バグ票の書き方」といった基本的な教育に力を使った方がいい状況があるのだと思い知りました。

Wikiによる検索可能なナレッジの蓄積

検索性や履歴の保持を考えた場合、Office文章でナレッジを蓄えるよりWikiを採用した方が望ましいのです。

しかしながら、ある種の環境にいるIT技術者の多くは、文章を書くのにExcelしか使ったことがないケースがあるので、無理な導入はやめましょう。

無駄な話の次善策

上記の理想的な話が全て無駄話になっている現場においては、テスト仕様書、バグ管理票、ナレッジ文章はExcelやWordで記述されて共有フォルダで管理されていることでしょう。

この問題点は2点です。
1つ目は検索性の問題
2つ目は構成管理の問題です。

これらの問題にどう対応すべきか考えましょう。

検索性の問題

Office文章の検索については、いくつかのフリーソフトを使えば行えます。
なんなら、VBSなどで作成することができます。

なお、Officeの検索には時間がかかると思うので、可能であればキャッシュを使っているソフトを採用した方がストレスが溜まらないと思います。

構成管理の問題

共有フォルダに無造作に置かれたファイルは、いつだれが、どんな修正したかが不明になります。
また、誤操作かなにかで稀によく消えたりします。

この対応として共有フォルダでなく構成管理ツールを使用してドキュメントを管理しましょう。

なお、構成管理ツール導入が不可能だった場合は下記の記事を参考にプランBを考えてください。

共有フォルダ教が支配する世界に転生した場合の対応案
https://needtec.sakura.ne.jp/wod07672/?p=9173

さいごに

長い年月を経た保守的な環境でテスト工程を改善してみようとして私が得た知見を紹介しました。
以下の記事でも書きましたが、特に長い年月を経た保守的な環境を改善しようとする場合、とりあえず現状を受け入れたうえで現実的な着地点を探す必要があります。

ぼくのかんがえたさいきょーの提案方法
https://needtec.sakura.ne.jp/wod07672/?p=9223

なお、そもそも「テスト工程を改善しよう」とかいうセリフ自体が

image.png

という反応をされてもしょうがないものなので、たまたま受け入れられたら喜べばいい程度のもんだと思います。

jmockit 1.48を使ってみる

目的

本記事の目的はjmockit 1.48について調査した際のメモです。

ドキュメント
http://jmockit.github.io/index.html

ソースコード
https://github.com/jmockit/jmockit1

JavaDoc
https://repo1.maven.org/maven2/org/jmockit/jmockit/1.48/jmockit-1.48-javadoc.jar

検証環境
java version "1.8.0_202"
Eclipse IDE for Enterprise Java Developers.
Version: 2019-03 (4.11.0)
Build id: 20190314-1200
JUnit4

jmockitとは

xUnitを使用して単体テストを行う場合、依存する部品が問題になってテストが困難な場合があります。
image.png

たとえば、以下のようなケースです。
・依存する部品で任意の内容をテスト対象に返すのが困難な場合
 ※たとえばHDDの容量不足というエラーを出力する必要がある試験の場合
・依存する部品を利用すると別の試験で副作用が発生する場合
 ※たとえばデーターベースの特定のテーブルを全て削除するような試験を行う場合
・依存する部品がまだ完成していない場合
 ※たとえばテスト対象のプログラムと依存する部品が並行で開発されている場合。

こういったケースの場合に、依存する部品の代わりにjmockitで作成したメソッドを利用することで単体テストを容易にします。

image.png

jmockitを使用することで、依存する部品の代わりにテストに都合のいい値をテスト対象に渡したり、依存する部品がどのようにテスト対象から呼び出されたかを記録し、検証することが可能になります。

簡単な使用方法

(1)以下からJarをダウンロードしてプロジェクトから参照する。
https://mvnrepository.com/artifact/org.jmockit/jmockit

あるいはMavenの場合は以下をpom.xmlに追加する

<!-- https://mvnrepository.com/artifact/org.jmockit/jmockit -->
<dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.48</version>
    <scope>test</scope>
</dependency>

(2)JUnitのテストケースを追加する

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import mockit.Mock;
import mockit.MockUp;

public class SimpleTest {

    @Test
    public void test() {
        new MockUp<java.lang.Math>() {
            @Mock
            public double random() {
                // 常に2.5を返すrandom()メソッド
                return 2.5;
            }
        };
        assertEquals(2.5, Math.random(), 0.1);
        assertEquals(2.5, Math.random(), 0.1);
    }

}

(3)junit実行時の実行構成にて、VM引数に「-javaagent:jmockit-1.48.jar」を付与して実行する。
image.png

image.png

実行方法の詳細は下記を参照:
http://jmockit.github.io/tutorial/Introduction.html#runningTests

トラブルシュート

initializationErrorが発生する場合

事象
image.png

エラートレース

java.lang.Exception: Method testVerifications should have no parameters
    at org.junit.runners.model.FrameworkMethod.validatePublicVoidNoArg(FrameworkMethod.java:76)
    at org.junit.runners.ParentRunner.validatePublicVoidNoArgMethods(ParentRunner.java:155)
// 略
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)

原因
VM引数に「-javaagent:jmockit-1.48.jar」が付与されていない。

クラスパスの順番が重要?あるいはRunWithを使用する必要があるのか?

ビルドのクラスパスをjmockit→junitの順番で行うことが重要であるという記載がたまにあります。
https://stackoverflow.com/questions/32817982/jmockit-wasnt-properly-initialized?rq=1

おそらく、これは現バージョンでは問題にならない、あるいはVM引数に「-javaagent:jmockit-X.XX.jar」が付与されていないことが原因と考えられます。

また、ビルドのクラスパスの順番の別解に、「@RunWith(JMockit.class)」を使用するという方法があるらしいですが、少なくとも1.48時点では、この属性は存在しません。

https://github.com/jmockit/jmockit1/issues/554

jmockitの使い方

Mocking

Mocking はテスト対象のクラスをその依存関係(の一部)から分離するメカニズムを提供します。
モック化されたインスタンスを作成するには@Mocked/@Injectable/@Capturingアノテーションを使用します。
モック化されたインスタンスはExpectationsで期待する動作を設定したり、Verificationsでモック化されたインスタンスがどのように実行されたかを検証可能です。

@Mockedアノテーション

テストケースのメソッドのパラメータまたはテストケースのクラスのフィールドとして@Mockedアノテーションを使用してモック化を行うことが可能です。@Mockedアノテーションを使用した場合、それを使用するテストの期間は同じ型のインスタンスは全てモック化されます。

なお、プリミティブ型と配列型を除き、任意の型をモック化可能です。

では以下のクラスをモックオブジェクトとして使用する方法を考えてみます。

テスト対象

package SampleProject;

public class Hoge001 {
    public int hoge(int x, int y) {
        return x + y;
    }
    public String hoge(String x) {
        return "test" + x;
    }
}

Mockedの使用例

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge001;
import mockit.Expectations;
import mockit.Mocked;

public class Test001 {
    // モックを使用しない場合...
    @Test
    public void test0() {
        Hoge001 hoge = new Hoge001();
        assertEquals(11, hoge.hoge(5,6));
        assertEquals("testxxx", hoge.hoge("xxx"));
    }

    // テストメソッドのパラメータとして指定することで、モック化されたインスタンスを作成できます
    @Test
    public void test1(@Mocked Hoge001 mock) {
        new Expectations() {{
            // hogeがx=5, y = 6で呼ばれたら1回目は99を返す
            mock.hoge(5,6);
            result  = 99;
        }};
        // Expectationsでメソッドのresultを指定した場合は、その値が取得される
        assertEquals(99, mock.hoge(5,6));
        // Expectationsでメソッドのresultを指定されていない場合は、初期値(null)となる
        assertEquals(null, mock.hoge("xxx"));

        // @Mockedを使用した場合、そのテストの期間は、すべての該当のインスタンスがモック化される
        Hoge001 hoge = new Hoge001();
        assertEquals(99, hoge.hoge(5,6));
        assertEquals(null, hoge.hoge("xxx"));

    }
}

test0()はモック化しない場合のテストケースになっており、test1()はパラメータに@Mockedを使用したテストケースになっています。
test1()のテストケースの間は、Hoge001のインスタンスは全てモック化されてたインスタンスとなります。

テストケースで直接作成されていない場合もモックになることを確認

テストケース内で直接インスタンスを作成していない場合もモック化されることを以下のテストで確認します。

テスト対象
Hoge001のインスタンスを作成して利用するHoge002をテスト対象とします。

package SampleProject;

public class Hoge002 {
    public int test(int x , int y) {
        Hoge001 hoge1 = new Hoge001();
        return hoge1.hoge(x*2, y*2);
    }
}

テストコード

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge001;
import SampleProject.Hoge002;
import mockit.Expectations;
import mockit.Mocked;

public class Test001_3 {

    // テストメソッドのパラメータとして指定することで、モック化されたインスタンスを作成できます
    @Test
    public void test1(@Mocked Hoge001 mock) {
        new Expectations() {{
            // hogeがx=10, y = 12で呼ばれたら1回目は99を返す
            mock.hoge(10,12);
            result  = 99;
        }};
        Hoge002 hoge2 = new Hoge002();
        assertEquals(99, hoge2.test(5,6));
    }
}

このテストケースを実行すると、Hoge002で作成したHoge001がモック化されているものであると確認できます。

クラスのフィールドに@Mockedを使用したケース

クラスのフィールドに@Mockedを使用した場合、クラスのテスト全てで対象のクラスがモック化されます。

テストコード

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge001;
import SampleProject.Hoge002;
import mockit.Expectations;
import mockit.Mocked;

public class Test001_2 {

    // テストクラスのフィールドとして指定することで、モック化されたインスタンスをそれぞれのテストケースで利用できます
    @Mocked
    private Hoge001 fieldMocked;
    @Test
    public void test1() {
        new Expectations() {{
            fieldMocked.hoge(anyInt, anyInt);
            result = 100;
        }};
        assertEquals(100, fieldMocked.hoge(1,2));
    }
    @Test
    public void test2() {
        new Expectations() {{
            // hogeがx=10, y = 12で呼ばれたら1回目は99を返す
            fieldMocked.hoge(10,12);
            result  = 99;
        }};
        Hoge002 hoge2 = new Hoge002();
        assertEquals(99, hoge2.test(5,6));
    }
}

カスケードされたモック

多くの異なるオブジェクトを使用して提供される機能があるとします。たとえば「obj1.getObj2(...).getYetAnotherObj().doSomething(...)」のような呼び出しがあることは珍しくありません。
この場合のモックの例を見てみましょう。

以下の例ではmock.getDepend1().output()といったオブジェクトを返すメソッドにおいてモック化がされるか確認するコードになっています。

テスト対象のクラス

package SampleProject;

public class Depend001 {
    private String prefix;
    public Depend001(String p) {
        this.prefix = p;
    }
    public String output(String msg) {
        return this.prefix + msg;
    }
}
package SampleProject;

public class Hoge003 {
    private Depend001 d1;
    public Depend001 d2;
    public Hoge003() {

    }

    public Hoge003(Depend001 depend1, Depend001 depend2) {
        this.d1 = depend1;
        this.d2 = depend2;
    }

    public String output() {
        String ret = "";
        ret = ret + this.d1.output("test1") + "\n";
        ret = ret + this.d2.output("test2") + "\n";
        return ret;
    }
    public Depend001 getDepend1() {
        return this.d1;
    }
}

テストコード

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge003;
import mockit.Expectations;
import mockit.Mocked;

public class Test002 {
    @Test
    public void test1(@Mocked Hoge003 mock) {
        new Expectations() {{
            mock.getDepend1().output(anyString);
            result  = "abcde";
        }};
        assertEquals("abcde", mock.getDepend1().output("abc"));
    }

}

上記のサンプルのように、枝葉のDepend001を明示的にモック化しなくても、大元のHoge003クラスをモック化することで目的のメソッドの期待する動作を変更することが確認できました。

@Injectableアノテーション

@Mockedアノテーションと同様にモック化を行うためのアノテーションですが、@Mockedアノテーションとの違いはモックを1つのインスタンスに制限することです。
また、@Testedアノテーションとと組み合わせることで、テスト対象オブジェクトへの自動注入に使用することができます。

@Mockedアノテーションとの違い

@Mockedアノテーションと@Injectableアノテーションの違いを確認するために、@Mockedアノテーションで使用したテストコードを@Injectableに変更して確認をしてみます。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge001;
import mockit.Expectations;
import mockit.Injectable;

public class Test004 {

    // テストメソッドのパラメータとして指定することで、モック化されたインスタンスを作成できます
    @Test
    public void test1(@Injectable Hoge001 mock) {
        new Expectations() {{
            // hogeがx=5, y = 6で呼ばれたら1回目は99を返す
            mock.hoge(5,6);
            result  = 99;
        }};
        // Expectationsでメソッドのresultを指定した場合は、その値が取得される
        assertEquals(99, mock.hoge(5,6));
        // Expectationsでメソッドのresultを指定されていない場合は、初期値(null)となる
        assertEquals(null, mock.hoge("xxx"));

        // @Mockedを使用した場合とことなり、すべての該当のインスタンスがモック化されるわけではない。
        Hoge001 hoge = new Hoge001();
        assertEquals(11, hoge.hoge(5,6));
        assertEquals("testxxx", hoge.hoge("xxx"));

    }
}

@Mockedアノテーションを使用した場合、テストの期間中は対象のクラスのインスタンスを作成するたびにモック化されたものとなりましたが、@Injectableを使用することでモック化されるインスタンスを1つに制限していることが確認できます。

@Testedアノテーションに対する注入

@Testedアノテーションで指定したテスト対象のオブジェクトにモックを注入するサンプルを確認してみます。
@Testedで指定したテスト対象のオブジェクトのコンストラクタの引数に注入する方法と、テスト対象のフィールドに注入する方法があります。

コンストラクタの引数に注入する方法
以下はHoge003(Depend001 depend1, Depend001 depend2) のコンストラクタの引数であるdepend1とdepend2を指定する例です。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Depend001;
import SampleProject.Hoge003;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;

public class Test003 {
    @Tested
    Hoge003 target;

    @Injectable
    Depend001 depend1;

    @Injectable
    Depend001 depend2;

    @Test
    public void test1() {
        new Expectations() {{
            depend1.output(anyString);
            result  = "abcde";
            depend2.output(anyString);
            result  = "xxxxx";
        }};
        assertEquals("abcde\nxxxxx\n", target.output());
    }

}

フィールドに注入する方法
以下はHoge003オブジェクトのd1,d2フィールドに注入するサンプルになります。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Depend001;
import SampleProject.Hoge003;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;

public class Test003B {
    @Tested
    Hoge003 target;

    @Injectable
    Depend001 d1;

    @Injectable
    Depend001 d2;

    @Test
    public void test1() {
        new Expectations() {{
            d1.output(anyString);
            result  = "abcde";
            d2.output(anyString);
            result  = "xxxxx";
        }};
        assertEquals("abcde\nxxxxx\n", target.output());
    }
}
プリミティブ型のフィールドやコンストラクトに注入を行う方法

@Injectableアノテーションのvalue要素を使用することで、@Testedアノテーションで指定したテスト対象のプリミティブ型のフィールドやコンストラクトに注入を行うことが可能です。

package jmockittest;

import org.junit.Test;

import SampleProject.Depend001;
import mockit.Injectable;
import mockit.Tested;

public class Test005 {
    @Tested
    Depend001 tested;

    @Test
    public void test1(@Injectable("abc") String p) {
        // 以下を出力
        // abcaaa
        System.out.println(tested.output("aaa"));
    }
    @Test
    public void test2(@Injectable("abc") String prefix) {
        // 以下を出力
        // abcbbb
        System.out.println(tested.output("bbb"));
    }
}

test1はテスト対象のオブジェクトのコンストラクタ引数pに指定して注入をおこなっており、test2はテスト対象のオブジェクトのフィールドprefixを指定して注入をしています。

@Testedアノテーションのオプション要素
タイプ 名前 オプションの要素と説明 規定値
boolean availableDuringSetup テストされたクラスが、テストセットアップメソッド(つまり、@ Beforeまたは@BeforeMethodとして注釈が付けられたメソッド)の実行前にインスタンス化および初期化されるか、それらの後に初期化されるかを示します。 false
boolean fullyInitialized 注入に適格なテスト済みオブジェクトの各非最終フィールドに値を割り当てる必要があることを示します。Springを使用している場合での使いどころは次のページで記載されています。https://stackoverflow.com/questions/25856210/injecting-only-some-properties-mocking-others false
boolean global テスト対象クラスの単一の名前付きインスタンスを作成して、テスト実行全体で使用するかどうかを示します。 false
String value テストするフィールド/パラメーターのタイプがストリング、プリミティブまたはラッパータイプ、数値タイプ、または列挙タイプの場合、リテラル値を指定します。 ""

availableDuringSetupとglobalを検証するテストコード

package jmockittest;

import org.junit.Before;
import org.junit.Test;

import SampleProject.Hoge001;
import mockit.Tested;

public class Test007 {
    @Tested(availableDuringSetup=true, global=true)
    Hoge001 tested;

    @Before
    public void before()
    {
        // null以外 availableDuringSetupがfalseだとnullになる
        System.out.println("before:" + tested);
    }

    @Test
    public void test1() {
        // null以外
        System.out.println("test1:" + tested);
    }
    @Test
    public void test2() {
        // null以外 test1と同じオブジェクトが使われていることが確認できる
        System.out.println("test2:" + tested);
    }
}

@Capturingアノテーション

@Capturingアノテーションを使用することで既定クラスやインターフェイスに対してモック化を行いうことが可能です。
下記のサンプルは、個々の実装クラスではなく、インターフェイスに対してモック化されたメソッドを作成するサンプルになっています。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import mockit.Capturing;
import mockit.Expectations;

public class Test006 {
    public interface Service { int doSomething(); }
    final class ServiceImpl implements Service { public int doSomething() { return 1; } }

    public final class TestedUnit {
       private final Service service1 = new ServiceImpl();
       private final Service service2 = new Service() { public int doSomething() { return 2; } };

       public int businessOperation() {
          return service1.doSomething() + service2.doSomething();
       }
    }

    // インターフェイスや既定クラスに対してモックを作成する
    @Test
    public void test1(@Capturing Service anyService) {
          new Expectations() {{ anyService.doSomething(); returns(3, 4); }};

          int result = new TestedUnit().businessOperation();

          assertEquals(7, result);
    }
}

Expectations

Expectationsは特定のテストに関連するモックオブジェクトに対して期待する動作を設定します。

期待値を設定する

Expectations中にはモックオブジェクトのメソッドをどのパラメータを指定したら、どの値を返すかを指定できます。
下記の例では「String hoge(String)」と「int hoge(int, int)」メソッドを実行した際にどのような値を返すかを設定した例になります。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import SampleProject.Hoge001;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;

public class Test008 {

    // Expectationsでメソッドのresultを指定した場合は、その値が取得されることを確認
    @Test
    public void test1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge("test");
            result = "abcde";

            mock.hoge(5,6);
            result  = 99;
            result = 100;
            result = 101;

        }};
        // mock.hoge("test")を実行した際の期待値を取得
        assertEquals("abcde", mock.hoge("test"));

        // mock.hoge(5,6)を実行した際の期待値を取得
        // Expectationsで設定した1つめの値が取得
        assertEquals(99, mock.hoge(5,6));
        // Expectationsで設定した2つめの値が取得
        assertEquals(100, mock.hoge(5,6));
        // Expectationsで設定した3つめの値が取得
        assertEquals(101, mock.hoge(5,6));
        // Expectationsで設定した最後の値が取得
        assertEquals(101, mock.hoge(5,6));
        // Expectationsで設定した最後の値が取得
        assertEquals(101, mock.hoge(5,6));

        // 引数が異なる場合は初期値となる
        assertEquals(0, mock.hoge(7,6));
    }
}
returnsで記載する例

複数のresultはreturnsで以下のように、まとめて記載することも可能です。

        new Expectations() {{
            mock.hoge("test");
            result = "abcde";

            mock.hoge(5,6);
            returns(99, 100, 101);

        }};

引数の柔軟な指定方法

先の例では特定の引数の値を受け付けたときのみ戻り値を返すようにしていましたが、any~やwith~を引数に指定することで引数の値を柔軟に設定することができます。

anyフィールドの使用

Expectationsには任意のあたいをあらわすいくつかのanyフィールドが存在します。

type name
Object any
Boolean anyBoolean
Byte anyByte
Character anyChar
Double anyDouble
Float anyFloat
Integer anyInt
Long anyLong
Short anyShort
String anyString

anyフィールドを利用した例は以下のようになります。

    @Test
    public void test1_1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyString);
            result = "abcde";

            mock.hoge(anyInt, anyInt);
            result  = 99;

        }};
        // mock.hoge("test")を実行した際の期待値を取得
        assertEquals("abcde", mock.hoge("test"));
        assertEquals("abcde", mock.hoge("hogehoget"));

        // mock.hoge(5,6)を実行した際の期待値を取得
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
    }
固定の引数の値と任意の引数の値を組み合わせる

固定の引数の値と任意の引数の値を組み合わせることもできます。下記の例ではhoge(5,6)の場合は10を返して、それ以外の場合は99を返すモックメソッドを作ります。

    @Test
    public void test1_2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(5,6);
            result = 10;

            mock.hoge(anyInt, anyInt);
            result  = 99;

        }};

        // mock.hoge(5,6)を実行した際の期待値を取得
        assertEquals(10, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
    }

任意の引数の値と組み合わせる場合は、固定値を先に記載してください。

withメソッドの利用

with~メソッドを使用することで、Expectationsで指定したモックメソッドとして一致するかどうかの判断を柔軟におこなうことが可能です。

メソッド 説明
with(Delegate<? super T> objectWithDelegateMethod) 引数が一致したかどうかをデリゲートメソッドを用いて判定します。デリゲートメソッドの戻り値がtrueである場合、一致したことを意味します
withEqual(T arg) 指定された値がモック実行時の引数に一致するか確認します。通常は、この方法は使用しないで、目的の引数の値を渡す方法を使用してください。
withEqual(double value, double delta) deltaで指定した値に近い場合に一致したものとします
withEqual(float value, double delta) deltaで指定した値に近い場合に一致したものとします
withAny(T arg) anyBoolean、anyByte、anyChar、anyDouble、anyFloat、anyInt、anyLong、anyShort、anyString、anyを使用することを検討してください。
withNotEqual(T arg) 指定した値以外の場合、一致したものとします
withNotNull() 指定した値がNULL以外の場合、一致したものとします
withNull() 指定した値がNULLの場合、一致したものとします
withInstanceOf(Class argClass) 指定されたクラスのインスタンスであることを確認します。
withInstanceLike(T object) 指定されたオブジェクトと同じクラスのインスタンスであることを確認します。withInstanceOf(object.getClass()) と同等になります
withSameInstance(T object) まったく同じインスタンスであることを確認します
withPrefix(T text) 特定の文字が含まれた場合、一致したものとみなします
withSubstring(T text) 先頭が指定した文字に一致した場合、一致したものとみなします
withSuffix(T text) 末尾が指定した文字に一致した場合、一致したものとみなします
withMatch(T regex) 正規表現で一致するかどうかを指定できます
withでデリゲートメソッドを使用した例

withでデリゲートメソッドを使用することでメソッドでモックの引数が一致するかどうかの判定を行うことが可能です。

    @Test
    public void test1_4(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(with(new Delegate<Integer>() {
                @Mock boolean validate(int value) {
                    return value >= 0;
                }
            }),anyInt);
            result = 99;
        }};

        // xがプラスなのでモックで設定した値に一致する
        assertEquals(99, mock.hoge(1,2));
        // xがマイナスなのでモックで設定した値に一致しない
        assertEquals(0, mock.hoge(-1,2));
    }
withEqualを使用した例

基本的にwithEqualを使用するよりリテラルをそのまま使用した方がいいです。しかし、浮動小数点を使用する場合はwithEqualを使用した方がいいでしょう。

    class testWithEqual {
        int test1(double v) {
            return 1000;
        }
        int test2(int v) {
            return 2000;
        }
    }

    @Test
    public void test_withEqual1(@Mocked testWithEqual mock) {
        new Expectations() {{
            mock.test2(withEqual(100));
            result = 99;
        }};
        // 一致する mock.test2(100)と同じ
        assertEquals(99, mock.test2(100));
        // 一致しない
        assertEquals(0, mock.test2(101));
    }
    @Test
    public void test_withEqual2(@Mocked testWithEqual mock) {
        new Expectations() {{
            mock.test1(withEqual(100, 1));
            result = 99;
        }};
        // 一致する
        assertEquals(99, mock.test1(100.0));
        assertEquals(99, mock.test1(101.0));
        assertEquals(99, mock.test1(99.0));
        // 一致しない
        assertEquals(0, mock.test1(101.1));
        assertEquals(0, mock.test1(98.99));
    }
withInstanceOf,withInstanceOf,withSameInstanceの例

withInstanceOf,withInstanceOf,withSameInstanceを使用することで特定のインスタンスと一致するかどうかを確認することが可能です。

    class classA {
    }
    class classB {
    }
    class classX  {
        public int method1(Object obj) {
            return 999;
        }
    }

    @Test
    public void test_withInst1(@Mocked classX mock) {
        new Expectations() {{
            mock.method1(withInstanceOf(classA.class));
            result = 99;
        }};
        // 一致する
        {
            classA obj = new classA();
            assertEquals(99, mock.method1(obj));
        }

        // 一致しない
        {
            classB obj = new classB();
            assertEquals(0, mock.method1(obj));
        }
    }

    @Test
    public void test_withInst2(@Mocked classX mock) {
        new Expectations() {{
            classA objA = new classA();
            mock.method1(withInstanceLike(objA));
            result = 99;
        }};
        // 一致する
        {
            classA obj = new classA();
            assertEquals(99, mock.method1(obj));
        }

        // 一致しない
        {
            classB obj = new classB();
            assertEquals(0, mock.method1(obj));
        }
    }
    @Test
    public void test_withInst3(@Mocked classX mock) {
        classA obj1 = new classA();
        new Expectations() {{
            mock.method1(withSameInstance(obj1));
            result = 99;
        }};
        // 一致する
        {
            assertEquals(99, mock.method1(obj1));
        }

        // 一致しない
        {
            classA obj2 = new classA();
            assertEquals(0, mock.method1(obj2));
        }
    }
withPrefix,withSubstring,withSuffix,withMatchの例

withPrefix,withSubstring,withSuffix,withMatchを用いることで文字列の一部が一致するかどうかを調べることが可能です。

    @Test
    public void test_withString1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(withPrefix("abc"));
            result = "test";
        }};

        // 以下は一致する
        assertEquals("test", mock.hoge("abc"));
        assertEquals("test", mock.hoge("abcAA"));

        // 以下は一致しない
        assertEquals(null, mock.hoge("AAabc"));
        assertEquals(null, mock.hoge("AabcA"));
        assertEquals(null, mock.hoge("xx"));
    }

    @Test
    public void test_withString2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(withSuffix("abc"));
            result = "test";
        }};

        // 以下は一致する
        assertEquals("test", mock.hoge("abc"));
        assertEquals("test", mock.hoge("AAabc"));

        // 以下は一致しない
        assertEquals(null, mock.hoge("abcAA"));
        assertEquals(null, mock.hoge("AabcA"));
        assertEquals(null, mock.hoge("xx"));
    }
    @Test
    public void test_withString3(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(withSubstring("abc"));
            result = "test";
        }};

        // 以下は一致する
        assertEquals("test", mock.hoge("abc"));
        assertEquals("test", mock.hoge("abcAA"));
        assertEquals("test", mock.hoge("AAabc"));
        assertEquals("test", mock.hoge("AabcA"));

        // 以下は一致しない
        assertEquals(null, mock.hoge("xx"));
    }
    @Test
    public void test_withString4(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(withMatch("[0-9]+"));
            result = "test";
        }};

        // 以下は一致する
        assertEquals("test", mock.hoge("1234"));

        // 以下は一致しない
        assertEquals(null, mock.hoge("xxx"));
    }

インスタンスの作成のされ方でモックメソッドを分ける方法

Expectationsにてインスタンスの作成のされ方でモックメソッドを分けることが可能です。
以下の例では「new TestA(10)」を実行して作成したインスタンスにのみモックメソッドを適用するサンプルを示します。

    class TestA {
        public TestA(int x) {

        }
        public int hoge() {
            return 99999;
        }
    }

    @Test
    public void test8(@Mocked TestA mock) {
        new Expectations() {{
            TestA t1 = new TestA(10);
            t1.hoge();
            result = 10;

        }};

        {
            TestA obj = new TestA(10);
            assertEquals(10, obj.hoge());
        }
        {
            TestA obj = new TestA(99);
            assertEquals(0, obj.hoge());
        }
    }

例外を発生させる方法

モックメソッドの処理中に例外を発生させることができます。
以下の例ではhoge()メソッド実行中にIllegalArgumentExceptionを発生させます。

    // Expectationsでメソッドの例外を返す例
    @Test
    public void test2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(5,6);
            result = 99;
            result = new IllegalArgumentException("test");
        }};
        // Expectationsで設定した1つめの値が取得
        assertEquals(99, mock.hoge(5,6));
        try {
            // Expectationsで設定した2つめの値が取得
            mock.hoge(5,6);
            fail();

        } catch (IllegalArgumentException ex) {
            assertEquals("test", ex.getMessage());
        }
    }

実行回数を確認する

Expectationsでtimes,maxTImes,minTimesを指定することでメソッドの実行回数を指定することが可能です。

Field Description
tiems 実行中に何回メソッドが呼び出されるかを指定します。これと異なる回数、呼び出された場合、エラーとなります。
maxTimes 呼び出されるメソッドの最大回数を指定します。これを上回る回数、呼び出された場合エラーとなります。
minTimes 呼び出されるメソッドの最小回数を指定します。これを下回る回数しか呼び出されない場合エラーとなります。
    @Test
    public void test4_1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            times = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
    }
    // この試験はMissing 2 invocations  が発生してエラーとなります
    @Test
    public void test4_2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            times = 3;

        }};
        assertEquals(99, mock.hoge(3,6));
    }

    // この試験はUnexpected invocation が発生してエラーとなります
    @Test
    public void test4_3(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            times = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
        assertEquals(99, mock.hoge(3,6));
    }

    @Test
    public void test5_1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            minTimes = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
    }

    // この試験はMissing 2 invocations  が発生してエラーとなります
    @Test
    public void test5_2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            minTimes = 3;

        }};
        assertEquals(99, mock.hoge(3,6));
    }
    @Test
    public void test5_3(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            minTimes = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
        assertEquals(99, mock.hoge(3,6));
    }
    @Test
    public void test6_1(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            maxTimes = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
    }
    @Test
    public void test6_2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            maxTimes = 3;

        }};
        assertEquals(99, mock.hoge(3,6));
    }

    // この試験はUnexpected invocation が発生してエラーとなります
    @Test
    public void test6_3(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt, anyInt);
            result  = 99;
            maxTimes = 3;

        }};
        assertEquals(99, mock.hoge(5,6));
        assertEquals(99, mock.hoge(99,1234));
        assertEquals(99, mock.hoge(3,6));
        assertEquals(99, mock.hoge(3,6));
    }

Delegateを利用したresultのカスタム指定

モックメソッド実行時に引数に基づいて、モックで返す結果を変更したい場合はDeglegateを使用します。
下記の例では入力引数の2倍を加えた値を返すモックメソッドを作成しています。

    @Test
    public void test7(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt,anyInt);
            result= new Delegate<Integer>() {
                @SuppressWarnings("unused")
                int aDelegateMethod(int x, int y) {
                    return x * 2 + y * 2;

                }
            };
        }};
        // Expectationsで設定した1つめの値が取得
        assertEquals(22, mock.hoge(5,6));
    }
Invocationの使用

Delegateのメソッドの最初のパラメータとしてInvocationを使用することが可能です。
Invocationは下記のゲッターを提供しています。

メソッド 説明
getInvocationCount() 呼び出し回数
getInvocationIndex() 現在の呼び出しIndexを取得
getInvokedArguments() 呼び出しに使用した引数を取得
getInvokedInstance() 現在の呼び出しのインスタンス。staticメソッドの場合はnullとなる
getInvokedMember() 呼び出しのメソッド/コンストラクタを取得
proceed(Object... replacementArguments) 実際のメソッド/コンストラクタを実行します
    @Test
    public void testDelegate2(@Mocked Hoge001 mock) {
        new Expectations() {{
            mock.hoge(anyInt,anyInt);
            result= new Delegate<Integer>() {
                @SuppressWarnings("unused")
                int aDelegateMethod(Invocation inv ,int x, int y) {
                    System.out.println("--------------------------------");
                    // 呼び出し回数
                    System.out.format("Invocation getInvocationCount %d \n", inv.getInvocationCount());
                    // 現在の呼び出しのインデックス
                    System.out.format("Invocation getInvocationIndex() %d \n", inv.getInvocationIndex());
                    // 引数を取得
                    System.out.println("getInvokedArguments");
                    for(Object obj : inv.getInvokedArguments()) {
                        System.out.println(obj);
                    }
                    // インスタンスを取得
                    System.out.format("Invocation getInvokedInstance() %s \n", inv.getInvokedInstance().toString());
                    // 実際のメソッドを取得
                    System.out.format("Invocation getInvokedMember() %s \n", inv.getInvokedMember().toString());
                    // 実際のメソッドの実行が可能。
                    System.out.format("Invocation  proceed %s \n", inv.proceed().toString());
                    // 引数を改ざんして実際のメソッドを実行可能
                    System.out.format("Invocation  proceed %s \n", inv.proceed(5,6).toString());
                    return 0;
                }
            };
        }};
        // Expectationsで設定した1つめの値が取得
        Hoge001 a = new Hoge001();
        Hoge001 b = new Hoge001();
        a.hoge(5,6);
        a.hoge(45,63);
        b.hoge(99,100);
    }

上記を実行したコンソールログは以下のようになります。

--------------------------------
Invocation getInvocationCount 1 
Invocation getInvocationIndex() 0 
getInvokedArguments
5
6
Invocation getInvokedInstance() SampleProject.Hoge001@2a2d45ba 
Invocation getInvokedMember() public int SampleProject.Hoge001.hoge(int,int) 
Invocation  proceed 11 
Invocation  proceed 11 
--------------------------------
Invocation getInvocationCount 2 
Invocation getInvocationIndex() 1 
getInvokedArguments
45
63
Invocation getInvokedInstance() SampleProject.Hoge001@2a2d45ba 
Invocation getInvokedMember() public int SampleProject.Hoge001.hoge(int,int) 
Invocation  proceed 108 
Invocation  proceed 11 
--------------------------------
Invocation getInvocationCount 3 
Invocation getInvocationIndex() 2 
getInvokedArguments
99
100
Invocation getInvokedInstance() SampleProject.Hoge001@675d3402 
Invocation getInvokedMember() public int SampleProject.Hoge001.hoge(int,int) 
Invocation  proceed 199 
Invocation  proceed 11 

オブジェクトの一部をモック化する

すべてのメソッドでなく一部のみをモック化するには以下のようにExpectationsにオブジェクトを渡します。

    @Test
    public void test10() {
        Hoge001 hoge = new Hoge001();
        new Expectations(hoge) {{
            hoge.hoge(5,6);
            result = 99;
        }};
        // モックの結果を返す
        assertEquals(99, hoge.hoge(5,6));

        // 実際のメソッドを実行する
        assertEquals(3, hoge.hoge(1,2));
        assertEquals("testabc", hoge.hoge("abc"));

    }

Verifications

Verifications、VerificationsInOrder、FullVerificationsを使用することでモックオブジェクトがどのように呼び出されたかを明示的に検証することが可能です。

    @Test
    public void test_v1(@Mocked Hoge001 mock) {
        mock.hoge(1,2);
        mock.hoge(2,3);
        mock.hoge(4,5);

        //
        new Verifications() {{
            mock.hoge(anyInt,anyInt);
            times = 3;
            mock.hoge(anyString);
            times = 0;
        }};
        // Verificationsは順番の違いや余計な呼び出しについては合格と見なします
        new Verifications() {{
            mock.hoge(4,5);
            mock.hoge(1,2);
        }};
    }
    @Test
    public void test_v2(@Mocked Hoge001 mock) {
        mock.hoge(1,2);
        mock.hoge(2,3);
        mock.hoge(4,5);

        // VerificationsInOrderは順番が異なるとエラーになります
        /*
        new VerificationsInOrder() {{
            mock.hoge(4,5);
            mock.hoge(1,2);
        }};
        */
        new VerificationsInOrder() {{
            mock.hoge(1,2);
            mock.hoge(4,5);
        }};
    }
    @Test
    public void test_v3(@Mocked Hoge001 mock) {
        mock.hoge(1,2);
        mock.hoge(2,3);
        mock.hoge(4,5);

        // FullVerificationsでは余計な呼び出しがされているとエラーになります
        /*
        new FullVerifications() {{
            mock.hoge(1,2);
            mock.hoge(4,5);
        }};
        */
        new FullVerifications() {{
            mock.hoge(1,2);
            mock.hoge(2,3);
            mock.hoge(4,5);
        }};
        // 順番はことなっていても合格となります
        new FullVerifications() {{
            mock.hoge(4,5);
            mock.hoge(2,3);
            mock.hoge(1,2);
        }};
    }
withCaptureを使用した検証の例

withCaptureでどのようなパラメータが与えられたインスタンスをListで取得できます。

    // withCaptureでパラメータを確認する例
    @Test
    public void test_v4(@Mocked Hoge001 mock) {
        mock.hoge(1,2);
        mock.hoge(2,3);
        mock.hoge(4,5);

        //
        new Verifications() {{
            List<Integer> argXList = new ArrayList<Integer>();
            List<Integer> argYList = new ArrayList<Integer>();
            mock.hoge(withCapture(argXList),withCapture(argYList));
            assertEquals(3, argXList.size());
            assertEquals(3, argYList.size());

            assertEquals(1, (int)argXList.get(0));
            assertEquals(2, (int)argXList.get(1));
            assertEquals(4, (int)argXList.get(2));

            assertEquals(2, (int)argYList.get(0));
            assertEquals(3, (int)argYList.get(1));
            assertEquals(5, (int)argYList.get(2));

        }};
    }

    // withCaptureでインスタンスの作成を確認する例
    class Person {
        public Person(String name , int age) {
        }
    }
    @Test
    public void test_v5(@Mocked Person mockPerson) {
        new Person("Joe", 10);
        new Person("Sara", 15);
        new Person("Jack", 99);

        //
        new Verifications() {{
            List<Person> created = withCapture(new Person(anyString, anyInt));
            assertEquals(3, created.size());

        }};
    }

Faking API

Faking APIはFakeの実装の作成のサポートを提供します。 通常、Fakeは、Fakeされるクラス内のいくつかのメソッドやコンストラクタをターゲットにし、他のほとんどのメソッドやコンストラクタは変更されません。

public/protectedメソッドのFake

以下の例ではProc1とProc2が存在するクラスのProc1のみFakeしている例です。

package jmockittest;

import static org.junit.Assert.*;

import org.junit.Test;

import mockit.Mock;
import mockit.MockUp;

public class FakeTest {
    class ClassA {
        protected String Proc1() {
            return "...Proc1";
        }
        public String Proc2() {
            return  "Proc2:" + this.Proc1();
        }

    }
    @Test
    public void test1() {
        new MockUp<ClassA>() {

            @Mock
            String Proc1() {
                System.out.print("Proc1");
                return "xxx";
            }
        };
        ClassA obj = new ClassA();
        assertEquals("Proc2:xxx", obj.Proc2());
    }
}

Private メソッドのFake

1.48では無理。以下のようなエラーが出る。

java.lang.IllegalArgumentException: Unsupported fake for private method ClassA#Proc1()Ljava/lang/String; found
    at jmockittest.FakeTest$1.<init>(FakeTest.java:22)
    at jmockittest.FakeTest.test1(FakeTest.java:22)

おそらく、以前はできていてできなくなった模様。
https://github.com/jmockit/jmockit1/issues/605

Staticメソッドの例

StaticメソッドのFakeは可能です。
下記の例はjava.lang.Math.randomで常に固定値を返す例になります。

    @Test
    public void test() {
        new MockUp<java.lang.Math>() {
            @Mock
            public double random() {
                // 常に2.5を返すrandom()メソッド
                return 2.5;
            }
        };
        assertEquals(2.5, Math.random(), 0.1);
        assertEquals(2.5, Math.random(), 0.1);
    }

finailが指定されているメソッドのFakeは作成できるか?

作成可能でした。

    class ClassB {
        final protected String Proc1() {
            return "...Proc1";
        }
        public String Proc2() {
            return  "Proc2:" + this.Proc1();
        }

    }
    @Test
    public void test3() {
        new MockUp<ClassB>() {

            @Mock
            String Proc1() {
                System.out.print("Proc1");
                return "xxx";
            }
        };
        ClassB obj = new ClassB();
        assertEquals("Proc2:xxx", obj.Proc2());
    }

Fakeクラス内の特別なメソッド

Fakeクラス内で特別なメソッドとして\$init,\$clinit,\$adviceが存在します。
\$initはコンストラクタをターゲットとしています。
\$clinitは静的初期化子を対象としています。
\$adviceはターゲットのクラスの全てのメソッドをあらわします。

テスト対象

ClassC.java

package SampleProject;

public class ClassC {
    public static int sx;
    private int x;
    static {
        sx = 999;
    }
    public ClassC(int x) {
        this.x = x;
    }

    public String Proc1() {
        System.out.format("ClassC Proc1 %d %d\n", sx, this.x);
        return "...Proc1";
    }

}

テストコード

    @Test
    public void test4() {
        new MockUp<ClassC>() {
            @Mock
            void $clinit() {
                // ClassCのスタティック初期化が動いていないことを確認
                assertEquals(0, ClassC.sx);
            }

            @Mock
            void $init(int x) {
                assertEquals(100, x);
            }

            @Mock
            Object $advice(Invocation inv) {
                return "test";
            }
        };
        ClassC obj = new ClassC(100);
        assertEquals("test", obj.Proc1());

    }

Fakeメソッドの特別なパラメータ

Fakeメソッドの最初のパラメータにInvocationを使用することが可能です。
これを使用して現在時刻にたいして固定値を返すサンプルを下記に示します。

    @Test
    public void testTime() {
        Calendar nowCalendar = Calendar.getInstance();
        System.out.println("現在日時 : " + nowCalendar.getTime());
        new MockUp<Calendar>() {
            @Mock
            Calendar getInstance(Invocation inv) {
                Calendar cal = inv.proceed();
                cal.set(Calendar.YEAR, 2018);
                cal.set(Calendar.MONTH, 0);
                cal.set(Calendar.DAY_OF_MONTH, 1);
                cal.set(Calendar.HOUR, 22);
                cal.set(Calendar.MINUTE, 32);
                cal.set(Calendar.SECOND, 12);
                cal.set(Calendar.MILLISECOND, 512);
                return cal;
            }
            @Mock
            Calendar getInstance(Invocation inv, TimeZone zone, Locale aLocale) {
                Calendar cal = inv.proceed();
                cal.set(Calendar.YEAR, 2018);
                cal.set(Calendar.MONTH, 0);
                cal.set(Calendar.DAY_OF_MONTH, 1);
                cal.set(Calendar.HOUR, 22);
                cal.set(Calendar.MINUTE, 32);
                cal.set(Calendar.SECOND, 12);
                cal.set(Calendar.MILLISECOND, 512);
                return cal;
            }
        };
        final Calendar c = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        assertEquals("20180102103212512", sdf.format(c.getTime()));

    }

カバレッジの計測

実行構成でVMの引数を与えることでカバレッジの計測結果を出力することができます。

-Dcoverage-output=html -Dcoverage-srcDirs=..\SampleProject\src

image.png

image.png

image.png

その他引数は下記を参照してください。
http://jmockit.github.io/tutorial/CodeCoverage.html

アンドキュメントな動作として「-Dcoverage-output=xml」とするとXMLを出力するようです。

まとめ

ここまで調べてなんですが、GitHub上のprivate methodまわりの議論や更新履歴の廃止履歴を観るに、完全で完璧な理想的なテスト環境にいない場合、ちょっと使うのにリスクがある感じがします。

なお、以下でpowermock+mockitoも調べてみました。

powermock-mockito2-2.0.2を使ってみる
https://needtec.sakura.ne.jp/wod07672/?p=9156

自動テストを撲滅させる方法

はじめに

過去に色々と自動テストを導入する方法とかを書きました。とはいえ、なかなか進めるのが面倒な場合がおおいです。実際、2019年11月現在、Googleで「TDD テストファースト」と日本語検索をすると、ネガティブな記事がトップの方に来てしまうのが現実です。

そこで、今回は視点を変えて、大勢側に迎合して自動テストをぶっ潰す方法を考えてみることにしたいとおもいます。

自動テストをやりたいとかいう反逆者は以下の記事でも読んでください。

20年物のC言語で作られたシステムのテスト工程を改善しようとした話
https://needtec.sakura.ne.jp/wod07672/?p=9159

意識が高くないVisualStudioを使用した単体テストの自動化
https://needtec.sakura.ne.jp/wod07672/?p=9213

一度、痛い目にあわせる戦略

実は所属組織でテストの自動化の文化をつぶすのに一番いいやり方は、自動テストをさせないことではありません。一度テストを自動化させるプロジェクトを行って大炎上させることです。

人間というのは不思議なもので、可能性があるとそれにすがってしまいます。
それにすがらせないためには、一度、最悪の現実を体験させることです。
一度、テスト自動化を試みたプロジェクトを大炎上させれば、二度とやりたいと言い出すやつは出てきませんし、出てきたとしても、拒否する大義名分ができます。

是非、自動テストを叩き潰したい皆さんは、この記事で紹介するテスト自動化の叩き潰し方を学んでいただいて、テストの自動化を失敗に導きましょう。

人員の選定

ソフトウェア開発は人が重要です。
テストの自動化を叩き潰すのに重要な人員の分類方法は以下の通りになります。

image.png

分類ごとの解説

自動化したくない-経験なし

なにも考えずに採用活動した場合、この層が一番厚くなると思います。
ここの層にたいして自動化にたいするネガティブな感情を植え付けていくのが肝要です。
おそらく本記事のやり方を参考にプロジェクトを進めていただければ、それは簡単に果たすことが可能でしょう。

自動化したくない-経験あり

その経験は脅威になりうる可能性もあります。
しかしながら、この層の多くは我々の先駆者たる失敗したテストの自動化プロジェクトの生き残りであるため、我々の最大の味方となりうる層です。

会議の場では積極的に過去のできない話から「難しいです」「無理です」「できません」と言った言葉を引き出して、周りのテストの自動化に対するモチベーションを下げていくのに利用しましょう。

この際、「自動化したい―経験あり」の人間は会議に出さないように全力で阻止します。
あいつらがいると「やり方がわるかったんじゃないっすか?こうやってこうすればうまくいきますよ」とか空気の読めないことを言い出しやがります。

自動化したい-経験なし

作業者として配置した場合、そのモチベーションが脅威になることがあります。
しかしながら、テストの自動化は本で読んだり、セミナー受けたりしただけでは、基本的にできません。

この層の中の一番有効な使い方は、経験と考えは浅いが、意識ライジングな人員を選抜して、リーダー層に任命することです。経験がなく意識だけが高い場合、この記事で書いているようなことを無意識でやってくれるので、それだけでテスト自動化撲滅のためのキーマンになりえます。

また、この層と「自動化したい-経験あり」が接触すると化学反応を起こして計画の障害になりうるので、なんとしても接触を防ぎましょう。

自動化したい-経験あり

なんとしても排除しましょう。
やむえずプロジェクトメンバーにいる場合は、発言権をあたえてはいけません。また、自由に動く裁量を極限まで削りましょう。与えるマシンのメモリは4Gを上限として、ソフトウェアのダウンロードは禁止にしてください。
これを怠ると、自分のマシンで継続的インテグレーションの環境つくり始めたりするのでなんとしても防ぎましょう。

なんとしても、この層の「自動化したい」という心をへし折ってやりましょう。

自動テストのつぶし方

計画について

正直、どんな技術者を雇っても計画さえ適切に自動テストを潰す方向に向いていれば確実につぶすことが可能です。

無理な目標を立てる

テストの自動化を導入するセオリーが「有効そうなところの、できるとこから」なので、ここでは、いきなり完璧を目指しましょう。

単体テストのカバレッジは100%を必須とし、やるのが難しいGUIの自動テストやスレッドの自動テストを積極的にやりましょう。重要度の高いところだけとかいう甘えを捨てさせるために優先度は全てが最優先でトリアージをさせないように心がけましょう。

是非、完全で完璧な自動テストを目指しましょう。きっと自滅してくれます。

準備期間は与えない。

準備期間は与えずに自動テストを始めるようにしましょう。
当該プロジェクトにあった自動テストの方法や、共通的なテスト用のユーティリティの作成、参考となるテンプレート的なテストの作成、要員の教育を実施する余裕を削ります。

この際、毎日、何件テストを作って何件消化したかを報告させるようにして遅延が発生したら詰めるようにしましょう。これはコッソリ目先の進捗を犠牲にして共通的なテスト用ユーティリティを作る不届きものを排除するためです。

全員が同時に始める

テストコードの書き方のパターンがなるべくバラバラになって、制御が困難になるように
テストコードを書くタイミングがなるべく同時になるようにします。可能であれば、経験のない人間が最初にテストコードを書き始めるようなスケジュールをひきましょう。そして、経験のある人間がテストコードに着手できるタイミングを可能な限り遅らせます。

これにより、効果的なテストコードの書き方のテンプレート作成を防止し、仮に、作成できたとしても状況を手遅れにすることが可能になります。

新しいぶどう酒は古い革袋に入れる

テストの自動化を行うからといって、従来の管理方法を変えてはいけません。

実装→単体テストのチェックリストの作成→単体テストの実施…これらの順番は重要で、実装しながらテストを書くなどという不届きなことをさせないために、この順番は守らせましょう。

また、単体テストのチェックリストは必ず作成を義務付けて、バグの発生件数の指標やステップ辺りのテスト件数も従来のものから変えてはいけません。

新しいぶどう酒は古い革袋に入れて、ぶどう酒も手袋も無駄にしてやりましょう。

動かないテストコードを作成する

正しいかどうかよくわからない大量のテストコードは、確実に自動テストをしない方がマシという状況に我々にもたらしてくれます。自動テストの文化を潰すには動かない、あるいは動いているふりをするよくわからないテストコードを大量に作成することです。

テストコードのレビューを防ぐ

テストコードの作成の経験のない人は自分の環境でないと動かないテストや、特定の秒数じゃないとうごかないテストコードを書いたり、そもそもxUnitの検証にあたる箇所を目視でおこなったりします。

これらはテストコードを読むと簡単に検知できますが、そういう工数を削りましょう。
これにより動かないテストコードあるいは動いているふりをしているテストコードを大量に作成できます。

GUIのテストケースで不安定にさせる方法

そもそも、GUI操作でのテストの自動化が不安定になる傾向があります。
さらに不安定にさせるには「Windowsの画面操作を自動化しよう」で書いたことを参考に不安定な操作方法を模索しましょう。

  • 自動操作ツールが言っている「だれでも簡単に自動操作ができます」という営業文句を真に受けよう!
    • この手のツールはGUI操作をレコーディングし、操作内容のスクリプトが自動生成されることがあります。これは一見それっぽく動きますが、よく確認しないと不安定な動作の原因となります。
  • コントロールの特定には座標を積極的に使う
    • 解像度の違いなどで簡単に不安定にできる
      • オブジェクトで特定すると安定するので、その機能がないツールを選定する
  • 画面の切り替えにはSleepを積極的に使うようにする

定期的にテスト実行をしない

デイリービルドなどの定期処理にテストの実行を組み込まないようにしましょう。
組み込んでも何が合格して何が失敗したかわからないようにしましょう。

これにより、動かないテストコードが作成されても検知できないくなり、動かないテストが大量に増殖していきます。

定期的にテストが実行できない状況をつくる

定期的にテストは実行しないとか決めても、勝手に定期的にテストを実行しだす奴はあらわれます。
彼らの暴挙を防ぐには定期的にテストが実行できない状況を積極的に作りましょう。
定期的にテストが実行できない状況とは以下のような状況です。

  • 特定の環境でないと動作しないテストを作る
  • 時間のかかるテストを作る
  • リソースを大量に消費するテストを作る

これらは、通常回すテストと、頻繁に実行しないテストを分けるというような優先順位をつけて自動テストを実行することで回避されてしまいます。

これを防ぐには、テストコードを実行する場合は全て実行しなければならないとか、全てが優先度が高いとかいうトリアージができないルールでも作ってしまえばいいでしょう。

完全で完璧を求めるほど、逆にテストコードは実行されなくなっていきます。理想的な自動テスト推進派の顔を演じて、自動テストを叩き潰してやりましょう。

テストコードのメンテナンスにコストをかけない。

テストコードは簡単に動かなくなります。
たとえば、共通処理が変わっただけで動かなくなったり、WindowsUpdateを実行したりして動かなくなったりすることがあります。

是非、目先の作業を優先してテストコードのメンテナンスを行わないようにしましょう。
これにより、動かないテストコードが増殖します。

プロジェクト終了後

どんなに旨く失敗させた後も油断はしないでください。
実は失敗した際の情報というのは、次の成功につながる情報が含まれていることがあります。

ふりかえりなどのプロジェクトの反省はもってのほかですし、失敗したテストコードなどの資産などは削除するか、圧縮して他の人間が簡単に閲覧できないところに置いて別チームが活用できないようにしましょう。

俺たちは、一分前の俺たちより進化する、
一回転すればほんの少しだけ前に進む、
それがドリルなんだよ!!

…のように穴堀シモンのような事を言い出す奴がいるので、確実にリセットして何度でも同じ失敗をするように導きましょう。

そもそも自動テストをさせない戦略

xUnitの自動テストを防ぐ方法

ツールをダウンロードさせない

EclipseやVisualStudioに最初っから、単体テスト用のフレームワークが入っていることが多いのでxUnitのフレームワーク自体を使用不可にするのは難しいです。
しかしながら、モックやスタブ用のライブラリはダウンロードしないと入手できないので、これを防ぎましょう。
しかし、ダウンロードを防いでも自力で作り出す奴がいたりするので油断はしてはいけません。

xUnitでテストしづらいルールにする

xUnitのフレームワークでテストができなくなるルールとしては以下の通りです。

  • ローカル変数を含めてステップごとに変数の変化をテスト結果に求める
    • 入出力だけ確認するのは「ゆとり」。こころをこめてローカル変数の内容をエビデンスとしてのこす
  • IDEのステップごとの実行中のスクリーンキャプチャを取るようにする
    • 実際、パスが通ったかどうかを心を込めたスクリーンキャプチャで確認する

GUIで自動テストさせない方法

そもそもGUIの自動テストは難しいので手を下す必要はないですが、確実に根絶したい場合は以下を参考にしてください。

Webアプリケーション

マルチブラウザ対応の場合、Seleniumをダウンロードを禁止すれば、たいてい旨く防止できます。
InternetExploreが対応の場合、VBAやWSH,PowerShellで書けてしまいます。
基本これを防ぐ方法は難しいですが、Officeのマクロ、WSH,Powershellについてはセキュリティー方面で攻めてみればワンチャンあるかもしれません。

Windowsアプリケーション

基本的にUIAutomationを使用してVBAやPowerShellでなんとかなってしまうので、UIAutomationを使用するためのオブジェクトの構造をしらべるinspectのダウンロードを禁止することでなんとかしましょう。
inspectがなくても自作してくる狂人がいるので、これはOfficeのマクロ、WSH,Powershellのセキュリティー方面で攻めてみればワンチャンあるかもしれません。

さいごに

今回は自動テストをさせない方法を考えてみました。
正直、どうやって実現するかというのを考えるよりは、頭を使わずに済んで簡単ですね。なお、本記事の内容はフィクションで、実践している組織は存在しないはずです。ないですよね?

なお、私は基本的に自動テストを導入したりテストコードを書いた方が楽と考える立場ですが、定期的にテストを実行したり、回帰テストをするという文化や能力がないところで、テストコードを書くのを義務づけてはいけない。
これだけははっきりと真実を伝えたかった。
いや、マジでその状況はテストコードが負債になるので、やんない方がマシです。

powermock-mockito2-2.0.2を使ってみる

はじめに

以前でJavaのjmockitを調べてみたんですが、ちょっと合わないのでプランBとしてMockitoとPowermockを調べた際のメモになります。

Mockito
https://site.mockito.org/

Mockito JavaDoc
https://javadoc.io/doc/org.mockito/mockito-core/3.1.0/org/mockito/Mockito.html

Powermock
https://github.com/powermock/powermock/

検証環境
java version "1.8.0_202"
Eclipse IDE for Enterprise Java Developers.
Version: 2019-03 (4.11.0)
Build id: 20190314-1200
JUnit4
mockito-2.23
powermock-mockito2-2.0.2

mockitoとpowermock

Mockitoはテストコードを書く際に利用するモックを提供するライブラリです。
制限として、コンストラクタやスタティックメソッド、プライベートメソッドをモックできません。

PowerMockはEasyMockとMockitoの両方を拡張し、staticメソッド、finalメソッド、さらにはprivateをモックする機能を備えています。Mockitoと組み合わせて動作することが可能になっています。

今回は以下から「powermock-mockito2-junit-2.0.2.zip」をダウンロードして使用しています。
https://github.com/powermock/powermock/wiki/Downloads

また、当該環境では下記のJarを別途入手して参照しないと実行時にエラーになりました。
・byte-buddy-1.9.10.jar
・byte-buddy-agent-1.9.10.jar
https://howtodoinjava.com/mockito/plugin-mockmaker-error/

使ってみる

Mockitoを使う

スタブを使う

下記の例では特定の引数をモックメソッドに渡された場合に固定値を返却するスタブを作成した例になります。

package powermockTest;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class Test001 {
    class ClassA {
        public int testInt(int x, int y) {
            return x + y;
        }
    }
    @Test
    public void testWhen1() {
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(5, 6)).thenReturn(99);
        // モックで指定した結果
        assertEquals(99, mockedA.testInt(5, 6));

        // モック化されていない結果
        assertEquals(0, mockedA.testInt(5, 7));
    }
}

引数マッチャー

モック化する際に指定するパラメータは固定値だけでなく引数マッチャーを使用できます。
以下の例では固定値のかわりにanyInt()を使用して任意の整数を受け付けるようにできます。

    @Test
    public void testWhen2() {
        ClassA mockedA = mock(ClassA.class);
        // 引数マッチャーを使用して条件を指定可能です。
        // その他の引数マッチャーは以下参照
        // https://javadoc.io/static/org.mockito/mockito-core/3.1.0/org/mockito/ArgumentMatchers.html
        when(mockedA.testInt(anyInt(), anyInt())).thenReturn(99);
        // モックで指定した結果
        assertEquals(99, mockedA.testInt(5, 6));
        assertEquals(99, mockedA.testInt(5, 7));
    }

その他以下のような引数マッチャーを使用できます
https://javadoc.io/static/org.mockito/mockito-core/3.1.0/org/mockito/ArgumentMatchers.html

なお、複数引数があるメソッドの引数の一部だけに対して引数マッチャーを使用することはできません。すべての引数に対して引数マッチャーを使用してください。リテラル値を使いたい場合は以下のような実装になります。

    @Test
    public void testWhen3() {
        // 引数マッチャーを使用した場合、すべての引数はマッチャーによって提供される必要があります。
        /* eq(5)を使わず5を指定すると以下のエラーがでる
        org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
            Invalid use of argument matchers!
            2 matchers expected, 1 recorded:
        */
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(eq(5), anyInt())).thenReturn(99);
        // モックで指定した結果
        assertEquals(99, mockedA.testInt(5, 6));
        assertEquals(99, mockedA.testInt(5, 7));
        // モック化されていない結果
        assertEquals(0, mockedA.testInt(6, 7));
    }

モック化の影響範囲

jmockitでは@Mockedを使用してモック化されたクラスがある場合、そのテストの期間に作成された全てのインスタンスはモック化されたものとなります。
mockitoでは次のそのような動作はしません。

    @Test
    public void testWhen4() {
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(5, 6)).thenReturn(99);

        // モックで指定した結果
        assertEquals(99, mockedA.testInt(5, 6));

        // jmockitと違い、次に作られるインスタンスに影響を与えるわけではない。
        ClassA objA = new ClassA();
        assertEquals(11, objA.testInt(5, 6));

    }

同じ引数値で別の結果を返す方法

同じ引数で別の結果を返すには以下のような実装を行います。

    // 同じ引数で異なる結果を返す方法
    @Test
    public void testWhen5() {
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(5, 6))
            .thenReturn(99)
            .thenReturn(100);
        // モックで指定した結果
        assertEquals(99, mockedA.testInt(5, 6));
        assertEquals(100, mockedA.testInt(5, 6));
        assertEquals(100, mockedA.testInt(5, 6));

        // 別の書き方
        when(mockedA.testInt(1, 2)).thenReturn(10,20,30);
        assertEquals(10, mockedA.testInt(1, 2));
        assertEquals(20, mockedA.testInt(1, 2));
        assertEquals(30, mockedA.testInt(1, 2));
        assertEquals(30, mockedA.testInt(1, 2));
    }

なお、以下のような実装をした場合、最後に記載した内容で上書きされます。

        when(mockedA.testInt(5, 6)).thenReturn(99); // これは無視される
        when(mockedA.testInt(5, 6)).thenReturn(100);

モック化されたメソッドで例外を出力する方法

モック化されたメソッドで例外を出力するには以下のような実装を行います。

    @Test
    public void testWhen6() {
        ClassA mockedA = mock(ClassA.class);
        doThrow(new IllegalArgumentException("test")).when(mockedA).testInt(5, 6);
        try {
            // Expectationsで設定した2つめの値が取得
            mockedA.testInt(5, 6);
            fail();

        } catch (IllegalArgumentException ex) {
            assertEquals("test", ex.getMessage());
        }
    }
    @Test
    public void testWhen6_2() {
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(5, 6)).thenThrow(new IllegalArgumentException("test"));
        try {
            // Expectationsで設定した2つめの値が取得
            mockedA.testInt(5, 6);
            fail();

        } catch (IllegalArgumentException ex) {
            assertEquals("test", ex.getMessage());
        }
    }

コールバックによるスタブ

Answerインターフェイスによるスタブを許可されています。

    @Test
    public void testWhen7() {
        ClassA mockedA = mock(ClassA.class);
        when(mockedA.testInt(5, 6)).thenAnswer(
            new Answer<Integer>() {
                public Integer answer(InvocationOnMock invocation) throws Throwable {
                    Object[] args = invocation.getArguments();
                    System.out.println("getArguments----------------");
                    for (Object arg : args) {
                        System.out.println(arg);
                    }
                    // 本物の処理の2倍の値とする
                    return (int)invocation.callRealMethod() * 2;
                }
            }
        );

        // モックで指定した結果
        assertEquals(22, mockedA.testInt(5, 6));

    }

コールバックメソッドではInvocationOnMockが使用可能です。InvocationOnMockを経由して渡された引数やモック化されたメソッドを取得したり、実際のメソッドを実行したりすることが可能です。

1ラインでモックを作成する

下記の実装で1ラインでモックを作成することが可能です。

    @Test
    public void testWhen8() {
        ClassA mockedA = when(mock(ClassA.class).testInt(anyInt(), anyInt())).thenReturn(99).getMock();
        assertEquals(99, mockedA.testInt(5, 6));
    }

Spyを利用した部分モック

Spyを利用することでオブジェクトの一部のみモック化することが可能です。
下記のサンプルは特定の引数で実行された場合のみモック化された値を返却します。それ以外は実際のメソッドの実行結果を返却します。

    @Test
    public void testSpy1() {
        ClassA objA = new ClassA();
        ClassA spyA = Mockito.spy(objA);

        when(spyA.testInt(5, 6)).thenReturn(99);

        // モックで指定した結果
        assertEquals(99, spyA.testInt(5, 6));

        // モック化されていない結果
        // mock時とことなり本物がよばれる。
        assertEquals(12, spyA.testInt(6, 6));

    }

Verifyを利用した検証

verifyを使用することで、モック化されたメソッドがどのように実行されたかを検証することが可能です。

    @Test
    public void testVerify1() {
        ClassA mockedA = mock(ClassA.class);
        mockedA.testInt(5,6);
        mockedA.testInt(3,4);

        // 下記のメソッドが実行されていることを確認する(順番は検証しない)
        verify(mockedA).testInt(3,4);
        verify(mockedA).testInt(5,6);
    }

実行された回数の検証

times/never/atLeastOnce/atMostOnce/atLeast/atMost等で実行された回数の検証が可能です。

    @Test
    public void testVerify2() {
        ClassA mockedA = mock(ClassA.class);
        mockedA.testInt(5,6);
        mockedA.testInt(3,4);

        // 2回 testInt(?,?)が実行されていること
        verify(mockedA, times(2)).testInt(anyInt(),anyInt());

        // testInt(1,1)は実行されていないことを確認
        verify(mockedA, never()).testInt(1,1);
    }

実行順序の検証

inOrderを用いることでメソッドの実行順番を検証することが可能です。

    class T1 {
        void t1(String a) {

        }
    }
    class T2 {
        void t2(String a) {

        }
    }
    @Test
    public void testVerify3() {
        T1 mocked = mock(T1.class);
        mocked.t1("first");
        mocked.t1("second");

        // 順番を含めて検証する
        InOrder order = inOrder(mocked);
        order.verify(mocked).t1("first");
        order.verify(mocked).t1("second");
    }

    @Test
    public void testVerify4() {
        // 複数のオブジェクトの実行順番を確認することも可能
        T1 t1 = mock(T1.class);
        T2 t2 = mock(T2.class);
        t1.t1("call 1");
        t2.t2("call 2");
        t1.t1("call 3");
        t2.t2("call 4");

        // 順番を含めて検証する
        InOrder order = inOrder(t1, t2);
        order.verify(t1).t1("call 1");
        order.verify(t2).t2("call 2");
        order.verify(t1).t1("call 3");
        order.verify(t2).t2("call 4");
    }

finalのメソッドのモック化

finalメソッドはデフォルトの設定ではモック化できません。
finalメソッドのモック化を有効するには下記のファイルを作成する必要があります。

mock-maker-inline

image.png

    class ClassB {
        final public int testFinal(int x, int y) {
            return x + y;
        }
    }
    @Test
    public void testFinal1() {
        // finalのメソッドはデフォルトではモック化できない
        // /mockito-extensions/org.mockito.plugins.MockMaker ファイルを作成し、
        // 以下の文字を入れる必要がある
        // mock-maker-inline
        ClassB mockedB = mock(ClassB.class);
        when(mockedB.testFinal(5, 6)).thenReturn(99);

        // モックで指定した結果
        assertEquals(99, mockedB.testFinal(5, 6));

    }

PowerMockを使う

PowerMockを使用する場合はクラスに下記のアノテーションを付けてください。
・@RunWith(PowerMockRunner.class)
・@PrepareForTest({ZZZZ.class, XXX.class})

PrepareForTestにはテスト中にバイトコードレベルで操作する必要があるクラスをしてしてください。staticメソッド、コンストラクタ、プライベートメソッドのモック化を行う場合です。

package powermockTest;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.text.SimpleDateFormat;
import java.util.Calendar;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Calendar.class, java.lang.Math.class})
public class Test002 {
}

Privateメソッドのモック化

下記の例ではプライベートメソッドのtest1をモック化して、test2経由で実行した場合にモック化された値が返却されたことを確認しています。

    class ClassPrivate {
        private int test1(int x, int y) {
            return x + y;
        }
        public int test2(int x) {
            return test1(x, x) + 1;
        }
    }
    @Test
    public void testPrivate1() {
        ClassPrivate objA = new ClassPrivate();
        ClassPrivate spyA = PowerMockito.spy(objA);
        PowerMockito.when(spyA.test1(5, 5)).thenReturn(99);

        // モックで指定した結果
        assertEquals(100, spyA.test2(5));
        assertEquals(99, spyA.test1(5,5));

    }

Staticメソッドのモック化

下記の例はjava.lang.Math.randomをモック化した例になります。

    // randomをモック化した例
    @Test
    public void testStatic1() {
        // クラスに以下を設定してください
        // @PrepareForTest({ java.lang.Math.class})
        PowerMockito.mockStatic(java.lang.Math.class);
        PowerMockito.when(java.lang.Math.random()).thenReturn(1.5);

        assertEquals(1.5, java.lang.Math.random(), 0.1);
    }

現在時刻の偽装

Staticメソッドがモック化できると現在時刻を都合のいい時刻に偽装可能です。

    // 現在時刻をモック化した例
    @Test
    public void testStatic2() {
        // クラスに以下を設定してください
        // @PrepareForTest({ Calendar.class})
        Calendar cal = Calendar.getInstance();
        cal.set(2018, 1, 25, 23, 32, 30);
        cal.set(Calendar.MILLISECOND, 0);
        PowerMockito.mockStatic(Calendar.class);
        PowerMockito.when(Calendar.getInstance()).thenReturn(cal);

        Calendar c = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        assertEquals("20180225233230000", sdf.format(c.getTime()));
    }

メソッド内で作成されたインスタンスをモック化する

メソッド内で作成されたインスタンスをモック化するにはPowerMockito.whenNewを使用します。

    class classA {
        public String getA() {
            return "getA";
        }
    }
    class classB {
        public String hoge(String x) {
            return x + (new classA()).getA();
        }
    }

    // インスタンスをモックして、テスト対象のメソッド内で使用しているオブジェクトをモック化する
    @Test
    public void testInner() throws Exception {
        classA mockedA = Mockito.mock(classA.class);
        when(mockedA.getA()).thenReturn("abc");

        PowerMockito.whenNew(classA.class).withNoArguments().thenReturn(mockedA);
        classB obj = new classB();
        assertEquals("testabc", obj.hoge("test"));

    }

まとめ

使用事例が多いのと、学習コストの低さはJMockitよりアドバンテージがあると思います。
なお、JMockitに存在したカバレッジ計測はできないようです。

VisualStudio2019で複数プラットフォームをビルドしたい

複数プラットフォームをビルドしたい

VisualStudio2019にて複数プラットフォームのビルドを一括でしたい場合は、構成マネージャでビルドをしたい構成を作成したのち、バッチビルドを行う。

構成マネージャでの構成の追加方法

(1)[ビルド]→[構成マネージャー]を選択
image.png

(2)アクティブソリューションプラットフォームのドロップダウンにて「新規作成」を選択
image.png

(3)新しいプラットフォームダイアログにてプラットフォームを選択してOKを押す
今回はx64とx86の両方を新規作成で追加した
image.png

image.png

(4)構成マネージャーで追加したプラットフォームが選択可能になる
image.png

(5)メインメニューのツールバーでも追加したプラットフォームが選択可能になる
image.png

バッチビルドの方法

(1)[ビルド]→[バッチビルド]を選択
image.png

(2)ビルドしたい構成にチェックを付けて「ビルド」または「リビルド」を実行
image.png

(3)既定では以下のようなディレクトリにファイルが出力がされる。

├─x64
│  └─Release
└─x86
    └─Release

※出力先を変更したい場合は以下をいじる
image.png
なおC#の場合、C++と違い$(SolutionDir)とかの環境変数を使用するとエスケープ処理がされるようなので、直接プロジェクトファイルを開いて直す必要がある模様。
https://social.msdn.microsoft.com/Forums/vstudio/en-US/b0eb3746-285f-4ec6-9658-154f427cdb80/c-project-setting-output-directory-to-solution-dir?forum=msbuild

バッチビルドのメニューがない場合

バージョンによってはバッチビルドのメニューがないこともある。その場合は下記を参考にメニューのカスタマイズでバッチビルドのメニューを追加する

VisualStdio2012でバッチビルドを行う方法
https://needtec.exblog.jp/21564296/

以上

Javaで作った画面をWindowsで自動操作する方法

まえがき

たいていのWindowsの画面の自動操作は以下で紹介した方法で可能になっています。

RPA九人衆による「アカネチャンカワイイヤッタ」の自動化
https://needtec.sakura.ne.jp/wod07672/?p=9204

実は厄介なケースがあって、それはJavaで画面を作っているケースです。
今回はJavaで作られた画面を例に自動操作が可能かどうか検討してみましょう。

実験環境
Windows10 Home
Java 8
Visual Studio 2019
PowerShell 5.1
UiPath 2019.10.0-beta 111

Javaの画面の作成方法

Javaで画面を作成する主な方法としてSwingを使用する場合と、JavaFxを使用する場合があります。

Swingで作成した画面

下記のページを参考に簡単なSwingの画面を作成します。

image.png

ToDoListPane.java

package SwingSample;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
/**
* ToDoリスト
* 以下参考
* https://www.atmarkit.co.jp/ait/articles/0609/23/news027.html
*/
public class ToDoListPane extends JPanel {
        private JList<String> toDoList;
        private DefaultListModel<String> toDoListModel;
        private JTextField toDoInputField;
        private JButton addButton;
        public ToDoListPane() {
                super(new BorderLayout());
                // 一覧を生成
                toDoListModel = new DefaultListModel<String>();
                toDoList = new JList<String>(toDoListModel);
                JScrollPane listScrollPane = new   JScrollPane(toDoList);
                // ToDo追加用テキストフィールドの生成
                toDoInputField = new JTextField();
                // 各ボタンの生成
                JPanel buttonPanel = new JPanel();
                addButton = new JButton("追加");
                // ボタンにリスナを設定
                addButton.addActionListener(new    AddActionHandler());
                buttonPanel.add(addButton);
                add(listScrollPane, BorderLayout.NORTH);
                add(toDoInputField, BorderLayout.CENTER);
                add(buttonPanel, BorderLayout.SOUTH);
        }
        /**
        * 追加ボタンアクションのハンドラ
        */
        private class AddActionHandler implements ActionListener {
                public void actionPerformed(ActionEvent e) {
                        // テキストフィールドの内容をリストモデルに追加
                        toDoListModel.addElement
                        (toDoInputField.getText());
                }
        }
}

すべてのコードは以下にあります。
https://github.com/mima3/testjavagui/tree/master/java/Swing001

JavaFxで作成した画面

JavaFxでも単純な画面を作成します。
image.png

Main.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="ctrl.Controller">
  <!-- TODO Add Nodes -->
  <children>
    <Pane layoutX="0.0" layoutY="-14.0" prefHeight="297.0" prefWidth="345.0">
      <children>
        <Label layoutX="14.0" layoutY="14.0" text="リスト" />
        <ListView id="" fx:id="list" layoutX="14.0" layoutY="30.0" prefHeight="198.0" prefWidth="317.0" />
        <Button id="" fx:id="btnAdd" layoutX="14.0" layoutY="262.0" mnemonicParsing="false" onAction="#onAddButtonClicked" text="追加" />
        <TextField id="" fx:id="textBox" layoutX="14.0" layoutY="228.0" prefHeight="15.9609375" prefWidth="317.0" />
      </children>
    </Pane>
  </children>
</AnchorPane>

Controler.java

package ctrl;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;

public class Controller implements Initializable {
    @FXML
    private TextField textBox;

    @FXML
    private Button btnAdd;

    @FXML
    private ListView<String> list;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // TODO 自動生成されたメソッド・スタブ
        textBox.setText("値を入力してください。");

    }

    @FXML
    public void onAddButtonClicked(ActionEvent event) {
        // テキストボックスに文字列をセットする
        list.getItems().add(textBox.getText());
        textBox.setText("");
    }
}

すべてのコードは以下にあります。
https://github.com/mima3/testjavagui/tree/master/java/Java8Fx001

Java11でJavaFxの画面を作る場合の注意

JavaFXはJDK 11以降、Oracle JDKから分離されます。そのためJavaFxの画面を作る場合、以下のような手順が必要になります。

(1)JavaFXをダウンロードする。
https://gluonhq.com/products/javafx/

(2)ダウンロードしたフォルダ中のlibの中のjarをプロジェクトの参照ライブラリに追加する。
image.png

(3)実行時
コマンドラインから実行する場合

C:\pleiades201904\java\11\bin\java --module-path=C:\tool\lib\javafx-sdk-11.0.2\lib\ --add-modules=javafx.controls --add-modules=javafx.swing --add-modules=javafx.base --add-modules=javafx.fxml --add-modules=javafx.media --add-modules=javafx.web -jar Java11Fx.jar

Eclipseで実行する場合の実行構成
image.png

UIAutomationの自動操作

作成したJavaの画面をinspect.exeを使用してUIAutomation経由で操作できるかを確認します。

Swingの場合:

image.png

UIAutomationでコントロールの情報が取得されていないことが確認できます。つまりSwingで作成したアプリケーションはUIAutomation経由で操作が不可能です

JavaFxの場合:

image.png

UIAutomationの要素が取得されており、ControlTypeも適切に設定されていることが確認できます。
実際にPowerShellを使用して自動操作をしてみましょう。

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Drawing;

public class AutomationHelper
{
    // https://culage.hatenablog.com/entry/20130611/1370876400
    [DllImport("user32.dll")]
    extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

    [StructLayout(LayoutKind.Sequential)]
    struct INPUT
    {
        public int type;
        public MOUSEINPUT mi;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public int mouseData;
        public int dwFlags;
        public int time;
        public IntPtr dwExtraInfo;
    }

    const int MOUSEEVENTF_LEFTDOWN = 0x0002;
    const int MOUSEEVENTF_LEFTUP = 0x0004;
    static public void Click()
    {
        //struct 配列の宣言
        INPUT[] input = new INPUT[2];
        //左ボタン Down
        input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
        //左ボタン Up
        input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
        //イベントの一括生成
        SendInput(2, input, Marshal.SizeOf(input[0]));
    }
    static public void MouseMove(int x, int y)
    {
        var pt = new System.Drawing.Point(x, y);
        System.Windows.Forms.Cursor.Position = pt;
    }
    static public void SendKeys(string key) 
    {
        System.Windows.Forms.SendKeys.SendWait(key);
    }
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }

    public static AutomationElement GetMainWindowByTitle(string title) {
        PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
        return RootElement.FindFirst(TreeScope.Children, cond);
    }

    public static AutomationElement ChildWindowByTitle(AutomationElement parent , string title) {
        try {
            PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
            return parent.FindFirst(TreeScope.Children, cond);
        } catch {
            return null;
        }
    }

    public static AutomationElement WaitChildWindowByTitle(AutomationElement parent, string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = ChildWindowByTitle(parent, title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes", "System.Windows.Forms",  "System.Drawing")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを全て列挙
function findAllElements($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 要素をValuePatternに変換
function convertValuePattern($elem) {
    return $elem.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
}
function convertSelectionItemPattern($elem) {
    return $elem.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern) -as [System.Windows.Automation.SelectionItemPattern]
}

# 要素にテキストを入力
# Java8だとtxtValuePtn.SetValueが正常に動作しないための代替
function sendTextValue($textCtrl, $message) {
    [AutomationHelper]::MouseMove($textCtrl.Current.BoundingRectangle.X + 5, $textCtrl.Current.BoundingRectangle.Y + 5)
    [AutomationHelper]::Click()
    [AutomationHelper]::SendKeys("^(a)")
    [AutomationHelper]::SendKeys("{DEL}")
    [AutomationHelper]::SendKeys($message)
    Start-Sleep 1
}

# メイン処理
$mainForm = [AutomationHelper]::GetMainWindowByTitle("TODOリスト")
if ($mainForm -eq $null) {
    Write-Error "Java Fxの画面を起動してください"
    exit 1
}
$mainForm.SetFocus()
$editType = [System.Windows.Automation.ControlType]::Edit
$textCtrl = findFirstElement $mainForm $autoElem::ControlTypeProperty $editType

# Java8の場合ValuePatternのSetValueでエラーとなる
# $txtValuePtn = convertValuePattern $textCtrl
# $txtGetValue = $txtValuePtn.Current.Value
# Write-Host "変更前:$txtGetValue"
# $txtValuePtn.SetValue("わふる");

sendTextValue $textCtrl "わっふる"

$btnCtrl = findFirstElement $mainForm $autoElem::NameProperty "追加"
$btnInvoke = $btnCtrl.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
$btnInvoke.Invoke()

# 2文字目
sendTextValue $textCtrl "猫"
$btnInvoke.Invoke()

# 3文字目
sendTextValue $textCtrl "犬"
$btnInvoke.Invoke()

# リスト選択
$listitemType = [System.Windows.Automation.ControlType]::ListItem
$listitems = findAllElements $mainForm $autoElem::ControlTypeProperty $listitemType
$listPtn = convertSelectionItemPattern $listitems[1]
$listPtn.Select()

実行結果
auto4.gif

これを実行するとJava11のJavaFxを使用した画面は正常に完了しますが、Java8のJavaFxを使用した画面は下記のエラーを出力します。

JavaFxをUIAutomationでの操作時のエラー

Java8で作成したJavaFxに対してUiAutomationのValuePatternで値を設定すると下記のエラーが出ます。

PowerShell側

"1" 個の引数を指定して "SetValue" を呼び出し中に例外が発生しました: ""
発生場所 C:\dev\testjavagui\out\javafx_auto_err.ps1:146 文字:1
+ $txtValuePtn.SetValue("わふる");
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : COMException

Java側

Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
        at javafx.scene.control.TextInputControl.executeAccessibleAction(TextInputControl.java:1590)
        at javafx.scene.Node$19.executeAction(Node.java:9649)
        at com.sun.glass.ui.Accessible$ExecuteAction.run(Accessible.java:177)
        at com.sun.glass.ui.Accessible$ExecuteAction.run(Accessible.java:173)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.glass.ui.Accessible.lambda$executeAction$5(Accessible.java:190)
        at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
        at com.sun.glass.ui.Accessible.executeAction(Accessible.java:187)
        at com.sun.glass.ui.win.WinAccessible.SetValueString(WinAccessible.java:1262)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.lambda$null$152(WinApplication.java:177)
        at java.lang.Thread.run(Thread.java:748)

このエラーはJava11で作成したJavaFxの場合は発生しません。

Java Access Bridgeを使用した自動操作

Java Access Bridgeを使用することでWindowsはJavaのGUIを操作することが可能になります。

JavaやJava Access Bridgeを使用するプロセスが32bitか64bitかは意識して使用するようにしてください。

Java Access Bridgeを使用したGUI要素の探索

最初にJava Access Bridgeを使用したGUI要素の探索ツールであるAccess Bridge Explorerの使用方法について説明します。

(1)Java Access Bridgeを有効にする

%JRE_HOME%\bin\jabswitch -enable

(2)%JRE_HOME%\jre\binにWindowsAccessBridge-64.dllが存在することを確認し、環境変数PATHに %JRE_HOME%\jre\binを追加する。
古いバージョンだときは下記からダウンロードする必要がある。
https://www.oracle.com/technetwork/java/javase/tech/index-jsp-136191.html

(3)下記からAccess Bridge Explorerをダウンロードする。
https://github.com/google/access-bridge-explorer/releases

(4)Access Bridge Explorerを起動する。
image.png

※Java Access BridgeではSwingで作成した画面しか操作できません。JavaFxで作成した画面を操作することは不可能です。

.NETからのJava Access Bridgeの操作

.NETからJava Access Bridgeを操作するサンプルは下記に公開されていました。
https://github.com/jdog3/JavaAccessBridge.Net-Sample

上記を基にクリック操作やコンソールアプリからの操作を可能したサンプルが以下になります。
https://github.com/mima3/testjavagui/tree/master/cs

using JabApiLib.JavaAccessBridge;
using System;
using System.Collections.Generic;
using System.Text;

namespace JabApiCsharpSample
{
    class Program
    {
        static void Main(string[] args)
        {
            //JabApi.Windows_run();
            JabHelpers.Init();
            int vmID = 0;
            JabHelpers.AccessibleTreeItem javaTree = null;
            javaTree = JabHelpers.GetComponentTreeByTitle("ToDoリスト", out vmID);

            // テキスト設定
            JabHelpers.AccessibleTreeItem txt = javaTree.children[0].children[1].children[0].children[0].children[1];
            JabApi.setTextContents(vmID, txt.acPtr, "わろすわろす");

            JabHelpers.AccessibleTreeItem button = javaTree.children[0].children[1].children[0].children[0].children[2].children[0];
            List<string> actionList = JabHelpers.GetAccessibleActionsList(vmID, button.acPtr);
            Console.WriteLine("操作可能なアクション-------------");
            foreach (string a in actionList)
            {
                Console.WriteLine(a);
            }
            // クリック実行
            JabHelpers.DoAccessibleActions(vmID, button.acPtr, "クリック");

            //
            JabApi.setTextContents(vmID, txt.acPtr, "いろはにほへと");
            JabHelpers.DoAccessibleActions(vmID, button.acPtr, "クリック");

            //
            JabApi.setTextContents(vmID, txt.acPtr, "ちりぬるお");
            JabHelpers.DoAccessibleActions(vmID, button.acPtr, "クリック");

            // リストの内容
            Console.WriteLine("リスト一覧-------------");
            javaTree = JabHelpers.GetComponentTreeByTitle("ToDoリスト", out vmID);
            JabHelpers.AccessibleTreeItem list = javaTree.children[0].children[1].children[0].children[0].children[0].children[0].children[0];
            foreach (JabHelpers.AccessibleTreeItem listitem in list.children)
            {
                Console.WriteLine(listitem.name );
            }
            JabHelpers.DoAccessibleActions(vmID, list.children[1].acPtr, "クリック");
            Console.ReadLine();
        }
    }
}

DoAccessibleActionsで実行可能な操作はコントロール毎にことなり、なにができるかは、GetAccessibleActionsで調べることができます。
JabApiではJava Access BridgeのAPIを呼び出す関数を纏めて実装してあります。
今回は64ビットで動作していることを前提としているので必要に応じてJabApi.csの下記の行を変更してください。

    public static class JabApi
    {

        public const String WinAccessBridgeDll = @"WindowsAccessBridge-64.dll";

なお、Java Access Bridgeの初期処理にあたるWindows_runはメッセージポンプを必要としており、メッセージが処理されないと後続の処理が正常に動作しません。
大元になった.NETからJava Access Bridge操作サンプルでFormLoad時にWindows_runを入れなければならいと言っているのはこのためです。
今回はコンソールで動作するように以下のようにWindows_run後にDoEventsを実行するようにしました。

        // Windows_runはメッセージポンプが必要
        // https://stackoverflow.com/questions/50582769/windowsaccessbridge-for-java-automation-using-c-sharp
        public static void Init()
        {
            JabApi.Windows_run();
            DoEvents();
        }

実行結果
auto5.gif

PowerShellでの例

C#を基にPowerShellでも同じ操作を行うスクリプトが記述できます。
使用しているJabApi.dllはダウンロードするかソースコードからコンパイルしてください。
GitHubに挙げたDLLは64bit+.NET2.0なので環境によっては使用できません。

# 64bit前提
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Set-Item Env:Path "$Env:Path;$dllPath"
Add-Type -Path "$dllPath\JabApi.dll"
[JabApiLib.JavaAccessBridge.JabHelpers]::init()
$vmID = 0
$javaTree = [JabApiLib.JavaAccessBridge.JabHelpers]::GetComponentTreeByTitle("ToDoリスト",[ref]$vmID)
$txt = $javaTree.children[0].children[1].children[0].children[0].children[1]
[JabApiLib.JavaAccessBridge.JabApi]::setTextContents($vmID, $txt.acPtr, "わろすわろす")

# クリック
$button = $javaTree.children[0].children[1].children[0].children[0].children[2].children[0]
[JabApiLib.JavaAccessBridge.JabHelpers]::DoAccessibleActions($vmID, $button.acPtr, "クリック")

#
[JabApiLib.JavaAccessBridge.JabApi]::setTextContents($vmID, $txt.acPtr, "あああああ")
[JabApiLib.JavaAccessBridge.JabHelpers]::DoAccessibleActions($vmID, $button.acPtr, "クリック")

#
[JabApiLib.JavaAccessBridge.JabApi]::setTextContents($vmID, $txt.acPtr, "いいいいい")
[JabApiLib.JavaAccessBridge.JabHelpers]::DoAccessibleActions($vmID, $button.acPtr, "クリック") 

# 更新の確認
$javaTree = [JabApiLib.JavaAccessBridge.JabHelpers]::GetComponentTreeByTitle("ToDoリスト",[ref]$vmID)
$list = $javaTree.children[0].children[1].children[0].children[0].children[0].children[0].children[0]
foreach($item in $list.children) {
  Write-Host $item.name
}
[JabApiLib.JavaAccessBridge.JabHelpers]::DoAccessibleActions($vmID, $list.children[1].acPtr, "クリック") 

UIPathの場合

ツールからJava拡張機能をインストールすることでJavaのGUI操作が可能になります。
image.png
拡張機能をインストールすると「%JRE_HOME%\bin\」にUiPathJavaBridgeV8_x64.dllが、格納されます。

拡張機能をインストールすると、いつも通りに画面が作成できるようになります。
image.png

実行結果
auto6.gif

その他の選択肢

GUIのテストフレームワークを利用して自動操作がおこなえるかもしれません。
今回の目的と違っていたので、詳しくは調べていません。

Automation

AutomationはSwingとJavaFxのGUIを簡単にテストできるフレームワークです。

Javaで記載することもできますが、下記のようなGroovyのスクリプトで記載することも可能です。

clickOn 'text:Some Button'
doubleClickOn 'username-input'
type 'my-username'
clickOn 'text:Login'

TestFX

JavaFXのシンプルでクリーンなテストフレームワークです。
https://github.com/TestFX/TestFX

AssertJ Swing

AssertJ SwingはSwingのGUIのテストが可能のようです。
今はFest Swingをフォークしたものになります。

Java Swing UI test driver replacement for Fest [closed]
https://stackoverflow.com/questions/31168990/java-swing-ui-test-driver-replacement-for-fest

参考

Electronで作った画面の自動操作

前書き

Electronを使用することでnode.jsを使用してマルチプラットフォームの画面を作成できます。
今回は、このElectronで作成された画面の自動操作について考えてみましょう。

テスト用の画面を作成

以下のようなファイルを作成します。
image.png

package.json

{
  "name": "test-app",
  "version": "0.9.0",
  "description": "test-app",
  "main": "app.js",
  "author": "Name",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^6.0.12"
  }
}

app.js

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

let mainWindow = null;
app.on('ready', () => {
    //メインウィンドウの定義:サイズ以外にもいろいろ設定可能
    mainWindow = new BrowserWindow(
    {
        width: 600, 
        height: 400,
        'min-width': 300,
        'min-height': 200,
    }
    );
    //現在実行中のファイルディレクトリパスの下のindex.htmlを読みに行く。
    mainWindow.loadURL('file://' + __dirname + '/index.html');
    //メインウィンドウ終了時の処理
    mainWindow.on('closed', function() {
        mainWindow = null;
    });
});

index.html

<!DOCTYPE html>
<html>

<head>
<meta Http-Equiv="content-type" Content="text/html;charset=UTF-8">
<title>Electronテストアプリ</title>
</head>

<body>
Hello Electron
<input id="txt" type="TEXT"></input>
<button id="btn" >ボタン</button>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

index.js

const btnCtrl = document.getElementById('btn');
btnCtrl.onclick = function(element) {
  alert(document.getElementById('txt').value + "を出力");
};

これらのファイルをパッケージ化します。以下のページを参考にしてください。
Electron:アプリの実行とexe作成
https://web-dev.hatenablog.com/entry/web/js/electron/run-app-and-create-exe

作成されたExeは以下のようになります。
image.png

ボタンを押すとテキストで入力した文字をポップアップ表示します。
image.png

Seleniumでの自動操作方法

結局はChromeと同じなのでSeleniumが使用できます。
ただし使用するWebDriverはElectronのものを使用します。
使用したElectronのバージョンにあったWebDriverを入手してください。
https://github.com/electron/electron/releases
image.png

Pythonの例

基本的にChromeの自動操作で行ったSeleniumと同じ実装です。
ただし、ChromeOptionsのbinary_locationにelectronのアプリケーションへのパスを指定してください。

from selenium import webdriver
from selenium.webdriver.support.ui import Select
options = webdriver.ChromeOptions()
options.binary_location = 'C:\\dev\\node\\electronsample\\test-app-win32-x64\\test-app.exe'
print(options.binary_location)
# 使用しているElectronのVersionにあうWebDriverを入手すること!!!
# https://github.com/electron/electron/releases?after=v7.0.0-beta.6
driver = webdriver.Chrome("C:\\tool\\selenium\\chromedriver-v6.0.12-win32-x64\\chromedriver.exe", options=options)
#
driver.find_element_by_id("txt").send_keys("名前太郎")
driver.find_element_by_id("btn").click()
print(driver.switch_to.alert.text)
driver.switch_to.alert.accept()
driver.close()

Node.jsの例

ChromeOptionsのsetChromeBinaryPathを使用してelectronのアプリケーションへのパスを指定してください。

// https://seleniumhq.github.io/selenium/docs/api/javascript/index.html
const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const path = 'C:\\tool\\selenium\\chromedriver-v6.0.12-win32-x64\\chromedriver.exe';
const options = new chrome.Options();
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Options.html
options.setChromeBinaryPath('C:\\dev\\node\\electronsample\\test-app-win32-x64\\test-app.exe');
const service = new chrome.ServiceBuilder(path).build();
chrome.setDefaultService(service);
(async () => {
  const driver = await new webdriver.Builder()
                            .withCapabilities(webdriver.Capabilities.chrome())
                            .setChromeOptions(options)
                            .build();
  await driver.findElement(webdriver.By.id("txt")).sendKeys("名前太郎");
  await driver.findElement(webdriver.By.id("btn")).click();
  let alert = await driver.switchTo().alert();
  console.log(await alert.getText());
  await alert.accept();

  driver.quit();
})();

結果

auto7.gif

UiPathでの操作の場合

Chromeを操作する場合はブラウザの拡張機能と連携して操作していましたが、Electronの場合、そのブラウザの拡張機能機能は使用できません。
あくまでWindowsの画面として操作するので、簡単なテキスト入力やボタンクリックは問題ないですが、JavaScriptの実行等は不可能であると考えられます。
※すくなくとも試してみてテキスト入力やボタンクリックはできたが、JSスクリプト挿入は失敗した。

まとめ

Electronで作成した画面はSeleniumを利用することで、ブラウザの自動操作と同じように自動操作が行えます。
ただし、WebDriverはElectron用のものを使用してください。

自称IT企業があまりにITを使わずに嫌になって野に下った俺が紹介するWindowsの自動化の方法

はじめに

コンピュータを使用した多くの操作は自動化することができます。
この技術は運用や試験工程で大きな力を発揮します。

自動化の技術は一般的なソフトウェア技術者が、ちょっと努力すれば普通に身につく能力であって、特別なものではありません。

ただ残念なことにこれらの技術はあまり知られておらず、活用されているとは言い難い現場も多いです。
ユーザー企業さんができないのはしょうがないですが、ITで飯を食べているはずの自称IT企業においても、自動化を拒否して手動で心をこめて作業をしてリソースを無駄にするケースを稀によく見かけます。

自動化の拒否が「余剰人員のための経済対策だよ!」という身もふたもない理由でないと信じて今回は、Windowsでの作業の自動化についてお話しようと思います。

自動化のテクニックの話をする前に

Windowsの自動化のテクニックの話をする前にちょっと重要なことを先に述べておきます

銀の弾丸はない

あらかじめ言っておくと、ある種のセオリー的なものは紹介できますが、全ての環境で最善の方法を紹介するのは無理ですし、おそらく、そんなものは存在しません。せいぜいこういう状況でこれが使えるという話になります。

たとえば...

image.png

…と思っている方も、一旦、その存在は忘れてください。

RPAツールはどう自動化すべきか知っている人が使った場合、大きな効果がありますが、そのお手軽操作感に惑わされてRPAツールを採用したり、予算関係の都合でRPAツールを使うためだけにRPAツールを採用した場合、おそらく、後悔することになります。

たとえばRPAツールはお手軽に画面操作を記録して自動化が容易にできたと錯覚をさせることができます。おそらく、そうやって作成した自動化スクリプトは、多くの場合、安定して動作しません。
また、何故動作するのかを理解せずに作成された自動化スクリプトが大量に作成されるため、Windowsアップデートやアプリケーションのバージョンアップで動かなくなり、その修正に大きなコストがかかるようになります。

自分が使う道具の特性をよく見極めて、必要に応じて別の道具と組み合わせて使用してください。

本当に自動化必要ですか?

自動化を行うまえに本当にその作業が自動化必要なのか…そもそもその作業自体必要なのかを考えてください。

自動化をスクリプトを作成する場合、一定のコストがかかります。
これは一時的なコストになると思われがちですが、実際は継続的なコストになります。

たとえば、Windowsアップデートやアプリケーションのバージョンアップ、ハードウェアの変更など、自動化スクリプトが動かなくなる要因は多くあり、その対応コストを考える必要があります。

それらのコストを上回るリターンがある場合のみ着手してください。

適切に作成された自動化スクリプトは大きな資産になりますが、よくわからず作成された自動化スクリプトは負債になるということを覚えておいてください。

どこまでやるかを考えよう

業務の自動化を行う場合、すべての環境ですべての作業を自動化しようと考えると実現が不可能になります。
自動化がやりやすい箇所で最も効果的なものからやっていった方がいいでしょう。

また、自動化を行う環境も制限するようにした方がいいです。
たとえばWindows7とWindows10の環境で動かすようにするより、Windows10の.NET 4.7のみとか、この端末でのみとか動作環境を絞った方が自動化しやすいです。

自動化をはじめよう

コマンドラインを利用しよう

もっとも楽な自動化の方法はコマンドラインから実行することです。
今はやりのRPAで動作が安定しないという話がよくでてきますが、それは画面操作だけで自動化をめざすからです。
コマンドラインから自動操作するように心がければすれば、かなり安定します。

つまり、自動化を行うならまず最初にやるべきことはコマンドラインと仲良くなることです。
自分の環境のコマンドラインから、なにができるかを調べて、よく考えることです。

たとえば、ファイル操作であればコマンドプロンプトから、ほとんどのことは実行できます。
Windowsの場合なら、バッチやWSH、PowerShellを使用することで分岐や繰り返しを記述できます。

アプリケーションをコマンドラインから操作しよう。

GUIで動くアプリケーションも起動時にコマンドラインから引数を与えることで望んだ操作を行わせることができます。
たとえば、Windowsに最初から入っているメモ帳はコマンドラインから以下のように実行すると任意のテキストファイルを起動時に開くことができます。

notepad.exe C:\doc\books\統計.txt

自動操作したいアプリケーションが、コマンドラインからどのようなパラメータを受け付けるかは、アプリケーションのヘルプを見てみましょう。マニュアルを読んでもいいですし、アプリケーションによっては/?や/helpといったオプションを付けて実行してみるとヘルプが出てくる場合があります。

SIerではよく、サクラエディタ、WinScp、TeraTerm、WinMergeといったアプリケーションが使われていますが、これらのアプリケーションも多くのことがコマンドラインにパラメータを与えることで自動操作が行えます。

サクラエディタ
コマンドラインからGrepやマクロの実行が可能です。
https://needtec.sakura.ne.jp/wod07672/?p=9179

WinSCP
コマンドラインからファイルのアップロード、ダウンロードが自動で可能です。また、提供されているdllを使用することでPowerShellを含めた.NETFrameworkでのプログラミングが可能です
https://needtec.sakura.ne.jp/wod07672/?p=9178

TeraTerm
コマンドラインからマクロを実行することでリモートコンピュータに接続後に任意のコマンドを実行することも可能です。
https://needtec.sakura.ne.jp/wod07672/?p=9177

WinMerge
コマンドラインから比較レポートの作成やマージの実行ができます。
https://needtec.sakura.ne.jp/wod07672/?p=9176

また、これらのアプリケーションをインストールするためのインストーラ自体、コマンドプロンプトで起動ができます。
Windowsのアプリケーションのインストールは自動でやりたい
https://needtec.sakura.ne.jp/wod07672/?p=9185

このように、コマンドラインからパラメータを与えるだけで多くの自動操作を行うことが可能になっています。
まず、RPAツールとかプログラミング言語とか難しい技術を持ち出す前に、普段使用しているアプリケーションがコマンドラインからなにができるかを調べてみましょう。それだけで、かなりの自動操作が行えるようになります。

デフォルトの状態のWindowsでプログラムを行いたい

特別なプログラム言語をインストールしなくても、いくつかの方法でプログラミングが行えます。
主につかわれるのは以下の3つです。

  • バッチファイル
  • WSH
  • PowerShell

バッチファイル

ファイルの操作や、アプリケーションやコマンドの実行、エラー判定などの簡単な処理を記述できます。
COMや.NETのライブラリを使用した難しいことはできませんが、多くの場合、これで十分な場合があります。

ただ、複雑な処理を記述するには向いていないので、その場合、WSHかPowerShellを使用しましょう。

汝、コマンドプロンプトを愛せよ
https://needtec.sakura.ne.jp/wod07672/?p=9169

WSH

WSHとはWindows Script Hostの略で、Windows2000やWindowsXP環境下ですら使用可能です。
Microsoft Visual Basic Scripting Edition (以下 VBScript) と, Microsoft JScriptの2種類がサポートされています。JScriptはMS独自の規格で一般なJavaScriptとは規格が異なります。

環境によってはEdgeのエンジンを使用することができる。コメント欄参照

バッチファイルより複雑な処理を実現することが可能です。
具体的には下記のようなことが可能になっています。

  • システム情報の取得と変更
  • ファイル・フォルダの作成、コピー、属性変更
  • レジストリの読み書き
  • プログラムの実行
  • COMコンポーネントの作成・利用
    • InternetExploreの操作
    • Office(ExcelやWord)の操作

弱みとしては以下の通りです。

  • .NET Frameworkのライブラリを利用できない(一部を除く)
  • UTF8に弱い
  • Win32 APIが使用できない
  • VBScriptだとTry-Catch的な実装ができないのでエラー処理が弱い
    • JScriptは使えました。
  • デバッガーは存在するがVisualStudioを入れる必要がある

レガシー環境のためのWindows Script Host(WSH)の解説
https://needtec.sakura.ne.jp/wod07672/?p=9288

PowerShell

Windows7から使用可能です。WSHの上位互換といえるでしょう。
.NET Framework上で動作し、.NET Frameworkで作成されたライブラリをフルに使用できますし、Win32 APIも実行できます。
具体的な使用例は以下を参照ください。

Powershellによるファイル操作のまとめ
https://needtec.sakura.ne.jp/wod07672/?p=9182

PowerShellによるレジストリの操作例
https://needtec.sakura.ne.jp/wod07672/?p=9184

ただ、COMの操作だけは.NETを使用する都合上、WSHより解放処理が面倒になっています。

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。
https://needtec.sakura.ne.jp/wod07672/?p=9189

Officeアプリケーションの自動化をしよう

MicroSoftのOfficeアプリケーションは多くの現場で使用されています。
そして同時に多くの自動化が望まれているものでもあります。

幸いなことに、Excel,Word,PowerPoint…MsProjectにいたるまで、多くのOfficeアプリケーションはCOM経由での自動操作が可能になっており、また、マクロが実装できるようになっています。

Officeでマクロ開発しよう

ひと昔前のOfficeは既定のメニューから簡単にマクロの開発のメニューができましたが、最近のOfficeはひと手間必要になります。

(1)Word等のOfficeアプリケーションを開いたら、オプション画面を表示します。
image.png

(2)オプション画面を開いたら「リボンのユーザ設定」を選択します。
image.png

(3)その後、開発にチェックします。
image.png

(4)するとメニューに「開発」が表示されます。
image.png

これによりOfficeアプリケーションからマクロを記述して自動化することが可能になります。
おそらく、色々な書籍が売っているので学習には困らないと思います。
Excelなどが有名ですが、基本的にOfficeアプリケーションの多くがVBAによるマクロ開発のサポートをしています。

具体的なOfficeのVBAの開発の話は下記を参考にしてください。ExcelVBAとかいっていますが、その他製品のVBAに対する開発についての基本的な考えは同じです。

オプーナとゆっくりのExcelVBA講座
https://www.nicovideo.jp/mylist/36464404

Excel VBAコーディング ガイドライン案
https://needtec.sakura.ne.jp/wod07672/?p=9310

OfficeアプリケーションのCOM経由での操作

先ほどいったとおり、Officeアプリケーションの自動化はCOM経由で別のプログラミング言語から操作が可能です。

RedmineをあきらめたオレたちのPowerShellでのOutlookの自動操作
https://needtec.sakura.ne.jp/wod07672/?p=9188

PowerPointで茜ちゃんに喋ってもらう
https://needtec.sakura.ne.jp/wod07672/?p=9190

MSProjectをVBScriptまたはVBAで操作する
https://needtec.sakura.ne.jp/wod07672/?p=9303

ただし、PowerShellやC#などの.NET経由でのCOM操作を行う場合、解放処理が重要になります。

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。
https://needtec.sakura.ne.jp/wod07672/?p=9189

上記の記事でも触れていますが、.NETでの解放処理が面倒な場合、NetOfficeというMIT Licenseのライブラリについての採用を検討してください。OfficeのCOMオブジェクトをラッパーしています。
https://github.com/NetOfficeFw/NetOffice

OfficeのCOM操作時のマルチスレッドについて

色々なプログラムからOfficeを操作可能となると、マルチスレッドで動かしたくなる場合があります。
しかし、無駄なのでやめましょう。
Officeのオブジェクトモデルはスレッドセーフでなく、同時呼び出しをシリアル化するためのメカニズムが動いています。つまり、2つのスレッドから同時にOfficeのオブジェクトを操作しても、どちらかの処理が動いているだけです。

スレッドの Office でのサポート
https://docs.microsoft.com/ja-jp/visualstudio/vsto/threading-support-in-office?redirectedfrom=MSDN&view=vs-2019

Webアプリケーションを自動化しよう

ホームページにアクセスにてなんや、かんや自動化したいという話ですが、最近以下にまとめましたので参照してください。

Webアプリケーションを自動で操作してみよう
https://needtec.sakura.ne.jp/wod07672/?p=9167

Windowsの画面操作を自動化しよう

画面操作を自動化する前にコマンドラインでやりたいことが実現できないか、あるいはアプリケーションがプログラム可能なライブラリを提供していないか確認してください。たとえばWinSCPはWinSCPnet.dllを利用することでC#やPowerShellからWinSCPの操作が可能になっています。

これらを確認した上で初めて画面の自動操作のスクリプトについて考えましょう。

多くの場合、以下で紹介したいずれかのケースで上手くいきます。

RPA九人衆による「アカネチャンカワイイヤッタ」の自動化
https://needtec.sakura.ne.jp/wod07672/?p=9204

操作したいコントロールの特定方法

画面の自動化を行う場合、なんらかの方法で画面上のコントロールを特定する必要があります。

たとえば電卓アプリで「÷」キーを特定する方法を考えてみましょう。
image.png

座標で特定する方法

もっとも簡単な方法はコントロールの座標で特定する方法です。
image.png

この方法は簡単に、そして多くのツールで行える方法ですが、安定しません。
たとえば、ウィンドウの位置が変更されたり、サイズが変更されたりしただけで容易に破綻します。

もし安定した画面操作の自動化スクリプトを記述する場合、座標で特定する方法はなるべく避けてください。

オブジェクトで特定する方法

Windowsアプリの多くはInspectなどで表示されるようなオブジェクトの構造をもっています。
たとえば電卓の例だと以下のようにNameとControlTypeを指定して特定のオブジェクトを検出が可能です。
image.png

この方法が最も安定した画面の自動操作を実現しますが、常に使用できるとは限りません。
アプリケーションの作り方と使用した技術しだいでは検出できないコントロールが存在したり、そもそもゲームのようにボタンとかを独自に描画している場合は検出不能です。

UiAutomationWinAppDriverFriendlyUWSCUiPAthなどでサポートしている検出方法です。

なお、JavaのSwingアプリケーションでオブジェクトを特定したい場合は「Javaで作った画面をWindowsで自動操作する方法」をElectronでオブジェクトを特定したい場合は「Electronで作った画面の自動操作」をそれぞれ参照してください。

テンプレートの画像を指定して特定する方法

image.png

テンプレートの画像と一致する箇所をWindowsデスクトップ全体から検出します。
たとえ、オブジェクトが検出できない状況下でも画像でコントロールを見つけて検出が可能です。半面、画像でおこなっているため、色合いや画面のサイズなどによって影響を受けて安定して動作しなくなるケースも多いです。

UWSCUiPAthsikulixPyAutoGuiでサポートしている機能です。

OpenCVのライブラリを使用することで、このあたりの機能は自作も可能です。

C#やPowerShellで画面上の特定の画像の位置をクリックする方法
https://needtec.sakura.ne.jp/wod07672/?p=9168

どの技術を採用すべきか?

一番最初は「オブジェクトで特定する方法」が可能か検討してください。
そして、実現できないと判断した場合に「テンプレートの画像を指定して特定する方法」を使用しましょう。

オブジェクトで特定する方法の技術比較

UWSCは優れたツールですが、公式ページが消えたので今後は使用しない方がいいです。

WinAppDriverはSeleniumと同じインターフェイスで操作できるため、Seleniumになれている場合は採用を検討しましょう。ただし、Windows10でないと動作しません。

UiAutomationは、どの環境下でも画面の自動操作が行えます。多くの場合はこれで十分でしょう。

UiPAthはオブジェクトでの特定と画像認識での特定の両方が可能であるため、一つ覚えれば全て済むという強みがあります。ただし本格的に使う場合はかなりのお値段となります。

Friendlyは、この中では特殊です。自動操作のためにDLLをインジェクションするため、落としていいプロセスか、DLLをインジェクションしてかまわないかどうかで使用の判断がわかれます。逆にそのハードルを越えた場合、基本的にどんなオブジェクトでも特定したり、必要に応じて画面操作を無視して関数を実行したりできます。※ただし.NET Frameworkで動く画面に限ります。

画像で特定する方法の技術比較

UWSCは優れたツールですが、公式ページが消えたので今後は使用しない方がいいです。

Sikulix1.1.4を使って画面の自動操作をする方法はIDEも使い易く自動操作をサポートするための機能を多数有しておりおすすめです。

1.1.4時点の機能は下記にまとめています。

Sikulix1.1.4を使って画面の自動操作をする
https://needtec.sakura.ne.jp/wod07672/?p=9202

問題がでるとしたら、Java8SDKの64bitが必要であり環境を選ぶことと、使用しているPythonがJythonのため純粋なPythonではない点です。
純粋なPythonで自動操作をしたい場合はPyAutoGuiを採用するといいでしょう。

UiPAthはオブジェクトでの特定と画像認識での特定の両方が可能であるため、一つ覚えれば全て済むという強みがあります。ただし本格的に使う場合はかなりのお値段となります。

安定した画面操作の自動スクリプトを書くために

環境を特定する

OSやハードウェアの構成は無論のこと、ディスクトップの色や解像度の設定も指定した上で、他のアプケーションを動かさずに自動操作を行えるようにします。

並行して別の作業を行わないというのは結構重要になります。
たとえば、自動処理中にユーザーがうっかりマウスを移動させたりしただけで、後続の処理が動作しなかったり、裏で別のアプリケーションが動作していてそれが干渉しあって動作しなくなったりまします。
(たとえば一部の自動操作ツールはテキストの入力にクリップボードを使用するケースがあり、当然、この場合、コンピュータ上の別のアプリケーションまたは操作でクリップボードを使用すると正常に動作しなくなる場合がでてきます。)

安易なSleepを使用しない。

たとえば、ボタン押下で画面の切り替わりを待って、表示された新しいコントロールの内容を取得する例を考えてみましょう。

簡単にやろうとすると、ボタン押下後、2秒Sleepとかの処理をいれてしまします。
これを行うと安定しません。

安定したスクリプトを書くには定期的に新しいコントロールの取得を試みる処理をいれるようにすべきです。
UiPathやSikulixは要素が表示するまで待つとかの機能がありますが、もしUiAutomationを使用して自作する場合は以下のコードを参考にしてみてください。

https://github.com/mima3/rpa_akanechan/blob/master/powershell(UIAutomation.NET)/kawaii.ps1

    public static AutomationElement ChildWindowByTitle(AutomationElement parent , string title) {
        try {
            PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
            return parent.FindFirst(TreeScope.Children, cond);
        } catch {
            return null;
        }
    }

    public static AutomationElement WaitChildWindowByTitle(AutomationElement parent, string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = ChildWindowByTitle(parent, title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }

この例ではボタン押下後に特定のタイトルのウィンドウが表示するまで待機して、タイムアウトがでたらエラー、それまでに取得できたら後続の処理をおこなっています。

決してユーザーの操作と同一でないことを忘れない

UiPathなどはドライバからイベントを発生させてユーザの操作に似せようとしていますが、多くの場合、完全にユーザの操作と同一になっているわけでなく無理やり通している面があります。
このため、アプリケーションの作り方次第では、手動操作と異なる挙動をするケースがあります。

たとえば、テキストの値をUiAutomationで設定した場合を考えてみましょう。
もし、そのコントロールでキーボード操作のイベントで何らかの処理していたり、フォーカスの移動で何らかの処理をしていたりする場合、結果が同じにならない場合があります。

この場合は、クリックしてからテキストを設定したり、オブジェクトの値を更新するのでなく、System.Windows.Forms.SendKeys.SendWaitを使用してキーボード操作を真似する必要があります。
もしかすると、手動入力のタイミングにあわせるためSleepを使用する必要がでてくるかもしれません。

その他自動操作の話

タスクスケジューラ―を使いこなそう。

タスクバーにてtaskschdと入力することでタスクスケジューラ―が起動します。
image.png

image.png

定期的に実行する処理を指定できます。
タスクスケジューラのもっとも強力な機能として管理者権限で動作が可能になることです。
これにより、管理者権限が必要な自動操作も可能になります。

たとえ途中で再起動と管理者権限が必要でも、自動化をあきらめたらそこで試合終了ですよ…?
https://needtec.sakura.ne.jp/wod07672/?p=9183

なお、タスクスケジューラ―はコマンドプロンプトから操作することが可能になっています。

まくまくWindowsノート Windows で任意のコマンド(タスク)を自動実行する (schtasks)
https://maku77.github.io/windows/admin/schtasks.html

リモートコンピュータとの連携

リモートコンピュータと連携して実現する方法は以下のページにまとめがあります。

別端末(Windows)のプログラムを標準機能でリモート起動する方法まとめ
https://qiita.com/0829/items/5518256b348521ac358c

管理者権限が必要ならschtasks (タスクスケジューラ)、PowerShell等でそのまま操作したければPowerShellからWinRMを利用する方法が楽そうに見えます。

その他に、私が実際やった方法としてはHTTPサーバーを立ててクライアントからアクセスがあったらクエリパラメータで指定したコマンドを実行するというセキュリティがガバガバなことをやっていました。いまやるなら、Jenkinsなどを利用してスクリプトを動かすようにすると思います。

よくある問題

予算がおりて〇〇というRPAツール導入したから、今後はこれを使って自動化するように

自動操作の種類によって、向き不向きがあります。
たとえば、Excelの内容を抽出してファイルに保存するという処理はUiPathを使用するよりVBSなどで書いた方が早くて正確です。

適切な方法を選択するようにしましょう。

RPAツールを使わないと上司に怒られるので、どんな処理でもRPAツールを使わないとだめなんだよー

背景をちゃんとききましょう。
おそらく、RPAツールで行うことで、シナリオの配布やスケジュール実行の管理が楽になるために、RPAツール使えと言っているのだと思います。
たとえばUiPathの場合は以下のようなことができます。

この場合、処理のメイン自体はVBSやPowerShellで書いちゃいましょう。そして、UiPathからPowerShellの実行等のアクティビティを利用して起動しましょう。

なお、特に理由がなく、上司がRPAツールに興奮を覚えるだけの紳士なだけだった場合の時は、我慢するか、できないなら辞めたほうがいいです。

このOSは出来損ないだ初期状態でPowerShellが使えないよ。

どういう状況かをいっているかわからないので認識があっているかわかりませんが、初期状態でPowershellのスクリプトが実行ポリシーの問題で実行できなくて、かつ管理者権限がなくて変更できない状態を指していると仮定します。

http://totech.hateblo.jp/entry/2017/09/29/162411

上記のページにあるようにExecutionPolicyを指定して実行してください。これには管理者権限は不要です。

powershell -ExecutionPolicy RemoteSigned .\test.ps1

なぜか自動化できる人材がいません

image.png

お野菜は勝手に生えてきませんし、人材は畑で採れません。自動化してくれる人材は勝手に生えてこないので、教育期間を確保するか、高い金払って外部から雇いましょう。

××ツールを使えば誰でも簡単に自動化ってできるものなの?

image.png

私は懐疑的です。
おそらく、「安定しない、保守コストの高い自動スクリプト」は「誰でも簡単」に作れる可能性はあります。

すくなくともWebアプリケーションを動かすならWebアプリケーションの開発経験者を、Windowsアプリケーションを動かすならWindowsアプリケーション開発経験者を一人は確保しておいたほうがいいでしょう。この場合、誰でも簡単にできる箇所とできない箇所を分けて仕事を割り当てた方がいいでしょう。

〇〇ツールがなくて自動化できません。

今までで説明した通り、初期状態のWindowsの環境でも色々自動化が可能です。
その実績や信頼をもって、〇〇ツール導入の交渉にあたりましょう。

実績や信頼もないのに、多くを望むのは無理です。できるところから頑張りましょう。
image.png

なにから自動化したらいいかわかりません。

費用対効果の高いところを選んでやりましょう。

費用が安く済むのは画面操作が不要で、コマンドラインから処理ができるものです。
たとえばファイルの拡張子を一律変更するとか、ExcelのC5セルに今日の日付を入れるとかになります。

ここで条件が入ると徐々に費用が高くなります。
たとえば、Aから始まるファイル名だけ変更するとか、Aから始まって日付が今日以降のExcelを開いてC5のセルを変更するとかの場合です。

効果があるというのはどういうものでしょうか?

まず作業量的な観点があります。
たとえば、先のファイル拡張子の変更が1ファイルだけなら自動化の効果は薄いですが、1000ファイルあったら効果は高くなったといえるでしょう。

次に質的な観点があります。
たとえば、ExcelのセルC5に今日の日付を入れるという作業があり、そのセルの周辺に似たような入力項目があるとします。
人が手でやる場合、まちがってB5に入れてしまうかもしれません。
自動でやればそのようなミスはなくなります。こういった作業の品質を高くしたい場合にも効果があると言えるでしょう。

そもそも〇〇使っているから悪い、全部変えちゃおう!

〇〇にOS名やアプリケーション名など好きなのをいれてくださって構いません。
その上で、一気に全部変えるという戦略はお勧めしません。

現状のシステムで動いているものがあり、そのシステムを使う関係者が大勢いる場合、調整するのに非常に力が要ります。
そもそも、どんな状況にせよ現状の状態は、それなりの理由があって現状の状態になっているわけで、抜本的な改革とかグレートリセットなどを、周りの合意が取れずにやるのは無謀です。
小さいところから始めていきましょう。

image.png

せっかく作った自動化スクリプトが動かなくなった

アプリケーションのバージョンアップやWindowsアップデートでよく発生します。
なので、作成した自動化スクリプトの管理方法については考えておきましょう。

1度動かせばすむスクリプトなのか、定期的に動かすスクリプトなのかによって、スクリプトの管理方法が変わります。
定期的に動かすスクリプトの場合、常にバージョンアップ等で動作しなくなるというリスクがあります。
このリスクをどう扱うか考えましょう。

たとえば、実際やってエラーとなった時点で修正する時間的余裕のあるものであれば、その時に考えればいいでしょう。
しかし、そういう時間的余裕が確保できないようなスクリプトの場合は、事前にそれを検出する必要があります。
定期的にスモークテストを行う計画を立てるか、アプリケーションのリリースノートをチェックする工数をとるか、いずれにせよなんらかの対策をとりましょう。

自動化自体は簡単なのですが、保守的な組織なため導入に消極的です

おそらく作業品質が問題になっていると思います。
こういった保守的な組織でよく利用した手法は手動と自動をしばらく並行で動かして、周りが納得がいったら自動に切り替えるという方法です。

たとえば面倒な操作を色々してExcelを作成するという業務があったとします。
しばらくは、「面倒な操作」を自動でも、手動でもやって、Excelを作成します。その結果を比較し続けて、周りの合意がとれたら手動をやらなくするという方法です。

自動化する時間がありません

管理者の場合は、ちゃんと確保するようにスケジュールを立てましょう。

ちゃんとした組織の作業者の場合は、管理者に自分の案を説明して相談しましょう。
ちゃんとしない組織でかつ通常作業が平均より早い自信のある方は、見積もりにコッソリのせてダマで作りましょう。
だいたい動くものを作ると黙らせることができます。

この辺の立ち回りとしてはJoel on softwareの以下の記事が参考になります。

下っ端でも何かを成し遂げる方法
http://web.archive.org/web/20170625093906/http://japanese.joelonsoftware.com/Articles/GettingThingsDoneWhenYour.html

あなたは管理する立場にいなくとも、ものごとを改善することはできる。しかしあなたはシーザーの妻である必要がある: 疑惑を招いてはならない。そうでなければあなたは敵を作るだけだろう。

ある業務が自動化できれば、時間的余裕ができて、別の業務を自動化するという好循環がつづきます。
完全で完璧な状況は作れませんが、あきらめなければ少しづつ改善していくことはできると思います

せっかく自動化して改善したのにあんまり喜ばれません

image.png

色々理由があります。

1つ目は自動化すべきでないときに自動化してしまった時です。
たとえば、緊急の障害票がたまっているときに、優先度の低い業務を自動化しましたとか言われてもうれしくないでしょう。

2つ目は自動化することで、他人の仕事を奪ってしまったケースです。
環境によっては、余剰人員対策のためにマクロで実行すればすぐ終わるような作業があったりします。そこを空気を読まずに自動化してしまうと、上の人は新しい仕事を考える必要がでてきてあんまりうれしくないです。

3つ目は改善前の苦労を誰もわかっていなかったので、それが改善されたところで興味がないケースです。
そして、このケースが多いです。
このケースの場合は、他人に評価されようとかいうの余分な感情…いわば贅肉を奥にしまって、プロジェクトやシステムに対してなすべきことをなすようにしましょう。

…それでなすべきことをなせましたか?

image.png

Pythonで久しぶりにHTMLを出力したくなったのでテンプレートについて調べる

はじめに

ひさしぶりにPythonで久しぶりにHTMLを出力したくなったのでテンプレートについて調べます。

環境:
Python 3.7.4

標準ライブラリでの文字列の書式の扱い

まず標準で使える範囲で文字列の書式をどう扱えるか調べます。
これについては下記の素晴らしいまとめが存在します。

Python String Formatting Best Practices
https://realpython.com/python-string-formatting/

ここで紹介されている方法は以下の通りです。

  • 古い形式の文字列の書式
  • 新しい形式の文字列の書式
  • PEP498で定義された書式設定方法
  • テンプレート文字列

古い形式の文字列の書式

昔ながらの文字列の書式の指定方法です。

name = 'アンジュ'
age = 18

print('古いやり方--------------------------------------')
outstr = '%s は %d歳' %(name, age)
print (outstr)

outstr = '%s は 0x%x歳' %(name, age)
print (outstr)

outstr = '%(name)s は %(age)d歳' % {'name':name, 'age': age}
print (outstr)

outstr = '%(name)s は 0x%(age)x歳' % {'name':name, 'age': age}
print (outstr)

printf 形式の文字列書式化に以下のような記述があるので今となっては別の方法を選択した方がいいでしょう。

注釈 ここで解説されているフォーマット操作には、(タプルや辞書を正しく表示するのに失敗するなどの) よくある多くの問題を引き起こす、様々な欠陥が出現します。 新しい フォーマット済み文字列リテラル や str.format() インターフェースや テンプレート文字列 が、これらの問題を回避する助けになるでしょう。 これらの代替手段には、それ自身に、トレードオフや、簡潔さ、柔軟さ、拡張性といった利点があります。

新しい形式の文字列の書式

str.formatによる書式の設定は、Python3から導入され後にPython2.7に移植されました。

name = 'アンジュ'
age = 18

print('str.format--------------------------------------')
outstr = '{} は {}歳'.format(name,age)
print (outstr)
outstr = '{} は 0x{:x}歳'.format(name,age)
print (outstr)
outstr = '{name} は {age}歳'.format(name=name, age=age)
print (outstr)
outstr = '{name} は 0x{age:x}歳'.format(name=name, age=age)
print (outstr)

PEP498で定義された書式設定方法

PEP498で定義された書式の設定方法でPython3.6から追加されました
これはES2015/ES6で追加されたJavaScriptのテンプレートリテラルに似ています

name = 'アンジュ'
age = 18

print('f-strings--------------------------------------')
outstr = f'{name} は {age}歳'
print (outstr)
outstr = f'{name} は 0x{age:x}歳'
print (outstr)
outstr = f'{name} は {10+8}歳'
print (outstr)

テンプレート文字列

テンプレート文字列PEP292で解説されている単純な文字列置換を行います。

from string import Template
tpl = Template('$name は $age歳')

name = 'アンジュ'
age = 18

print('template--------------------------------------')
print(tpl.substitute(name=name, age=age))
# %xなどの書式は使えない
print(tpl.substitute(name=name, age=hex(age)))

ユーザからの入力を受け取る場合はテンプレート文字列を使用した方が望ましいです。
たとえばformat()を使用して以下の実装をしたとしましょう。

SECRET = '秘密の文字列'

class Error:
  def __init__(self):
    pass

err = Error()

# ユーザから以下のような入力を受け付けたとする
user_input = '{error.__init__.__globals__[SECRET]}'
# 出したくない文字列が表示される
print(user_input.format(error=err))

ユーザ入力に対してformatを実行すると出したくない文字列にアクセスできたりします。

同様の実装をテンプレート文字列を使用すると以下のようになります。

SECRET = '秘密の文字列'

class Error:
  def __init__(self):
    pass

err = Error()

# ユーザが以下を入力したとする
user_input = '${error.__init__.__globals__[SECRET]}'

# エラーになります
# ValueError: Invalid placeholder in string: line 1, col 1
print(Template(user_input).substitute(error=err))

この問題はBe Careful with Python's New-Style String Formatに記載されています。

HTML用のテンプレートライブラリ

単純な文字の置換であれば今まで標準のライブラリで十分ですが、繰り返しとかを定義したい場合はHTML用のテンプレートライブラリを探す必要があります。

HTMLを自動生成するためのテンプレートライブラリとして以下のページで比較がなされています。

3 Python template libraries compared
https://opensource.com/resources/python/template-libraries

このページで紹介されている方法は以下の3つになります。

StackOverflowでの質問件数の比較

Mako,Jinja2,Genshiともに、機能や環境的には今回の使用目的にはあっているので、よく使われているライブラリを使用することにします。
そのため、StackOverflowでの質問件数を調べます。

2019/10/18時点
image.png

image.png

image.png

結果:

名前 検索結果
Mako 1937
Jinja2 16980
Genshi 363

Jinja2の圧勝のようです。

なお、以下のページでタグごとのトレンドを比較できますが、Jinja2以外タグがなかったので今回は採用しませんでした。
https://insights.stackoverflow.com/trends

Jinja2を使用してみる

インストール方法

インストール可能な条件は以下の通りです。

Jinja2 works with Python 2.6.x, 2.7.x and >= 3.3. If you are using Python 3.2 you can use an older release of Jinja2 (2.6) as support for Python 3.2 was dropped in Jinja2 version 2.7.

Introduction - Jinja Documentation

2019年10月18日時点では以下のコマンドでJinja2-2.10.3がインストールされるようです。

pip install Jinja2
# アップデートの場合
# pip install -U Jinja2

使ってみる

テンプレートの書き方は以下参考。
https://jinja.palletsprojects.com/en/2.10.x/templates/

単純な例


from jinja2 import Template
template = Template('Hello {{ name }}!')
print (template.render(name='アンジュ'))

出力結果

Hello アンジュ!

ループしてみる

from jinja2 import Template
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption}} </a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

</body>
</html>
'''
template = Template(html)
data = {
  'a_variable' : 'わっふる',
  'navigation' : [
    {'href':'http://hogehoge1', 'caption': 'test1'},
    {'href':'http://hogehoge2', 'caption': 'test2'}
  ]
}
print (template.render(data))

出力結果

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">

        <li><a href="http://hogehoge1">test1 </a></li>

        <li><a href="http://hogehoge2">test2 </a></li>

    </ul>

    <h1>My Webpage</h1>
    わっふる

</body>
</html>

テンプレートを外部ファイルに出してみる

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        <li><a href="{{ item.href }}">{{ item.caption}} </a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

</body>
</html>
from jinja2 import Environment, FileSystemLoader
data = {
  'a_variable' : 'わっふる',
  'navigation' : [
    {'href':'http://hogehoge1', 'caption': 'test1'},
    {'href':'http://hogehoge2', 'caption': 'test<>2'}
  ]
}
from jinja2 import Environment, FileSystemLoader
env = Environment(loader = FileSystemLoader('./templates'))
template = env.get_template('001.tpl')
print (template.render(data))

出力結果

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">

        <li><a href="http://hogehoge1">test1 </a></li>

        <li><a href="http://hogehoge2">test<>2 </a></li>

    </ul>

    <h1>My Webpage</h1>
    わっふる

</body>
</html>

HTMLエスケープの方法

与えるデータに< , > , &, "が存在する場合のエスケープ方法には2種類ある。

手動エスケープ

手動エスケープはescapeフィルタをテンプレートの変数に指定する方法です。

templates/002.tpl

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">
    {% for item in navigation %}
        {# | e または | escape でエスケープ可能 #}
        <li><a href="{{ item.href }}">{{ item.caption | e}} </a></li>
    {% endfor %}
    </ul>

    <h1>My Webpage</h1>
    {{ a_variable }}

</body>
</html>
from jinja2 import Environment, FileSystemLoader
data = {
  'a_variable' : 'わっふる',
  'navigation' : [
    {'href':'http://hogehoge1', 'caption': 'test&!1'},
    {'href':'http://hogehoge2', 'caption': 'test<>2'}
  ]
}
from jinja2 import Environment, FileSystemLoader
env = Environment(loader = FileSystemLoader('./templates'))
template = env.get_template('001.tpl')
print (template.render(data))
template = env.get_template('002.tpl')
print (template.render(data))

結果
エスケープしていないテンプレートとエスケープしたテンプレートを使用した場合の出力結果は以下になります。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">

        <li><a href="http://hogehoge1">test&!1 </a></li>

        <li><a href="http://hogehoge2">test<>2 </a></li>

    </ul>

    <h1>My Webpage</h1>
    わっふる

</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Webpage</title>
</head>
<body>
    <ul id="navigation">

        <li><a href="http://hogehoge1">test&!1 </a></li>

        <li><a href="http://hogehoge2">test<>2 </a></li>

    </ul>

    <h1>My Webpage</h1>
    わっふる

</body>
</html>
自動エスケープ

Environmentを実行時にautoescapeを指定することでテンプレートにエスケープ処理を記載する必要がなくなります。

from jinja2 import Environment, FileSystemLoader
data = {
  'a_variable' : 'わっふる',
  'navigation' : [
    {'href':'http://hogehoge1', 'caption': 'test&!1'},
    {'href':'http://hogehoge2', 'caption': 'test<>2'}
  ]
}
env = Environment(loader = FileSystemLoader('./templates'), autoescape = True)
template = env.get_template('001.tpl')
print (template.render(data))

まとめ

単純な文字の書式を使う場合は以下の通りがいいようです。

  • 書式文字列に対してユーザ入力ある場合
    • テンプレート文字列
  • ユーザ入力ない場合
    • Python3.6以上
      • PEP498で定義された書式設定方法
    • Python3.6未満
      • 新しい形式の文字列の書式

HTMLのテンプレートライブラリとしてはJinja2がよく使われているようです。

WordPressで画像付きの記事を自動で投稿する

はじめに

WordPressのREST APIを使用して画像付きの記事を自動で投稿してみます。

環境

サーバー側
WordPress 5.2.4

クライアント側
Python 3.7.4

pip list
requests 2.22.0
Jinja2 2.10.3

事前準備

WordPressの認証には種類がいくつかありますが、今回はアプリケーションパスワードを使用します。

まずプラグインとしてApplication PasswordをWordPressにインストールします。

「ユーザ管理」→「あなたのプロフィール」画面で「Application Passwords」の項目が存在することを確認して投稿者権限を有するユーザ名を入力後、「Add User」ボタンを押下します。
image.png

パスワードが生成されるので控えておきます。
image.png

このユーザとパスワードの組み合わせでRESTAPIは動作しますが、ユーザ管理画面にはログインできません。また、管理画面から上記のパスワードをRevokeで取り消すことができます。

クライアントの実装例

まず、WordPressを操作するモジュールを用意します。

wordpress_ctrl.py

"""WORDPRESSの操作"""
import json
import os
import base64
import requests

class WordPressError(Exception):
    """WordPressのエラー情報"""
    def __init__(self, ctrl, status_code, reason, message):
        super(WordPressError, self).__init__()
        self.ctrl = ctrl
        self.status_code = status_code
        self.reason = reason
        self.message = message

class WordPressCtrl:
    """WordPressの操作"""

    def __init__(self, url, user, password):
        """初期化処理"""
        self.url = url
        auth_str = f"{user}:{password}"
        auth_base64_bytes = base64.b64encode(auth_str.encode(encoding='utf-8'))
        self.auth = auth_base64_bytes.decode(encoding='utf-8')

    def check_response(self, res, success_code):
        """WordPressからの応答をチェック"""
        try:
            json_object = json.loads(res.content)
        except ValueError as ex:
            raise WordPressError(self, res.status_code, res.reason, str(ex))
        if res.status_code != success_code:
            raise WordPressError(self, res.status_code, res.reason, json_object['message'])
        return json_object

    def add_post(self, title, content, categorie_ids=[], tag_ids=[]):
        """WordPressに記事を投稿"""
        headers = {
            'Authorization': 'Basic ' + self.auth
        }
        data = {
            'title': title,
            'content': content,
            'format': 'standard',
            'categories' : categorie_ids,
            'tags' : tag_ids
        }
        res = requests.post(f'{self.url}/wp-json/wp/v2/posts', json=data, headers=headers)
        return self.check_response(res, 201)

    def update_post(self, id, title, content, categorie_ids=[], tag_ids=[]):
        """WordPressの既存記事を更新"""
        headers = {
            'Authorization': 'Basic ' + self.auth
        }
        data = {
            'title': title,
            'content': content,
            'format': 'standard',
            'categories' : categorie_ids,
            'tags' : tag_ids
        }
        res = requests.post(f'{self.url}/wp-json/wp/v2/posts/{id}', json=data, headers=headers)
        return self.check_response(res, 200)

    def upload_media(self, path, content_type):
        """メディアのアップロード"""
        file_name = os.path.basename(path)
        headers = {
            'Authorization': 'Basic ' + self.auth,
            'Content-Type': content_type,
            'Content-Disposition' : 'attachiment; filename={filename}'.format(filename=file_name)
        }
        with open(path, 'rb') as media_file:
            data = media_file.read()
        res = requests.post(f'{self.url}/wp-json/wp/v2/media', data=data, headers=headers)
        return self.check_response(res, 201)

    def upload_png(self, path):
        """メディアにPNG画像を追加"""
        return self.upload_media(path, 'image/png')

    def upload_jpeg(self, path):
        """メディアにJPEG画像を追加"""
        return self.upload_media(path, 'image/jpeg')

つづいて、WORDPRESSに画像をアップロードして記事を追加するサンプルを以下に示します。
WordPressCtrlにはWORDPRESSのURL、ユーザー名、事前準備で作成したパスワードを指定してください。

from jinja2 import Template
from wordpress_ctrl import WordPressCtrl,WordPressError

data = {
    'data_list' : [
        { 
            'name': '阿多田太郎',
            'age' : 15,
            'image_path' : 'test1.png'
        },
        { 
            'name': 'アロハ太郎',
            'age' : 34,
            'image_path' : 'test2.png'
        }
    ]
}

wpctrl = WordPressCtrl('https://ワードプレスのURL', 'testuser', 'APIキー')

# 画像のアップロード
for item in data['data_list']:
    try:
        wpres = wpctrl.upload_png(item['image_path'])
        item['img_url'] = wpres['source_url']
    except WordPressError as ex:
        print(ex.status_code, ex.reason, ex.message)

html = """
<h2>結果</h2>
<table border=1>
    <tr>
      <th>名前</th>
      <th>年齢</th>
      <th>画像</th>
    </tr>
    {% for item in data_list %}
        <tr>
        <td>{{ item.name | e}}</td>
        <td>{{ item.age | e}}</td>
        <td><img src="{{ item.img_url | e}}" width=150/></td>
        </tr>
    {% endfor %}
</table>
"""
try:
    # 新規投稿
    wpres = wpctrl.add_post('タイトル', Template(html).render(data), [1], [3])
    print(wpres['id'])
    # 既存投稿の更新
    wpres = wpctrl.update_post(wpres['id'], 'タイトル(更新)', Template(html).render(data), [1,2], [3,4])
    print(wpres['id'], wpres['title'])

except WordPressError as ex:
    print(ex.status_code, ex.reason, ex.message)

上記のプログラムを実行するとメディアに2つ画像が追加されます。
image.png

また投稿一覧に記事が1つ追加されます。
image.png

記事のプレビューは下記の通りです。
image.png

参考: