社内se × プログラマ × ビッグデータ

プログラミングなどITに興味があります。

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 は作業完了後に削除して良いと思います。

4. パーティション変更

ディスクの割り当て量は増えてバンザイ!だと思ったのですが、df コマンドで確認しても領域は増えていません。。
まだ増やした領域はパーティションとして未割り当て領域になっているため、割り当てる必要がありました。
fdisk よりも、専用のツール GParted とか使った方が簡単で良いかもしれません。

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 ではない) が取得できた
所感

必要になるケースは少ないと思いますが、やや複雑な条件における Key に基づく Value を管理したい場合、自作クラスをキーとして扱うことがあるかもしれません。
equals() についてはイメージがつきやすいですが、hashCode() についてもケアしないといけません。
オーバーライドしないと、Object の hashCode がそのまま使われるので、Mapに入っているキーと実際に使おうとしているキーのインスタンスが異なることから、返却されるハッシュ値も異なります。