きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Javaで配列の特定の条件を満たすデータのみを操作したかった

※ 泥臭いやり方でしか実践できていません。


JavaのLINEオープンチャットにて、「配列内部の特定条件のみ変換したい」という質問がありました。

今後、同様の質問が来たときにスマートな回答ができるようにいくつか検証したメモを残します。なお冒頭にも書いてあるとおり、泥臭いやり方での変換ですので、コメントやTwitterにてよりよいコードを教えていただけると非常に喜びます。

環境

  • Java
    • 17

仕様

次の仕様を満たせるようにします。

[When]
パラメータとして、1,2,3の配列を渡す。
[Then]
・偶数は+100する
・奇数は+50する
・※ 期待値は各案ごとに異なる

対応案1

拡張For文かStream式のMapに条件分岐と処理を記載する

一番素直に処理したパターンです。配列内の順序等も変更したくないのであれば、これが一番ベストです。

条件分岐と処理が混ざっているので嫌ですが、1週回ってシンプルな処理となります。

  @Test
  @DisplayName("""51, 102, 53の配列に変換する
      """)
  void test_01_01() {
    var base = Stream.of(1, 2, 3);

    var actual =
        base.map(e -> {
          if (e % 2 == 0) {
            return e + 100;
          } else {
            return e + 50;
          }
        }).collect(Collectors.toList());

    assertThat(actual).isEqualTo(Arrays.asList(51, 102, 53));
  }

対応案2

必要な処理を抽出してから操作し、マージする

配列内の順序は無視してよければ、こちらの方法も使用できます。

処理対象となる元の配列をSupplierクラスに変換します。Streamクラスを取得するためにSupplier#getを行う必要があります。

条件分岐ごとにStreamを分離させて処理させるため、配列内の順序が重要であれば結合させたのちにソートする必要があります。

少々面倒ですが、各抽出処理と操作を分離できるため、あとから確認したときに役割が分かりやすくなります。

  @Test
  @DisplayName("""
      ・合計値102, 51, 53を返却する
      """)
  void test_01_02() {
    Supplier<Stream<Integer>> base = () -> Stream.of(1, 2, 3);

    var evenList =
        base.get()
            .filter(e -> e % 2 == 0)
            .map(e -> e + 100);

    var oddList =
        base.get()
            .filter(e -> e % 2 != 0)
            .map(e -> e + 50);

    var actual = Stream.concat(evenList, oddList);

    assertThat(actual.collect(Collectors.toList()))
        .isEqualTo(Arrays.asList(102, 51, 53));
  }

なお、Supplierクラスを経由せずにStreamクラスでそのまま処理しようとすると次のエラーが発生します。

stream has already been operated upon or closed
java.lang.IllegalStateException: stream has already been operated upon or closed

対応案2の例だと偶数に+100処理した後、Streamが閉じられてしまうため、奇数の処理をしようとしたときにエラーが発生しています。Supplierクラスを挟んでいることで、Supplier#getをするたびに新しいStreamが生成されているため、問題が起こりません。

コンパイルエラーではなく、ランタイムエラーで発生するため、ちゃんとテストして確認しましょう。

ソースコード

※ CI/CDは落ちていますが、テスト自体はとおっています。

終わりに

JavaGoldの試験にてpeekメソッドを使用すると意図的に副作用を起こせるという認識がありました。ですので、「filterメソッドとpeekメソッドで期待どおり変換できるのでは」と回答したのですが検証すると嘘であることがわかりました。peekメソッドの使い方に関しては、別の記事にしたいとは考えています。

最終的には、対応案1で記載することは多いでしょうが、対応案2で処理できる可能性を見いだせたことは私のJavaスキルの成長だと考えることにします。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20211113160834p:plain

SpringBootで実際のDBにつなげてユニットテストする

SpringBootで実際に起動してあるDBを使ったユニットテストをしようとすると、デフォルトだと動かないようです。

正確にはEmbeddedDataSourceBeanFactoryPostProcessor(ログだと長すぎるのでbeddedDataSourceBeanFactoryPostProcessorとして表示される)やEmbeddedDatabaseFactoryが起動して組込みDBとして動こうとするようです。(詳しい挙動はよくわかりませんでした)

回避方法としては、実際のDBを使用してテストする、Flyway + H2のインメモリDBを使用する、TestContainersを使用して起動後にJDBC_URLを上書きする等が必要です。

今回の記事では、実際のDBを使用してテストする方法を残します。

環境

  • Java
    • 17
  • org.springframework.boot
    • 2.4.5
  • org.postgresql
    • 42.2.23

設定ファイルで制御する

設定ファイルに組込みDBの無効化設定をすることで、実際のDBに対して接続できます。

spring.test.database.replace: none # any, AUTO_CONFIGURED が設定できます。

上の設定をすることで、各テストクラスに対して下のアノテーションが追加されているものと同じ状態になります。ただしコードで設定してしまうと、H2で行うテストとの切り替えが面倒になってしまいますので、設定ファイルで切り替えるようにした方がよいでしょう。

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

ソースコード

終わりに

もともとFlyway + H2でテストをしていたこともあり、実際のDBでテストする方法に気付けませんでした。

画像のとおり、設定ファイル上はURLをjdbc:postgresql://127.0....と設定しているのに、テスト上有効になっているURLがjdbc:h2:mem:6f9309e0-...とH2の設定で上書きされていたので、かなり混乱していました…。

f:id:nainaistar:20211018000048p:plain

ただ、何が原因でこれが起こっているのかが正直分かっていません。職場でテストしていた時はこの設定を追加していないはずなのですが…。

こういう細かいところの挙動は全然抑えられてないので、Springはまだまだ勉強することがありますね。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20211018000039p:plain

Pytestでコマンドオプションからパラメータを渡す

Pytestでコマンドのオプションからパラメータを渡すことができないか調べたときのメモ。似た記事はたくさんありますので、詳しいことは参考情報を見てください。

環境

  • Python
    • 3.8.10
  • pytest
    • 6.2.4

ゴール

Pytestのコマンドオプションからパラメータを渡し、テストコード上で値を取得する。

pytest -s 
# デフォルト値のITがテストクラスに渡される

pytest -s --env=IT
# ITがテストクラスに渡される

pytest -s --env=ST
# STがテストクラスに渡される

要約

  • デフォルトではパラメータを受け取れるようなオプションは存在しない
  • conftest.pyでコマンドラインからオプションとパラメータを受け取れるようにする
  • pytest.fixtureでテストメソッドにパラメータを渡す

    対応

    オプションとパラメータを受け取る準備をする

デフォルトでは、Pytestでオプションとパラメータを自由に受け取れるようなものはありません(pytest --vars=hogeのように、すぐに使える--varsオプションはありません)。

conftest.pyでパラメータを受け取れるようにする必要があります。

conftest.pyにメソッドのpytest_addoption(parser)を作成します。パラメータのparser#addoptionでオプション名等を指定します。

※ このparserはoptparseargparseのどちらかだと思いますが、調べ方がわかりません。Pytestで調べても出てこないので、参考程度に見ると勉強になります。

def pytest_addoption(parser):
    parser.addoption('--env', # オプション名
                    default='IT' # デフォルト値
                    )

オプションからパラメータを受け取る

conftest.pypytest.fixtureを付与したメソッドを追加します。このメソッドではrequestを受け取ります。

パラメータのrequestからrequest.config#getoptionを使用します。getoptionにオプション名を指定して、パラメータを取得します。

@pytest.fixture
def env(request):
    return request.config.getoption("--env")

テストメソッドにパラメータを渡す

テストメソッドにfixtureを付与したメソッド名と同名のパラメータを付与します。自動でマッピングして、パラメータに渡されます。

def test_env(env):
    print(env)

結果確認

pytest -s
# ITが出力される(デフォルト値)

pytest -s --env=IT
# ITが出力される

pytest -s --env=ST
# STが出力される

備考

正直、Pytestでコマンドラインでオプションを渡すと処理が非常に面倒です。MacやLinuxであれば、環境変数に指定する方法も簡単なので、そちらを使用したほうが良いかもしれません。

なお、Windowsはこの書き方での環境変数の設定ができないので、注意です。

import os

def test_(env):
    print(os.environ.get('ENV'))
# コマンドの手前に指定すると環境変数に設定できる
ENV=IT pytest -s 
# ITが出力される

ENV=ST pytest -s 
# STが出力される

ソースコード

終わりに

Pytestの理解が浅いので、参考情報を読んでも時間がかかってしまったので自分でブログに残しておきます。

正直、だいたい環境変数でなんとかできるケースしか出会っていないので、Windows環境以外はあんまり頑張る必要はないでしょう。

参考情報