Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.NullPointerException when upgraded to kotlin 2.0 #519

Open
hanrw opened this issue Jun 18, 2024 · 5 comments
Open

java.lang.NullPointerException when upgraded to kotlin 2.0 #519

hanrw opened this issue Jun 18, 2024 · 5 comments

Comments

@hanrw
Copy link

hanrw commented Jun 18, 2024

gradle config

plugins {
    id("org.springframework.boot") version "3.3.0"
    id("io.spring.dependency-management") version "1.1.5"
    kotlin("jvm") version "2.0.0"
    kotlin("plugin.spring") version "2.0.0"
}

group = "com.kotlin.npe"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenLocal()

    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

test

@ExtendWith(MockitoExtension::class)
class UserStatusServiceTest {
    @InjectMocks
    lateinit var target: UserStatusServiceImpl

    @Mock
    lateinit var userStatusRepository: UserStatusRepository

    @Test
    fun addStatus() {
        whenever(
            userStatusRepository.findByUserId(
                1L,
            )
        ).thenReturn(
            Optional.empty()
        )

        target.addStatus(1L)

        val suspension = UserStatus()
        verify(userStatusRepository).save(suspension)
    }
}

service

interface UserStatusService {
    fun addStatus(userId: Long)
}

class UserStatusServiceImpl(private val userStatusRepository: UserStatusRepository) :
    UserStatusService {
    override fun addStatus(userId: Long) {
        val optional = userStatusRepository.findByUserId(userId)
        if (optional.isEmpty) {
            println("[userId: $userId] adding a new status")

            userStatusRepository.save(UserStatus())
        } else {
            println("[userId: $userId] status already exists")
        }
    }
}

repository

@Repository
interface UserStatusRepository : JpaRepository<UserStatus, Long>,
    JpaSpecificationExecutor<UserStatus> {
    fun findByUserId(userId: Long): Optional<UserStatus>
}

data class UserStatus(
    val userId: Long = 1L
)

and got error

WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
[userId: 1] adding a new status

java.lang.NullPointerException
	at com.kotlin.npe.reporduce.UserStatusServiceImpl.addStatus(UserStatusService.kt:11)
	at com.kotlin.npe.reporduce.UserStatusServiceTest.addStatus(UserStatusServiceTest.kt:31)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

here's reproducing repo and it works after downgrading to kotlin 1.9.24
archive.zip

@lasficw
Copy link

lasficw commented Jul 8, 2024

We have the same Problem, is there something we can do about it?

@Vladimir-Buttn
Copy link

+1 here

@heliming
Copy link

+1

@silviuburceadev
Copy link

silviuburceadev commented Oct 31, 2024

I am facing the same issue. My setup:

@ExtendWith(MockitoExtension::class)
internal class MyTest {
  @Mock
  private lateinit var myRepository: MyRepository

  @Mock
  private lateinit var myService: MyService

  // depends on myRepository and myService
  @InjectMocks
  private lateinit var myCompositeService: MyCompositeService
}

MyRepository is a org.springframework.data.repository.CrudRepository, no annotations, and MyService is a classic @Service class.

Even though I'm checking that myRepository is not null, both in the test class, before calling the method tested and inside the method, before the repository does a save. And yet, myRepository.save fails with NPE.

Edit:
The root cause for me was that the CrudRepository's save method was not mocked, so by default mockito seem to return null for it, which Kotlin doesn't like, as the method returns a non-null object. The solution was:

`when`(myRepository.save(notNull(MyEntity::class.java))).thenAnswer(AdditionalAnswers.returnsFirstArg<MyEntity>)

This would be nice to be auto-configured, somehow. I am guessing that the Kotlin compiler is able to be more strict in this case, compared to Kotlin 1.9, which was more lenient and the build was passing.

@silviuburceadev
Copy link

silviuburceadev commented Nov 4, 2024

@hanrw I have tested my finding on your reproducible project. You have a commented-out line there, replace it with:

whenever(userStatusRepository.save(Mockito.notNull(UserStatus::class.java)))
    .thenAnswer(AdditionalAnswers.returnsFirstArg<UserStatus>())

... and it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants