はじめに
ファイルを読み込むメソッドを作成しようとする。引数として考えるのが、
・ファイル名のString型
・ファイルのFile型
が考えられるが、InputStreamが良いのではないか、と結論付けた。
理由としては「テストしやすい」からである。
StringやFileだと、どうしてもファイルの場所を指定するので、ファイル構造を変更すると、ファイルの場所をすべて書き換えないといけない。そこでテストに使用するファイルはgetResourceAsStream()で指定し、テストクラスと同じ場所に置いてしまうのが、一番柔軟性が高いと判断した。
以下、テスト方法や実際の実装方法をまとめた。
実際のソース
ということで、テスト対象のメソッド。引数はInputStreamにしてます。
/**
* 一行で「グループ名:メンバー1,メンバー2,メンバー3」という形式のファイルを読み込む。<br/>
* メンバーをキーにして、どのグループに所属しているか確認できるMapを返す。
* @param inputStream 読み込みファイル
* @return メンバーをキーにして、どのグループに所属しているか確認できるMap
*/
public Map<String, String> read(InputStream inputStream){
try(BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, Charset.forName("UTF-8")))){
Map<String, String> result = new HashMap<>();
String line;
while((line = reader.readLine()) != null){
// グループ名
String group = line.substring(0, line.indexOf(":"));
// メンバー名
for (String member: line.substring(group.length() + 1).split(",")){
result.put(member, group);
}
}
return result;
}catch (IOException | IllegalArgumentException | StringIndexOutOfBoundsException e) {
// 複数例外catchを使用。初めて使うw
System.err.println("グループの設定ファイルがおかしい");
return null;
}
}
さて、「いざファイルを読み込ませよう!」と思う前に、不安だから一行だけの読み込ませてみるか。でも、一行のファイルを作るのはめんどくさいから、ByteArrayInputStreamで引数に渡すInputStreamをテストソース内で作成しよう。
@Test
public void read1行(){
// 一行お試し。メモリー上にストリームを作成して、テストを実施する
ByteArrayInputStream stream = new ByteArrayInputStream("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes());
InputStreamMethod target = new InputStreamMethod();
Map<String,String> result = target.read(stream);
assertThat(result.get("青森県"), is("東北"));
assertThat(result.get("秋田県"), is("東北"));
assertThat(result.get("福島県"), is("東北"));
}
複数行だと、どうかな?
@Test
public void read3行(){
StringBuffer sb = new StringBuffer();
sb.append("北海道:北海道").append(System.lineSeparator());
sb.append("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県").append(System.lineSeparator());
sb.append("関東:茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県").append(System.lineSeparator());
ByteArrayInputStream stream = new ByteArrayInputStream(sb.toString().getBytes());
InputStreamMethod target = new InputStreamMethod();
Map<String,String> result = target.read(stream);
assertThat(result.get("北海道"), is("北海道"));
assertThat(result.get("青森県"), is("東北"));
assertThat(result.get("神奈川県"), is("関東"));
}
よし、大丈夫そうだ。じゃあ、実際のファイルを読ませるか。
@Test
public void read() {
InputStreamMethod target = new InputStreamMethod();
Map<String,String> result = target.read(getClass().getResourceAsStream("InputStreamMethodTest.txt"));
assertThat(result.get("北海道"), is("北海道"));
assertThat(result.get("青森県"), is("東北"));
assertThat(result.get("秋田県"), is("東北"));
assertThat(result.get("福島県"), is("東北"));
assertThat(result.get("神奈川県"), is("関東"));
assertThat(result.get("鳥取県"), is("中国"));
assertThat(result.get("徳島県"), is("四国"));
assertThat(result.get("沖縄県"), is("九州"));
assertThat(result.get("うどん県"), is(nullValue()));
}
テストで読み込むテキストファイル。InputStreamMethodTest.javaと同じ場所に配置するのがミソです。
北海道:北海道
東北:青森県,岩手県,秋田県,宮城県,山形県,福島県
関東:茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県
中部:山梨県,長野県,新潟県,富山県,石川県,福井県,静岡県,愛知県,岐阜県
近畿:三重県,滋賀県,京都府,大阪府,兵庫県,奈良県,和歌山県
中国:鳥取県,島根県,岡山県,広島県,山口県
四国:香川県,愛媛県,徳島県,高知県
九州:福岡県,佐賀県,長崎県,熊本県,大分県,宮崎県,鹿児島県,沖縄県
ファイルのフォーマットがおかしかったときのテストもしておこう。
@Test
public void readファイルエラー(){
ByteArrayInputStream stream = new ByteArrayInputStream("東北青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes());
InputStreamMethod target = new InputStreamMethod();
assertThat(target.read(stream), is(nullValue()));
}
では、肝心のメイン文を、と。
public static void main(String[] args) throws FileNotFoundException {
File file = new File("src/streamtest/InputStreamMethodTest.txt");
if (! file.exists()){
System.err.println("ファイルがありません:" + file.getAbsolutePath());
System.exit(-1);
}
InputStreamMethod target = new InputStreamMethod();
Map<String, String> map = target.read(new FileInputStream(file));
map.keySet().stream()
.map(key -> key +":" + map.get(key))
.forEach(System.out::println);
}
ということで、完成。
ひとまず、まとめ
以前書いたソースの引数をStringにしていたが、Maven導入でフォルダ構成がちょっと変わったので、格好悪い感じになっています。どうやったら、うまくいくか、と考えたとき、InputStreamを引数にするのが柔軟性があるかなと思いました。実際業務でも書いてみましたが、いい感じに収まった気がします。
closeって入れ子でも呼ばれるのか
こう書いてみて、引数にとったInputStreamってちゃんとcloseしているのか、不安になりました。InputStreamMethod.readの内部でByteArrayInputStream streamを使用したInputStreamReaderをtryで囲んでいるが、この場合closeが呼ばれているか気になったのでテスト。呼ばれているようだ。
@Test
public void close(){
final StringBuilder sb = new StringBuilder();
ByteArrayInputStream stream =
new ByteArrayInputStream("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes())
{
@Override
public void close() throws IOException{
sb.append("stream was closed");
super.close();
}
};
InputStreamMethod target = new InputStreamMethod();
Map<String,String> result = target.read(stream);
assertThat(sb.toString(), is("stream was closed"));
}
@Test(expected = IOException.class)
public void closeFile() throws IOException {
InputStream stream = getClass().getResourceAsStream("InputStreamMethodTest.txt");
InputStreamMethod target = new InputStreamMethod();
Map<String,String> result = target.read(stream);
stream.read();
org.junit.Assert.fail("ファイルをcloseした状態でreadしているので、IOExceptionが発生するはず");
}
ファイル出力もOutputStreamを引数に?
ついでにファイル出力も引数をOutputStreamにした方が良いかとやってみた。
@Test
public void write_1データ() throws IOException {
OutputStreamMethod target = new OutputStreamMethod();
target.put("北海道", "北海道");
ByteArrayOutputStream out = new ByteArrayOutputStream();
target.write(out);
assertThat(out.toString(), is("北海道:北海道"));
}
@Test
public void write_1行() throws IOException {
OutputStreamMethod target = new OutputStreamMethod();
target.put("青森県", "東北");
target.put("岩手県", "東北");
target.put("秋田県", "東北");
ByteArrayOutputStream out = new ByteArrayOutputStream();
target.write(out);
assertThat(out.toString(), is("東北:岩手県,秋田県,青森県"));
}
@Test
public void write_3行() throws IOException {
OutputStreamMethod target = new OutputStreamMethod();
target.put("北海道", "北海道");
target.put("青森県", "東北");
target.put("岩手県", "東北");
target.put("秋田県", "東北");
target.put("茨城県", "関東");
target.put("栃木県", "関東");
target.put("群馬県", "関東");
ByteArrayOutputStream out = new ByteArrayOutputStream();
target.write(out);
String[] expected = {
"北海道:北海道"
,"東北:岩手県,秋田県,青森県"
,"関東:栃木県,群馬県,茨城県"
};
assertThat(out.toString(), is(String.join(System.lineSeparator(), expected)));
}
Map<String, String> map = new HashMap<>();
public String put(String key, String value ){
return map.put(key, value);
}
public void write(OutputStream outputtStream) throws IOException{
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputtStream));
String[] groups =
map.values().stream()
.distinct()
.sorted()
.toArray(count->new String[count]);
for (int i = 0; i < groups.length; i++){
final String group = groups[i];
String[] members =
map.keySet().stream()
.filter(p-> group.equals(map.get(p))) // 同じグループのみでフィルタリングしてる
.sorted()
.toArray(count -> new String[count]);
writer.write(group + ":" + String.join(",", members));
// 最終グループでないときは、改行を入れる
if (i != groups.length -1){
writer.newLine();
}
}
writer.flush();
}
public static void main(String[] args) throws FileNotFoundException, IOException {
File file = new File("src/streamtest/OutputStreamMethodTest.txt");
if (file.exists()){
System.err.println("ファイルがあります:" + file.getAbsolutePath());
System.exit(-1);
}
OutputStreamMethod target = new OutputStreamMethod();
target.put("北海道", "北海道");
target.put("青森県", "東北");
target.put("岩手県", "東北");
target.put("秋田県", "東北");
target.put("茨城県", "関東");
target.put("栃木県", "関東");
target.put("群馬県", "関東");
target.write(new FileOutputStream(file));
}
よし。いけてる。
ソースは
https://github.com/xaatw0/quiita/tree/master/src/streamtest
に落ちてます。