I am testing a Spring boot service using JUnit 5 and Mockito. I haven't spent much time on unit testing in the past so I'm not sure if I'm accidentally implementing an anti-pattern or not following best practices.
I read multiple times that it's good practice to have one assertion per test, I have one assertion but I also have a few verify assertions that check if the method I'm testing called the correct methods with the correct input based on the conditional branch I'm testing as well as the input to the method I'm testing, is this OK/acceptable or should I split it up into one assertion/verify assertion per test? It feels like that would require a lot of code duplication... Below are four tests I wrote and the method I'm testing. For clarity I'll also add the code that sets up and handles my test application context.
Any ideas/advice would be much appreciated.
The method I am testing:
@Transactional
override fun startSession(msisdn: String, origConnID: String, configSetName: String): StartSessionResponseDTO {
val txId = generateTxID()
val ss = sessionStatus(msisdn)
eventService.logEvent(EventType.StartSessionRequest, txId, msisdn, hashMapOf("extTxId" to origConnID))
if (ss == null) {
throw ApplicationException(type = ApplicationException.Type.client_error, message = "Concurrent Access Detected: ${msisdn}")
} else {
if (ss.sessionId == null || ss.ip == null) {
//create new Nuance session
nuanceService.startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName)).let {
updateSession(msisdn = msisdn, sessionId = it.sessionId, ip = it.ip, origConnID = origConnID)
}
} else {
//check if the sessionId is still valid
if (!nuanceService.isSessionValid(txId = txId, msisdn = msisdn, sessionId = ss.sessionId, host = ss.ip, configSetName = configSetName)) {
nuanceService.startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName)).let {
updateSession(msisdn = msisdn, sessionId = it.sessionId, ip = it.ip, origConnID = origConnID)
}
}
}
}
eventService.logEvent(type = EventType.StartSessionResponse, txId = txId, msisdn = msisdn)
return StartSessionResponseDTO(msisdn)
}
My test class:
@ExtendWith(SpringExtension::class)
@ContextConfiguration()
class VBServiceTests() {
@TestConfiguration
class testConfig {
@Bean
fun jdbcTemplate(): NamedParameterJdbcTemplate {
return mock<NamedParameterJdbcTemplate>()
}
@Bean
fun nuanceService(): NuanceService {
return mock<NuanceService>()
}
@Bean
fun appConfigProps(): AppConfigProps {
return mock<AppConfigProps>()
}
@Bean
fun eventService(): EventServiceImp {
return mock<EventServiceImp>()
}
@Bean
fun audioTrimService(): AudioTrimService {
return mock<AudioTrimService>()
}
@Bean
fun vbNuanceStagingDeletionService(): VbNuanceStagingDeletionsService {
return mock<VbNuanceStagingDeletionsService>()
}
}
@MockBean
lateinit var nuanceService: NuanceService
@MockBean
lateinit var eventService: EventServiceImp
@SpyBean
lateinit var vbServiceSpy: VbServiceImp
val msisdn = "0821234567"
val origConnID = "o123"
val sessionId = "0821234567"
val ip = "127.0.0.1"
val txId = "1234-5678"
val configSetName = "LoIvrPhraIvrHdr"
@Test
fun `startSession | When method is called with valid input a StartSessionDTO is returned, if no Nuance session exists a new Nuance session will be created and persisted`() {
doNothing().whenever(vbServiceSpy).updateSession(msisdn, origConnID, sessionId, ip)
doReturn(txId).whenever(vbServiceSpy).generateTxID()
doReturn(SessionStatus(msisdn, origConnID, null, null)).whenever(vbServiceSpy).sessionStatus(msisdn)
given(nuanceService.startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName))).willReturn(NuanceStartSessionRespDto(txId, sessionId, ip))
assertThat(vbServiceSpy.startSession(msisdn, origConnID, configSetName), Is(StartSessionResponseDTO(msisdn)))
verify(vbServiceSpy).generateTxID()
verify(vbServiceSpy).sessionStatus(msisdn)
verify(eventService).logEvent(StartSessionRequest, txId, msisdn, hashMapOf("extTxId" to origConnID))
verify(nuanceService).startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName))
verify(vbServiceSpy).updateSession(msisdn = msisdn, sessionId = sessionId, ip = ip, origConnID = origConnID)
verify(eventService).logEvent(type = EventType.StartSessionResponse, txId = txId, msisdn = msisdn)
}
@Test
fun `startSession | When method is called with valid input a StartSessionDTO is returned, if existing valid Nuance session exists the existing session will be used`() {
doNothing().whenever(vbServiceSpy).updateSession(msisdn, origConnID, sessionId, ip)
doReturn(txId).whenever(vbServiceSpy).generateTxID()
doReturn(SessionStatus(msisdn, origConnID, sessionId, ip)).whenever(vbServiceSpy).sessionStatus(msisdn)
given(nuanceService.isSessionValid(txId, msisdn, sessionId, ip, configSetName)).willReturn(true)
assertThat(vbServiceSpy.startSession(msisdn, origConnID, configSetName), Is(StartSessionResponseDTO(msisdn)))
verify(vbServiceSpy).generateTxID()
verify(vbServiceSpy).sessionStatus(msisdn)
verify(eventService).logEvent(StartSessionRequest, txId, msisdn, hashMapOf("extTxId" to origConnID))
verify(eventService).logEvent(type = EventType.StartSessionResponse, txId = txId, msisdn = msisdn)
}
@Test
fun `startSession | When method is called with valid input a StartSessionDTO is returned, if existing invalid Nuance session exists a new session will be created and the existing session record will be updated`() {
doNothing().whenever(vbServiceSpy).updateSession(msisdn, origConnID, sessionId, ip)
doReturn(txId).whenever(vbServiceSpy).generateTxID()
doReturn(SessionStatus(msisdn, origConnID, sessionId, ip)).whenever(vbServiceSpy).sessionStatus(msisdn)
given(nuanceService.isSessionValid(txId, msisdn, sessionId, ip, configSetName)).willReturn(false)
given(nuanceService.startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName))).willReturn(NuanceStartSessionRespDto(txId, sessionId, ip))
assertThat(vbServiceSpy.startSession(msisdn, origConnID, configSetName), Is(StartSessionResponseDTO(msisdn)))
verify(vbServiceSpy).generateTxID()
verify(vbServiceSpy).sessionStatus(msisdn)
verify(eventService).logEvent(StartSessionRequest, txId, msisdn, hashMapOf("extTxId" to origConnID))
verify(nuanceService).startSession(NuanceStartSessionReqDto(txId, msisdn, configSetName = configSetName))
verify(vbServiceSpy).updateSession(msisdn = msisdn, sessionId = sessionId, ip = ip, origConnID = origConnID)
verify(eventService).logEvent(type = EventType.StartSessionResponse, txId = txId, msisdn = msisdn)
}
@Test()
fun `startSession - When method is called with valid input when sessionStatus returns null concurrent access is detected and an ApplicationException gets thrown`() {
doReturn(txId).whenever(vbServiceSpy).generateTxID()
doReturn(null).whenever(vbServiceSpy).sessionStatus(msisdn)
val exception = Assertions.assertThrows(ApplicationException::class.java) {
vbServiceSpy.startSession(msisdn, origConnID, configSetName)
}
assertThat(exception.message, Is("Concurrent Access Detected: ${msisdn}"))
verify(vbServiceSpy).generateTxID()
verify(vbServiceSpy).sessionStatus(msisdn)
}
}
MockitoExtension
rather thanSpringExtension
if your code is well unit testable. Your tests will run faster. In addtion you may try to not overuseverify
\$\endgroup\$