Clomez
Clomez

Reputation: 1512

Trying to understand kotlin DI in Spring boot, at all

for some reason im still having huge problems grasping kotlin style DI in spring boot. Especially in JUnit tests.

@RunWith(SpringJUnit4ClassRunner::class)
@SpringBootTest(classes = [(ApplicationConfig::class)])
class DataServiceTest {

private var dataService: DataService
private var addressRepository: AddressRepository

@Test
fun shouldUpdateCustomerEmail() {

    dataService.setNewCustomerEmail("1212", ApiEmail("[email protected]"))

  }
}

Is very simple test i have. I've tried several style of injections.

private var dataService: DataService

Error: property must be initialized (makes sense)

private lateinit var dataService: DataService

Error: lateinit property dataService has not been initialized (well, yeah, i havent initialized it)

@Autowired
private lateinit var dataService: DataService

Error: No beans of type 'DataService' found (Why? it's fine in other services)

class DataServiceTest @Autowired constructor(
        private val dataService: DataService
) { ... }

Error: No beans of type 'DataService' found (Why? it's fine in other services)

private var dataService: DataService = DataService()

Gives no error.

but cant use same style to inject interface repository for addresses since there is no constructor for interface, so repository will be uninitialized and throw error again inside setNewCustomerEmail()

DataService is @Service class with single method (for now) and works fine when called from rest but crash when called from @Test

@Service
class DataService { 

    @Autowired
    private lateinit var addressRepository: AddressRepository

    fun setNewCustomerEmail(id: String, email: ApiEmail) {
        val addressList = addressRepository.findAddressById( id.toInt() )
        ...
    }
 }

I do get why most of these error arise, but there seems to be many maaany ways to DI other depedencies (classes and variables) in kotlin + spring boot, but it seems to be different for every use case to next, which is fine, but i cant seem to grasp when to use what.

How do i grasp kotlin DI?

How do i inject this repository interface properly?

I've read so far:

How to use spring annotations like @Autowired in kotlin?

https://www.bignerdranch.com/blog/kotlin-when-to-use-lazy-or-lateinit/

EDIT Furthermore

https://spring.io/guides/tutorials/spring-boot-kotlin/

Shows following code:

interface UserRepository : CrudRepository<User, Long> {
  fun findByLogin(login: String): User
}

and then:

@DataJpaTest
class RepositoriesTests @Autowired constructor(
    val entityManager: TestEntityManager,
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository) { ... }

Which is pretty much exactly what im trying here.. but if i try to do @Autowire constructor with DataService i get Error: No beans of type 'DataService' found again.

class DataServiceTest @Autowired constructor(
        val dataService: DataService
) ... no beans of type 'DataService' found

And if i try to use AddressRepository as constructor param in DataService i now ofc have to pass a Repository in, or use @Bean, neither one which i have?

class DataService @Autowired constructor(
        private val AddressRepository: AddressRepository
)

Im sooo super confused with this....

Upvotes: 2

Views: 791

Answers (1)

abendt
abendt

Reputation: 775

I would recommend using constructor injection to define all dependencies for your service:

@Service
class DataService(private val addressRepository: AddressRepository) {
   fun setNewCustomerEmail() {...}
}

Note that you can omit the @Autowired here.

To test that you could either use a plain unit test and setup the object on your own:

class DataServiceTest {
   val mock    = mock<AddressRepository>() // e.g. Mockito
   val service = DataService(mock)

   @Test
   fun myTest() {
     ...
   }
}

Alternatively you can use the SpringRunner to write an integration test. Spring will then handle creation and injection of your service and repository.

@RunWith(SpringRunner::class)
@SpringBootTest
class DataServiceTest {
   @Autowired
   lateinit var dataService: DataService
}

As you seem to use JUnit4 AFAIK it is not possible to use constructor injection. Therefor you need to use the @Autowired/lateinit combo. When using JUnit5 it should be possible to put define an extra constructor in your test class.

Upvotes: 1

Related Questions