I tried to create Unit Test using Rxjava + Retrofit but it always give an error.
I have tried all tutorials and reference related of my questions. I did success when create an unit test of other method (other case), but failed in this case (Rx + retrofit).
Request Data Code:
fun getDetailEvent(idEvent: String?) {
view.showLoading()
apiService.getDetailEvent(idEvent)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
val compositeDisposable: CompositeDisposable? = null
compositeDisposable?.add(it)
}
.doFinally { view.hideLoading() }
.subscribe({
val listModel = it
if (listModel != null) {
view.onDetailEventLoaded(listModel)
} else {
view.onDetailEventLoadFailed("Empty or Error List")
}
},
{
val errorMessage = it.message
if (errorMessage != null) {
view.onDetailEventLoadFailed(errorMessage)
}
})
}
Unit Test Code :
class DetailNextMatchPresenterTest {
#Mock
private lateinit var view : DetailNextMatchView
#Mock
private lateinit var apiService: ApiService
private lateinit var presenter: DetailNextMatchPresenter
#Before
fun setup(){
MockitoAnnotations.initMocks(this)
presenter = DetailNextMatchPresenter(view, apiService)
}
#Test
fun getDetailEvent() {
val event : MutableList<EventModel> = mutableListOf()
val response = ResponseEventModel(event)
val idEvent = "44163"
`when`(apiService.getDetailEvent(idEvent)
.test()
.assertSubscribed()
.assertValue(response)
.assertComplete()
.assertNoErrors()
)
presenter.getDetailEvent(idEvent)
verify(view).showLoading()
verify(view).onDetailEventLoaded(response)
verify(view).hideLoading()
}
}
I appreciate all suggestion. Thanks
I believe that the issue is that you haven't forced your code to behave synchronously in the context of your test, so the Observable runs in parallel to your test. Try adding this in your setup method:
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } If you're using RxJava2. Try looking for a similar method if you're using RxJava 1.
Related
I have the following class to handle account update kafka events
#Component
class AccountUpdateQueueListener(
private val accountUpdateUseCase: AccountUpdateUseCase
) {
#KafkaListener(
topics = [Topics.ACCOUNT_UPDATES],
groupId = "api",
containerFactory = "accountUpdatesConsumeFactory",
)
fun onAccountUpdate(
#Payload request: AccountUpdateEventMessage,
#Header("_txId") txId: String,
acknowledgment: Acknowledgment
) {
MDC.put("account_id", request.account.id.toString())
MDC.put("_txId", txId)
try {
accountUpdateUseCase.update(AccountUpdateEventMessage.toCommand(request))
acknowledgment.acknowledge()
} catch (e: AccountNotFoundException) {
...
}
}
I am trying to test the accountUpdateUseCase.update function used in onAccountUpdate function is called with this test class
#SpringBootTest
#ActiveProfiles("testing")
#DirtiesContext
#EmbeddedKafka(partitions = 1, brokerProperties = ["listeners=PLAINTEXT://localhost:9092", "port=9092"])
class AccountUpdateQueueListenerTest {
#MockK
lateinit var accountUpdateUseCase: AccountUpdateUseCase
#InjectMockKs
lateinit var accountUpdateQueueListener: AccountUpdateQueueListener
#Autowired
lateinit var kafkaTemplate: KafkaTemplate<String, AccountUpdateEventMessage>
val accountUpdateEventMessage = AccountUpdateEventMessage(
...
)
#BeforeEach
fun init() {
MockKAnnotations.init(this)
}
#Test
fun `no errors and update is entered`() {
kafkaTemplate.send(Topics.ACCOUNT_UPDATES, "", accountUpdateEventMessage)
verify(exactly = 1) { accountUpdateUseCase.update(any()) }
}
}
The tests failed as accountUpdateUseCase.update. When running this with a debugger it is clear that onAccountUpdate is never entered. However, if the test case is rewritten as follows the debugger actually stopped in onAccountUpdate
#Test
fun `no errors and update is entered`() {
kafkaTemplate.send(Topics.ACCOUNT_UPDATES, "", accountUpdateEventMessage)
verify(exactly = 1) { accountUpdateQueueListener.onAccountUpdate(any(),any(),any()) }
}
My questions are why does the debugger stop in the second test case and how can I rewrite the first test to achieve my original goal?
I am new to Kotlin. I get exception when trying to use Mockito's thenAnswer method
Controller:
#RestController
class SampleRestController(
val sampleService: SampleService
) {
#PostMapping(value = ["/sample-endpoint"], consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun sampleEndpoint(#RequestBody values: List<String>): ResponseEntity<String> {
val response = sampleService.serviceCall(values)
return ResponseEntity.status(HttpStatus.OK).body(response)
}
}
Service:
#Service
#Transactional
class SampleService {
fun serviceCall(values: List<String>): String {
return values.joinToString("")
}
}
Test:
#ExtendWith(MockitoExtension::class)
internal class SampleRestControllerTest {
#Mock
private lateinit var sampleService: SampleService
private lateinit var mockMvc: MockMvc
private lateinit var objectMapper: ObjectMapper
private lateinit var sampleRestController: SampleRestController
#BeforeEach
fun before() {
MockitoAnnotations.initMocks(this)
objectMapper = ObjectMapper()
.registerModule(JavaTimeModule())
.registerKotlinModule()
sampleRestController = SampleRestController(sampleService)
mockMvc = MockMvcBuilders.standaloneSetup(sampleRestController).build()
}
#Test
fun doTest() {
val testData = listOf("123", "456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenReturn("123456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> "123456" }
Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> {
val numbers = invocation.getArgument<List<String>>(0)
if ("123" == numbers[0] && "456" == numbers[1]) {
"123456"
} else {
"654321"
}
} }
val result: MvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/sample-endpoint")
.content(objectMapper.writeValueAsString(testData))
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
.andExpect(status().isOk)
.andReturn()
assertEquals("123456", result.response.contentAsString)
}
}
The unit test is working fine when using the thenReturn() and also when using thenAnswer() without any if condition.
When I try to use thenAnswer with if condition then I get classCastException.
Probably because Kotlin only accepts non-null value? How do I resolve this issue.
Check this.
#Test
fun doTest() {
val testData = listOf("123", "456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenReturn("123456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> "123456" }
Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer(Answer<Any?> { invocationOnMock: InvocationOnMock ->
val values = invocationOnMock.getArgument<List<String>>(0)
if ("123" == values[0] && "456" == values[1]) {
return#Answer "123456"
}
return#Answer "654321"
})
val result: MvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/sample-endpoint")
.content(objectMapper.writeValueAsString(testData))
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
.andExpect(status().isOk)
.andReturn()
assertEquals("123456", result.response.contentAsString)
}
I'm trying to write a component which uses different datasources of data.
Then data is combined and emitted in the different resulting flow.
class TaskControlComponent(
private val diskCacheDataSource: DiskCacheDataSource,
private val debugDataSource: DebugDataSource
) {
private val _localTasks = MutableStateFlow<Map<String, TaskItem>>(emptyMap())
val localTasks: StateFlow<Map<String, TaskItem>> = _localTasks
suspend fun loadLocal() {
flowOf(
diskCacheDataSource.defaultFeatures,
diskCacheDataSource.localFeatures,
debugDataSource.debugFeatures
).flattenMerge().collect {
computeLocalTasks()
}
}
private suspend fun computeLocalTasks() {
val resultTasks = HashMap<String, TaskItem>(64)
listOf(
diskCacheDataSource.defaultTasks,
diskCacheDataSource.localTasks,
debugDataSource.debugTasks
).forEach { tasksMap ->
tasksMap.value.forEach { entry ->
resultTasks[entry.key] = entry.value
}
}
_localTasks.emit(resultTasks)
}
}
DataSource
interface DiskCacheDataSource {
val defaultTasks: StateFlow<Map<String, TaskItem>>
val localTasks: StateFlow<Map<String, TaskItem>>
}
It works, but how to write junit test for that?
class TaskControlImplTest {
private lateinit var taskControl: TaskControlComponent
#Mock
lateinit var diskCacheDataSource: DiskCacheDataSource
#Mock
lateinit var debugDataSource: DebugDataSource
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
taskControl = TaskControlComponent(diskCacheDataSource, debugDataSource)
}
#Test
fun testFeatureControl() {
whenever(diskCacheDataSource.defaultTasks).thenReturn(
MutableStateFlow(
mapOf(
"1" to TaskItem(
"1",
TaskStatus.On
)
)
)
)
whenever(diskCacheDataSource.localTasks).thenReturn(MutableStateFlow(emptyMap()))
whenever(debugDataSource.debugTasks).thenReturn(MutableStateFlow(emptyMap()))
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
}
}
In case of the following sequence of commands
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
test freezes, and runs forewer.
When I swap the pieces of code, first instead of second and the contrary
runBlocking {
featureControl.localFeatures.collect {
Assert.assertEquals(it.size, 1)
}
}
runBlocking {
featureControl.loadLocal()
}
Tests finishes with warning
expected:<0> but was:<1>
Expected :0
Actual :1
Is it possible to write test for such usecase? What should be investigated or done in order to make test workable?
The reason the order matters here is because StateFlow is hot, unlike normal Flow, meaning it starts running with data immediately not when it is collected
I test with the turbine library but you don't need it. I don't remember the exact setup of not using turbine but it was a bit more complicated so I chose to use turbine
https://github.com/cashapp/turbine
I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.
I'm new to LiveData and Kotlin Coroutines. I'm trying to use the Chromium Cronet library to make a request from my repository class to return a LiveData object. To return the liveData, I'm using the new LiveData builder (coroutines with LiveData). How would I emit the result from a successful Cronet request?
class CustomRepository #Inject constructor(private val context: Context, private val gson: Gson) : Repository {
private val coroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
override suspend fun getLiveData(): LiveData<List<MyItem>> = liveData(coroutineDispatcher) {
val executor = Executors.newSingleThreadExecutor()
val cronetEngineBuilder = CronetEngine.Builder(context)
val cronetEngine = cronetEngineBuilder.build()
val requestBuilder = cronetEngine.newUrlRequestBuilder(
"http://www.exampleApi.com/example",
CustomRequestCallback(gson),
executor
)
val request: UrlRequest = requestBuilder.build()
request.start()
}
class CustomRequestCallback(private val gson: Gson) : UrlRequest.Callback() {
override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) {
byteBuffer?.flip()
byteBuffer?.let {
val byteArray = ByteArray(it.remaining())
it.get(byteArray)
String(byteArray, Charset.forName("UTF-8"))
}.apply {
val myItems = gson.fromJson(this, MyItem::class.java)
// THIS IS WHAT I WANT TO EMIT
// emit(myItems) doesn't work since I'm not in a suspending function
}
byteBuffer?.clear()
request?.read(byteBuffer)
}
// other callbacks not shown
}
}
The solution involves wrapping the UrlRequest.Callback traditional callback structure in a suspendCoroutine builder.
I also captured my learning in a Medium article which discusses Cronet integration with LiveData and Kotlin Coroutines.
override suspend fun getLiveData(): LiveData<List<MyItem>> = liveData(coroutineDispatcher) {
lateinit var result: List<MyItem>
suspendCoroutine<List<MyItem>> { continuation ->
val requestBuilder = cronetEngine.newUrlRequestBuilder(
"http://www.exampleApi.com/example",
object : UrlRequest.Callback() {
// other callbacks not shown
override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) {
byteBuffer?.flip()
byteBuffer?.let {
val byteArray = ByteArray(it.remaining())
it.get(byteArray)
String(byteArray, Charset.forName("UTF-8"))
}.apply {
val myItems = gson.fromJson(this, MyItem::class.java)
result = myItems
continuation.resume(result)
}
byteBuffer?.clear()
request?.read(byteBuffer)
},
executor
)
val request: UrlRequest = requestBuilder.build()
request.start()
}
emit(result)
}