HackToTech

Hack To Technology

KotlinでmockStaticを使おうとしてちょっとハマった話

kotlin 書いてて、mockito-inlinemockStatic を使おうとして
少し時間無駄にしたので、他の人が同じことで時間を無駄にしないように書いとく

tl;dr

@JvmStatic のAnnotationをつける必要がある
普通に考えれば当たり前なんだけど、つけ忘れてた時にエラー出てなんでだっけ?ってなる

object ExampleObject {
    @JvmStatic //これ
    fun static(): String {
        return "static"
    }
}

なぜか

じゃないとバイトコードになった時に static にならないので結果としてmock出来ずに失敗する

// ================com/example/springkotlin/utils/ExampleObject.class =================
// class version 52.0 (52)
// access flags 0x31
public final class com/example/springkotlin/utils/ExampleObject {


  // access flags 0x19
  public final static static()Ljava/lang/String;
  @Lkotlin/jvm/JvmStatic;()
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 6 L0
    LDC "static"
    ARETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x11
  public final notStatic()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 10 L0
    LDC "notStatic"
    ARETURN
   L1
    LOCALVARIABLE this Lcom/example/springkotlin/utils/ExampleObject; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x2
  private <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/springkotlin/utils/ExampleObject; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x19
  public final static Lcom/example/springkotlin/utils/ExampleObject; INSTANCE
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 3 L0
    NEW com/example/springkotlin/utils/ExampleObject
    DUP
    INVOKESPECIAL com/example/springkotlin/utils/ExampleObject.<init> ()V
    ASTORE 0
    ALOAD 0
    PUTSTATIC com/example/springkotlin/utils/ExampleObject.INSTANCE : Lcom/example/springkotlin/utils/ExampleObject;
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 1

  @Lkotlin/Metadata;(mv={1, 6, 0}, k=1, d1={"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u000e\n\u0002\u0008\u0002\u0008\u00c6\u0002\u0018\u00002\u00020\u0001B\u0007\u0008\u0002\u00a2\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0008\u0010\u0005\u001a\u00020\u0004H\u0007\u00a8\u0006\u0006"}, d2={"Lcom/example/springkotlin/utils/ExampleObject;", "", "()V", "notStatic", "", "static", "spring-kotlin"})
  // compiled from: ExampleObject.kt
}

エラー

エラーとしてはこんなのが出るけどぱっと見ですぐに気がつきにくい

when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.

コード

package com.example.springkotlin.utils

object ExampleObject {
    @JvmStatic
    fun static(): String {
        return "static"
    }

    fun notStatic(): String {
        return "notStatic"
    }
}

テスト

package com.example.springkotlin.utils

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mockStatic

class ExampleObjectTest {
    @Test
    fun test() {
       // 雑
        val mocked = mockStatic(ExampleObject::class.java)
        mocked.`when`<String>(ExampleObject::static).thenReturn("mocked")
        assertEquals("mocked", ExampleObject.static())
        mocked.close()
    }
}

Atlantisのカスタムワークフローで実行されるTerraformのバージョンが指定と異なる

issueにもなってた

github.com

なんか見栄えが悪いけど現状こうするしかないっぽい

version: 3
projects:
  - name: test
    dir: projects/test
    terraform_version: 1.1.6 #明示的に指定したい
workflows:
  default:
    plan:
      steps:
        - init
        # これはterraform_versionで実行される
        - plan
        # これはdefaultのterraform versionで実行される
        - run: terraform plan -input=false -refresh -no-color -out $PLANFILE
        # 明示的に指定することでterraform_versionで実行される
        - run: terraform$ATLANTIS_TERRAFORM_VERSION plan -input=false -refresh -no-color -out $PLANFILE
...

Terraform detected the following changes made outside of Terraformを無視することにした

実質メモ書き
尚、未だに公式的に回避する手立てがあるわけではないので、そこは実装されるのを待つしかない。

Terraform0.15.4 以降、これが出てくるようになった。
現状 ignore_changes するぐらいしかろくな回避手段がないが、ignore_changes は別にその為に使うもんでもないので悩んでいた。
(IAMとかのPolicyの変更ノイズが個人的にとてもつらいし、長いログが出力されてしまうのがきつい)
それでまあ Issue を眺めてたら言及されている Issue を見つけた。

github.com

荒れに荒れて10月にはIssue自体がロックされてしまったらしい。
(追記) 1.2でなんか対応入ったっぽい

個人的にはリフレッシュに対する差分が長すぎて、
変更差分を見落とすほうがつらいのでIssueのコメントにかかれているように、sed で無視することにした。
lockされているとコメントにスタンプ押せないのでこの場で感謝。
早くリソースの Attribute レベルで更新差分を無視することが出来るようになってほしい。

terraform plan | grep -v "Refreshing state..." | sed '/Objects have changed outside of Terraform/,/────────────/d'

auroraでフェイルオーバーした際にコネクションの接続がぜんぜん切り替わらなくて色々見てたのでメモ

環境

  • Aurora postgresql
  • Spring Boot
    • driver は org.postgresql.driver
  • Doma

メモ

AWSのベストプラクティスにも書かれているやつで、targetServerTypeprimary に指定しておけばFOした際に自動的にコネクションが再接続される

jdbc:postgresql://myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432,myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432/postgres?user=<primaryuser>&password=<primarypw>&loginTimeout=2&connectTimeout=2&cancelSignalTimeout=2&socketTimeout=60&tcpKeepAlive=true&targetServerType=primary

docs.aws.amazon.com

targetServerType = String

Allows opening connections to only servers with required state, the allowed values are any, primary, master, slave, secondary, preferSlave and preferSecondary. The primary/secondary distinction is currently done by observing if the server allows writes. The value preferSecondary tries to connect to secondary if any are available, otherwise allows falls back to connecting also to primary. jdbc.postgresql.org

ただAWS公式だと参照系クエリをreplicaに逃がす部分の話はなかったので、一応pgjdbcの実装を確認した github.com

HostStatus hostStatus = HostStatus.ConnectOK;
if (candidateHost.targetServerType != HostRequirement.any) {
  hostStatus = isPrimary(queryExecutor) ? HostStatus.Primary : HostStatus.Secondary;
}
GlobalHostStatusTracker.reportHostStatus(hostSpec, hostStatus);
knownStates.put(hostSpec, hostStatus);
if (!candidateHost.targetServerType.allowConnectingTo(hostStatus)) {
  queryExecutor.close();
  continue;
}

pgjdbc/ConnectionFactoryImpl.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

Tuple results = SetupQueryRunner.run(queryExecutor, "show transaction_read_only", true);

で接続先がreadonlyかどうかチェックしているらしい

pgjdbc/ConnectionFactoryImpl.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

  primary {
    public boolean allowConnectingTo(@Nullable HostStatus status) {
      return status == HostStatus.Primary || status == HostStatus.ConnectOK;
    }
  },
  secondary {
    public boolean allowConnectingTo(@Nullable HostStatus status) {
      return status == HostStatus.Secondary || status == HostStatus.ConnectOK;
    }
  },

allowConnectingTo でコネクションが接続先の条件(targetServerType で指定した HostStatus )にマッチしているかを確かめて、マッチしていなければクローズするみたいな実装にしているらしい

pgjdbc/HostRequirement.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

最終的に 書き込み用のコネクションプールの接続をjdbc:postgresql://<cluster_endpoiont>?targetServerType=primary
参照用のコネクションプールの接続をjdbc:postgresql://<cluster_readonly_endpoint>?targetServerType=secondary
にしていい感じになった
他にもベストプラクティスのパラメータ足したり、SpringBoot使ってるのでHikariCP周りの設定した