昨今mockの話が続いていますが、今週もmockに関してです。
今回は特にexecにより外部コマンドを呼び出す処理の代替についてと言う話であり、かつそのコマンドの出力結果が複数行に渡るJSON形式の場合という、かなりマニアックなネタです。
execの仕様
そもそもexecの仕様ですが、下記のようになっています。
exec(string $command, array &$output = null, int &$result = null)
第一引数には実行したいコマンドを指定します。
コマンドがオプション等を必要とする場合はそれらも含んだ文字列を指定します。
要は画面上から直接コマンドを実行する際に入力する書式をそのまま第一引数に指定すれば良いだけです。
第二引数にはコマンドの実行結果(標準出力)を格納する配列を指定しますが、本引数には2つ注意点があります。
1つ目は文字通り配列であること。コマンドの実行結果が複数行で出力される場合、その各行が配列の1つの要素になります。この点が処理をしていて意外に扱いづらいです。
2つ目は参照渡しになっていること。同じ変数を使い回している場合で配列が空でなかった場合、execの実行結果は同配列に追加される形になります。純粋に当該実行結果のみを取得したい場合は配列の内容をunset等を使ってクリアしておく必要があります。
第三引数にはコマンドの実行結果が格納されます。
厳密にはそれぞれのコマンドの仕様を確認する必要がありますが、一般的にLinux(Unix)におけるコマンドの実行結果は、正常終了時は0、異常終了時は0以外(コマンド依存)となっています。よって単純にコマンドの実行が正常に行えたかどうかを判定するためには第三引数に返された値が0かどうかを確認すれば良いと言うことになります。
JSON形式の実行結果を返すコマンドのmock
上記のような仕様のexecを使ってJSON形式の実行結果を返すコマンドの実行をmockで代替するケースを考えます(具体的にはAWS CLIの実行結果を想定していたりしますが)。
例えば、以下のようなJSON形式の実行結果を返すケースを想定します。
{
"a": {
"a-1": 1,
"a-2": "abc"
},
"b": {
"b-1": 2,
"b-2": "xyz"
}
}
上記出力結果はexecの第二引数には以下のように1行を1要素とし、10個の要素を持つ配列として格納されます。
Array
(
[0] => {
[1] => "a": {
[2] => "a-1": 1,
[3] => "a-2": "abc"
[4] => },
[5] => "b": {
[6] => "b-1": 2,
[7] => "b-2": "xyz"
[8] => }
[9] => }
)
上記はprint_rの出力結果ですが、インデント分のスペースを含む文字列になっていることが分かりにくいですね。
合わせてvar_dumpの出力結果も示しておきます。
array(10) {
[0]=>
string(1) "{"
[1]=>
string(10) " "a": {"
[2]=>
string(17) " "a-1": 1,"
[3]=>
string(20) " "a-2": "abc""
[4]=>
string(6) " },"
[5]=>
string(10) " "b": {"
[6]=>
string(17) " "b-1": 2,"
[7]=>
string(20) " "b-2": "xyz""
[8]=>
string(5) " }"
[9]=>
string(1) "}"
}
こちらはこちらで見難いですが、スペースを含む文字列になっていることは確認できるかと思います。
execを実行し、上記のような出力を取得する処理をmockで代替しようと思った場合、andReturnに上記のような配列を渡す必要がありますが、このような配列を直接生成するのは結構大変です。
固定的なデータを返すだけであれば実際にexecを実行して取得した出力結果をファイル等で保持しておいて、そこから上記のような配列を生成することはできるかと思いますが、できれば色々とデータの内容を変えながらテストできるようにしたいです。
構造化されたデータを加工しやすい形で持とうと思うと、やはり配列(連想配列)の形で保持しておきたくなります。
先のJSONに変換される配列であれば以下のような形になります。
Array
(
[a] => Array
(
[a-1] => 1
[a-2] => abc
)
[b] => Array
(
[b-1] => 2
[b-2] => xyz
)
)
上記からexecの第二引数のような形の配列に変換することを考えます。
まず、上記配列からJSON形式への変換を行いますが、単純にjson_encode関数を使用すると以下のようになります。
{"a":{"a-1":1,"a-2":"abc"},"b":{"b-1":2,"b-2":"xyz"}}
json_encode関数で元配列を期待するような複数行に渡る整形されたJSON形式で出力するためには、第二引数にJSON_PRETTY_PRINTを指定します。
さらにこのJSONをexecの出力結果のような形にするためには1行を1要素とする配列に変換する必要がありますが、これは改行をセパレータとするexplode関数で行えます。
と言うことで、まとめると以下のような変換を行う処理を行えば良いことになります。
// $baseArrayが元配列
$outputArray = explode("\n", json_encode($baseArray, JSON_PRETTY_PRINT));
この結果($outputArray)をmockのandReturn関数の引数とすることで、同出力を期待する処理を代替できるようになります。
総括
上記で例示した程度の構造であれば力技での対処も可能かと思いますが、例えばCloudFrontに対する「get-distribution-config」の実行結果辺りになると出力される構造がかなり複雑になるので、体力より頭を使うしかなくなってきます。
と言うことで頭を使ってみたところ、今回のケースではかなりシンプルな形に落ち着きました。
プログラミングを行なっていると直感的に思いつく方法が結構複雑なものであると言うケースはしばしばあります。
この時、素直に複雑な処理を書くか、頑張って頭を使ってみるかは、コストパフォーマンス的観点で意外に難しい選択です。
「下手の考え休むに似たり」と言うことも往々にしてありますので。
また、一旦は思いついた方法で対処しておき、後からリファクタリングでより良い形に書き換えようと考えることもしばしばですが、実際には開発終盤にリファクタリングの余裕など残らないことの方が多く、結局はごちゃごちゃした処理がそのまま残ってしまうことになりがちです。
知力と体力の使い方のバランスの良さも、技術者の技量の一つと言ったところでしょうか。