JMockito 引数に応じて返す値を変化させる(Java モック)
これの JMockito 版です。
blueskyarea.hatenablog.com
テストコード(JUnit)
@RunWith(JMockit.class) public class BirthMonthTest { // Without mock @Test public void testGetBirthStoneWithoutMock() { BirthMonth birthMonth = new BirthMonth(2); assertThat(birthMonth.getBirthStone(2), is(nullValue())); assertThat(birthMonth.getBirthStone(7), is(nullValue())); assertThat(birthMonth.getBirthStone(11), is(nullValue())); } // With mock @Test public void testGetBirthStoneWithMock(@Mocked BirthMonth birthMonth) { new NonStrictExpectations() { { birthMonth.getBirthStone(2); result = "amethyst"; birthMonth.getBirthStone(7); result = "ruby"; birthMonth.getBirthStone(11); result = "topaz"; birthMonth.getBirthStone(anyInt); result = "not found"; } }; // exception if not "NonStrictExpectations". assertThat(birthMonth.getBirthStone(5), is("not found")); assertThat(birthMonth.getBirthStone(2), is("amethyst")); assertThat(birthMonth.getBirthStone(7), is("ruby")); assertThat(birthMonth.getBirthStone(11), is("topaz")); assertThat(birthMonth.getBirthStone(20), is("not found")); // exception if not "NonStrictExpectations". assertThat(birthMonth.getBirthStone(30), is("not found")); } }
Expectations を使用した場合、定義した振る舞いの順番どおりに、そのメソッドが呼ばれることが期待される。
この例では、まず最初に 2 が引数として与えられた場合の振る舞いが定義されている。
ただ、assertThat では最初に 5 が引数として与えられることになっているため、この時点でエラーになる(expect 2, but 5)。
また、最後の assertThat では 30 を引数として与えているが、定義した振る舞いの anyInt は、その前の引数 20 に対して適用されるため(使用済みのような扱い)、30 の場合の振る舞いが定義していないということでエラーになる。
いずれの場合も以下のエラー。
UnexpectedInvocation: Unexpected invocation of:
NonStrictExpectations を使用してあげることで、この辺りの制約がゆるくなり、定義した振る舞いの数が呼び出しの数よりも少なかったとしても、anyInt で定義した振る舞いが適用されるか、もし anyInt を定義していなければ、null(初期値) が返却されるような動作になる。
機能的には問題ないと思うのですが、この辺りについては Mockito の方が若干分かり易い(書き方)気がします。
Mockito の場合。
when(birthMonth.getBirthStone(2)).thenReturn("amethyst");
JMockito でも、無理やり一行で書けますが。。
birthMonth.getBirthStone(2); result = "amethyst";
JMockito あるメソッドが任意の値を返す(Java モック)
これの JMockito 版です。
blueskyarea.hatenablog.com
Mockito 版の記事と同じクラスをテスト対象にしました。
テストコード(JUnit)
import mockit.Expectations; import mockit.Mocked; import mockit.integration.junit4.JMockit; @RunWith(JMockit.class) public class MemberTest { // Without mock @Test public void testGetMemberInfoWithoutMock() { Member member = new Member(1, "hoge"); assertThat(member.getPointCard().getPoint(), is(1000L)); } // With Mock @Test public void testGetMemberInfoWithMock(@Mocked Member member) { PointCard pointCard = new PointCard(1, "hoge", 2000L); new Expectations() { { member.getPointCard(); result = pointCard; } }; assertThat(member.getPointCard(), is(pointCard)); assertThat(pointCard.getPoint(), is(2000L)); } }
@RunWith(JMockit.class) アノテーションを付けないと、エラーで怒られました。
java.lang.IllegalStateException: JMockit wasn't properly initialized; check that jmockit.jar precedes junit.jar in the classpath (if using JUnit; if not, check the documentation)
classpath 内において、jmockit.jar を junit.jar よりも先に定義しておく必要があるみたいです。
自分の環境下で上手く出来なかったため、代わりにこのアノテーションで回避してます。
結果
Mockito と同様のモックを生成することが出来ました。
Mockito では期待する動作を when().thenReturn() で定義していましたが、JMockito では Expectations で定義します。
この程度の内容であれば Mockito とどちらが使いやすいか検討するまでの違いは見当たりません。
Mockito と JMockito 名前は非常に似ていますが、モックの書き方は全く異なるので、プロジェクト単位でどちらかを使うかは決めた方が良さそうです。
「モックの作り方」についてネットの検索結果をそのまま適用していると、いつの間にか混在しているみたいなこともありそうです。
python bash のコマンドを実行
調べてみると色んなやり方が見つかりましたが、自分にとって一番シンプルだったやり方を。
import subprocess bashCommand = "ls -alt" output = subprocess.Popen(bashCommand, stdout=subprocess.PIPE, shell=True).communicate()[0] print(output)
コマンドの意味とか
subprocess.Popen
新しいプロセスで子のプログラムを実行してくれる。
stdout=subprocess.PIPE
標準出力をパイプする。コンソールに表示させない。
communicate() はタプル (stdoutdata, stderrdata) を返す。
戻り値のタプルから None ではない値を取得するためには、 stdout=PIPE または stderr=PIPE を指定しなければならない。
shell=True
shell が True なら、指定されたコマンドはシェルによって実行される。
Popen.communicate(input)
プロセスと通信する。
end-of-file に到達するまでデータを stdin に送信し、stdout および stderr からデータを受信する。
オプション引数 input には子プロセスに送られる文字列か、あるいはデータを送らない場合は None を指定する。
受信したデータはメモリにバッファされるため、返されるデータが大きい場合はこのメソッドを使うべきではない。
結果
$ ls -alt 合計 12 drwxrwxr-x 2 xx xx 4096 6月 25 23:18 . -rw-rw-r-- 1 xx xx 339 6月 25 23:18 execBashCommand.py drwxrwxr-x 9 xx xx 4096 6月 25 22:40 ..
$ python execBashCommand.py 合計 12 drwxrwxr-x 2 xx xx 4096 6月 25 23:22 . -rw-rw-r-- 1 xx xx 309 6月 25 23:22 execBashCommand.py drwxrwxr-x 9 xx xx 4096 6月 25 22:40 ..
普通に bash で実行したコマンドと同じ結果が得られた。
今回、stdout が欲しいから、communicate()[0] としている。
もし、communicate()[1] とした場合
$ python execBashCommand.py None
stderr の値が(実際には存在しないから None)が出力されている。
pyspark TypeError: namedtuple() missing 3 required keyword-only arguments
$ /usr/local/spark/bin/spark-submit --master local[1] textStream.py Traceback (most recent call last): File "/home/mh/workspace/spark/pyspark-practice/textStream.py", line 1, in <module> from pyspark import SparkContext ------- pyspark TypeError: namedtuple() missing 3 required keyword-only arguments
タイトルのエラーメッセージ、最初の import の部分で躓いていました。
from pyspark import SparkContext
調べてみると、spark 2.1.0 以下のバージョンについては、python 3.6 をサポートしていないらしい。
現在使っている python のバージョンは、3.6.3 で spark のバージョンは 1.6
$ python Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49)
とりあえず、既に python 2.7.6 もインストール済みだったので python 側のバージョンを下げてみることに。
.bashrc に以下を適用することで、デフォルトのバージョンが変更が変更される。
# default python alias python='/usr/bin/python2.7'
$ python Python 2.7.6 (default, Mar 22 2014, 22:59:56)
よしこれで大丈夫!再実行!!したが、同じエラー。。。
pyspark で使用する python のバージョンは PYSPARK_PYTHON で定義してあげる必要がありました。
export PYSPARK_PYTHON=/usr/bin/python2.7
$ /usr/local/spark/bin/spark-submit --master local[1] textStream.py
これでエラーは表示されなくなりました。
VirtualBox 仮想ディスクのサイズを変更
VirtualBox で使用している仮想ディスクの使用率が99パーセント近くになってしまったので、ホストOSからのディスクサイズの割り当てを増やすことを決断。
Vagrantを使用しているため、仮想ディスクは VMDK 形式になっています。
ただこの形式は、サイズ変更がサポートされていないため、一旦 VDI 形式に変換してあげる必要がありました。
おおまかな手順
1. VMDK 形式から VDI 形式へ変換
2. ディスク割り当て量を増やす(リサイズ)
3. VDI 形式から VMDK 形式へ変換
4. パーティション変更
1. VMDK 形式から VDI 形式へ変換
既存の仮想ディスクの一覧
$ VBoxManage list hdds UUID: 8cbbe27d-8feb-4444-88ce-e5df4fe5ac71 Parent UUID: base State: created Type: normal (base) Location: C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\box-disk1.vmdk Storage format: VMDK Capacity: 30720 MBytes Encryption: disabled
既存でおよそ30GB割り当てられています。
VDI 形式へ変換(別ファイルで生成)
$ VBoxManage clonehd "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\box-disk1.vmdk" "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" --format vdi
既存のファイル(VMDK)はそのままで、新しいファイル(VDI)が生成されることになります。
そのため、ディスクの空き容量に注意が必要です!
この処理には1時間以上かかりました。
$ c:\Program Files\Oracle\VirtualBox>VBoxManage clonehd "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\box-disk1.vmdk" "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" --format vdi 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Clone medium created in format 'vdi'. UUID: 1a09f8cf-6eb8-40af-a233-f797810a31f3
$ VBoxManage showhdinfo "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" UUID: 1a09f8cf-6eb8-40af-a233-f797810a31f3 Parent UUID: base State: created Type: normal (base) Location: C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi Storage format: vdi Format variant: dynamic default Capacity: 30720 MBytes Size on disk: 30249 MBytes Encryption: disabled
出来上がりました。
2. ディスク割り当て量を増やす(リサイズ)
$ VBoxManage modifyhd "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" --resize 61440
約60GBに増やしています。
この処理は一瞬で完了しました。
$ VBoxManage showhdinfo "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" UUID: 1a09f8cf-6eb8-40af-a233-f797810a31f3 Parent UUID: base State: created Type: normal (base) Location: C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi Storage format: vdi Format variant: dynamic default Capacity: 61440 MBytes Size on disk: 30249 MBytes Encryption: disabled
Capacity が増えていますね。
3. VDI 形式から VMDK 形式へ変換
$ VBoxManage clonehd "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\clone-disk1.vdi" "C:\Users\xx\VirtualBox VMs\Ubuntu14.04-2-box\box-disk2.vmdk" --format vmdk 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Clone medium created in format 'vmdk'. UUID: f52bff5a-4e35-4148-bfa4-2f3781ffe989
実際に計っていませんが、この処理も時間がかかりました。
これも厳密には変換ではなく、新しい VMDK ファイルが生成されることになります。
よって、元のファイルと合わせると3つの仮想ディスクファイルが保持されることになります。
box-disk1.vmdk (元の VMDK ファイル) 30GB
clone-disk1.vdi (変換用の vdi ファイル) 30GB
box-disk2.vmdk (変換後の新しい VMDK ファイル) 30GB
そのため、ディスクの空き容量に注意が必要です!!
変換後の新しい VMDK ファイル は 60GB になるのでは? と思っていたのですが、30 GB ほどでした。
恐らく実データが入るようになってから、喰われるのではないかと思います。
box-disk1.vmdk と clone-disk1.vdi は作業完了後に削除して良いと思います。
Java ソート条件を動的に指定してみたい
Java8 で書いてます。
静的に指定
// Item("name", "price", "reviewAve", "reviewNum") Item itemA = new Item("itemA", 1000, 3.3f, 100); Item itemB = new Item("itemB", 2000, 4.5f, 20); Item itemC = new Item("itemC", 3000, 4.5f, 10); List<Item> items = Arrays.asList(itemA, itemB, itemC); // static sort condition System.out.println("sort by price desc"); items.sort(Comparator.comparing(Item::getPrice).reversed()); // <- こんな感じで指定 items.forEach(item -> System.out.println(item.getName()));
こんな感じで指定すれば、"price" の降順でソートされます。
sort by price desc itemC itemB itemA
もし自分たちが欲しい結果がいつも "price" の降順なのであれば、これで十分かもしれません。
ただ、もし"price" の昇順で結果が欲しい時があれば、このプログラムでは期待する結果を返してくれません。
動的に指定してみたい
設定ファイルやプログラムの引数などから、ソート条件を与えてあげて、それに基づいてソートして欲しい。
ここでは、以下の手順での実現を考えてみます。
1. 想定されるソートの条件を Map に格納(key に条件、value にComparator)おく
2. プログラムの引数でソート条件を与える
3. 与えられたソート条件を基に、Map から Comparator を取得する
4. 取得した Comparator を基に、ソートする
ソート条件を保持するクラスを定義する
public final class SortCondition { private final String field; private final Direction direction; public SortCondition(String field, Direction direction) { this.field = field; this.direction = direction; } @Override public boolean equals(Object obj) { if (obj instanceof SortCondition) { SortCondition condition = (SortCondition) obj; return this.field.equals(condition.field) && this.direction.equals(condition.direction); } else { return false; } } @Override public int hashCode() { return Objects.hash(field, direction); } }
昇順か降順か
protected enum Direction { ASC, DESC }
想定されるソート条件を Map に格納(定義)
Map<SortCondition, Comparator<Item>> comparatorMap = new HashMap<>(); comparatorMap.put(new SortCondition("price", Direction.ASC), Comparator.comparing(Item::getPrice)); comparatorMap.put(new SortCondition("price", Direction.DESC), Comparator.comparing(Item::getPrice).reversed()); comparatorMap.put(new SortCondition("reviewAve", Direction.ASC), Comparator.comparing(Item::getReviewAve)); comparatorMap.put(new SortCondition("reviewAve", Direction.DESC), Comparator.comparing(Item::getReviewAve).reversed()); comparatorMap.put(new SortCondition("reviewNum", Direction.ASC), Comparator.comparing(Item::getReviewNum)); comparatorMap.put(new SortCondition("reviewNum", Direction.DESC), Comparator.comparing(Item::getReviewNum).reversed());
ソート条件を引数から取得
private List<SortCondition> getSortCondition(String[] args) { List<SortCondition> sortConditions = new ArrayList<>(); for (int i = 0; i < args.length; i++) { String[] condition = args[i].split(":", 0); sortConditions.add(new SortCondition(condition[0], Direction.valueOf(condition[1]))); } return sortConditions; }
ソート条件から Comparator を構築
private Comparator<Item> comparatorBuilder(List<SortCondition> conditions) { Comparator<Item> dynamicComparator = this.comparatorMap.get(conditions.get(0)); for (int i = 1; i < conditions.size(); i++) { dynamicComparator = dynamicComparator.thenComparing(this.comparatorMap.get(conditions.get(i))); } return dynamicComparator; }
ソートする
private void sort(List<Item> items) { String[] args = {"reviewAve:DESC", "reviewNum:DESC"}; List<SortCondition> sortConditions = getSortCondition(args); Comparator<Item> dynamicComparator = comparatorBuilder(sortConditions); System.out.println("sort by dynamic condition"); items.sort(dynamicComparator); items.forEach(item -> System.out.println(item.getName())); }
今回の例では、第1ソート条件を"reviewAve:DESC", 第2ソート条件を"reviewNum:DESC" としました。
sort by dynamic condition itemB itemC itemA
所感
一応、やりたかったことは実現できましたが、欠点はやはり想定されるソート条件を予め Map に格納(定義)しているところで、結局定義しているソート条件以外は指定できない。
それを引数に基づいて呼び出しているに過ぎないところです。
リフレクションとか使えば、もっと柔軟に Comparator を生成することができるのかもしれないですが。
Java 自作クラスを Map のキーにする
Map のキーは、int 型 や String 型で済ませることが多い。
自作クラスを Map のキーにする機会はたぶん今までなかった。
今回たまたま、それをする機会があったのだけど、get する時に果たして Map に格納されているものと等しいキーとして認識してくれるのだろうか?
作成したクラス
オブジェクトのソート条件を保持するクラス。
ソート対象フィールド名と、昇順・降順の何れかを保持する。
public final class SortCondition { private final String field; private final Direction direction; public SortCondition(String field, Direction direction) { this.field = field; this.direction = direction; } }
Map の生成
こんな感じで Map を生成する。
Map<SortCondition, Comparator<Item>> comparatorMap = new HashMap<>(); comparatorMap.put(new SortCondition("reviewAve", Direction.ASC), Comparator.comparing(Item::getReviewAve));
Map から get してみる
SortCondition condition1 = new SortCondition("reviewAve", Direction.ASC); Comparator<Item> comparator = this.comparatorMap.get(condition1); System.out.println(comparator); // <- null
見た目上は、同じキーを生成しているつもりでも、インスタンスが異なるので、値は取得できず comparator は null。
Map に入っているものと一致するキーにするためには?
SortCondition 内で hashCode() と equals() を正しくオーバーライドする必要があります。
SortCondiiton クラスに以下を追記します。
@Override public boolean equals(Object obj) { if (obj instanceof SortCondition) { SortCondition condition = (SortCondition) obj; return this.field.equals(condition.field) && this.direction.equals(condition.direction); } else { return false; } } @Override public int hashCode() { return Objects.hash(field, direction); }
equals() の結果が true で、hashCode() の結果が両オブジェクトで等しくなるので、この状態であれば一致するキーがあった時、Mapから対応する値が取得されるようになります。
SortCondition condition1 = new SortCondition("reviewAve", Direction.ASC); Comparator<Item> comparator = this.comparatorMap.get(condition1); System.out.println(comparator); // <- 指定したキーで、値(null ではない) が取得できた