月: 2023年5月

AGP8.0対応後に「Unsupported class file major version 61」のエラーでテストが失敗する

mockkでユニットテストを書いている環境でAGP8.0に対応すると一部のユニットテストが失敗する場合があります。

java.lang.IllegalArgumentException: Unsupported class file major version 61
	at net.bytebuddy.jar.asm.ClassReader.<init>(ClassReader.java:196)
	at net.bytebuddy.jar.asm.ClassReader.<init>(ClassReader.java:177)
	at net.bytebuddy.jar.asm.ClassReader.<init>(ClassReader.java:163)
	at net.bytebuddy.utility.OpenedClassReader.of(OpenedClassReader.java:86)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:3824)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:2166)
	at net.bytebuddy.dynamic.scaffold.inline.RedefinitionDynamicTypeBuilder.make(RedefinitionDynamicTypeBuilder.java:224)
	at net.bytebuddy.dynamic.scaffold.inline.AbstractInliningDynamicTypeBuilder.make(AbstractInliningDynamicTypeBuilder.java:123)
	at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:3595)
	at io.mockk.proxy.jvm.transformation.InliningClassTransformer.transform(InliningClassTransformer.kt:78)
	at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
	at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
	at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
	at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
	at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:169)
	at io.mockk.proxy.jvm.transformation.JvmInlineInstrumentation.retransform(JvmInlineInstrumentation.kt:28)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumnetation$execute$1.invoke(RetransformInlineInstrumnetation.kt:19)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumnetation$execute$1.invoke(RetransformInlineInstrumnetation.kt:6)
	at io.mockk.proxy.common.transformation.ClassTransformationSpecMap.applyTransformation(ClassTransformationSpecMap.kt:41)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumnetation.execute(RetransformInlineInstrumnetation.kt:16)
	at io.mockk.proxy.jvm.ProxyMaker.inline(ProxyMaker.kt:88)
	at io.mockk.proxy.jvm.ProxyMaker.proxy(ProxyMaker.kt:30)
	at io.mockk.impl.instantiation.JvmMockFactory.newProxy(JvmMockFactory.kt:34)
	at io.mockk.impl.instantiation.AbstractMockFactory.newProxy$default(AbstractMockFactory.kt:29)
	at io.mockk.impl.instantiation.AbstractMockFactory.mockk(AbstractMockFactory.kt:59)
	at io.mockk.impl.annotations.JvmMockInitializer.assignMockK(JvmMockInitializer.kt:163)
	at io.mockk.impl.annotations.JvmMockInitializer.initMock(JvmMockInitializer.kt:41)
	at io.mockk.impl.annotations.JvmMockInitializer.initAnnotatedMocks(JvmMockInitializer.kt:24)

AGP8.0対応時にJavaのバージョンをアップデートする場合が多いと思いますがmockkも同時にアップデートしないとユニットテストが失敗します。

https://github.com/mockk/mockk/releases

私はmockkのバージョンを1.13.5(2023年5月末時点で最新)に更新したらテストが成功しました。(リリースノートを見て適切なバージョンを入れましょう)


SharedPreferencesからDataStoreに移行する時にproduceMigrationsを使用する場合の注意点

SharedPreferencesからDataStoreへ移行する場合は自分でマイグレーション処理を書くことも可能ですが、多くの場合はDataStoreのproduceMigrationsの機能を使ってマイグレーションを行います。

DataStoreインスタンスを作成する場合は下記のようにContextからDataStore<Preferences>を生成します。

build.gradle.ktsに下記を追加(バージョンは2023年5月時点の最新版)

implementation(“androidx.datastore:datastore-preferences:1.0.0”)

DataStoreを扱うモジュール内で下記を定義

import android.content.Context

import androidx.datastore.core.DataStore

import androidx.datastore.preferences.SharedPreferencesMigration

import androidx.datastore.preferences.core.Preferences

import androidx.datastore.preferences.preferencesDataStore

internal const val DATA_STORE_NAME = "data_store"

internal const val PREFERENCES_NAME = “preferences”

internal val Context.dataStore: DataStore<Preferences> by preferencesDataStore(

    name = DATA_STORE_NAME,

    produceMigrations = { context ->

        listOf(

            SharedPreferencesMigration(

                context = context,

                sharedPreferencesName = PREFERENCES_NAME

            )

        )

    }

)

ここまでは調べると出てくるのですがproduceMigrationsで指定した内容で「マイグレーションが実行されるのはDataStoreの読み込みまたは書き込みが発生する時になります」。アプリを起動した時やDependencyInjectionでDataStore<Preferences>のインスタンスを生成した時点ではマイグレーションは実行されません。

またマイグレーションが実行されると「SharedPreferencesで読み書きしているXMLファイルは削除され、DataStoreのファイルのみとなります」。そのためマイグレーションする時にDataStoreの書き込みだけリリースして次のリリースでDataStoreへの読み込みをリリースしようという段階的なリリースはできません。

どういうことかと言うと

マイグレーション機能を実装済みで書き込みのみDataStoreに対応してリリース ⇒ アプリで書き込み時にマイグレーションが実行されSharedPreferencesのXMLファイルが削除 ⇒ SharedPreferencesの読み込みが発生 ⇒ SharedPreferencesのXMLファイルは削除されているので期待された値が返却されない

という状況が発生します。アプリの重要な機能に使われている場合だと致命的なインシデントになってしまいます。

produceMigrationsを使う場合は必ず読み込み書き込み両方をDataStoreに対応させてからアプリをリリースしましょう。