I am new to test cases and I am trying to write test cases for the below code but I did not get success after trying several method. My main target is to cover code coverage of MaintenanceStatusResponseHandler.kt class. I am using mockito to write the test cases. I am already implemented jococo for code coverage but I am facing some issue to write a test cases. Please help me to write test cases of MaintenanceStatusResponseHandler class
Thanks in advance
internal class MaintenanceStatusResponseHandler {
public fun getMaintenanceResponse(voiceAiConfig : VoiceAiConfig):MaintenanceStatus{
val maintenanceStatus = MaintenanceStatus()
val retrofitRepository = RetrofitRepository()
val maintenanceUrl : String
val jwtToken : String
when (voiceAiConfig.server) {
BuildConfig.ENV_PRODUCTION_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_PRODUCTION
maintenanceUrl = BuildConfig.MAINTENANCE_PROD_URL
}
BuildConfig.ENV_STAGING_SERVER -> {
jwtToken = BuildConfig.JWT_TOKEN_STAGING
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
else -> {
jwtToken = BuildConfig.JWT_TOKEN_SANDBOX
maintenanceUrl = BuildConfig.MAINTENANCE_SANDBOX_URL
}
}
val header = "${VoiceAISDKConstant.JWT_TOKEN_PREFIX} $jwtToken"
retrofitRepository.getRetrofit(maintenanceUrl)
.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}
Related
I am currently facing a problem with the endpoint argument in the #GET Annotation in Android Studio.
Here are the list of endpoints
NewsApi Endpoints
Defining the get function with the following endpoint v2/top-headlines works completely fine
interface APIDirectory{
#GET("v2/top-headlines")
suspend fun getArticles(
#Query("country")
countryCode: String = "Singapore",
#Query("page")
pageNumber: Int = 1,
#Query("sortBy")
sortBy:String = "publishedAt",
#Query("apiKey")
apiKey: String = API_KEY
):Response<ArticleList>
However, changing the endpoint to /v2/everything will result in an error
2022-12-03 02:47:11.887 10220-10220/com.example.thenewsapplication E/Tech: An error occured
This error is located in the Tech Fragment, the error occurs in the is Resource.Error portion of the code.
viewModel.searchNews.observe(viewLifecycleOwner, Observer{ response ->
Log.e("Tech", "in the technology fragment")
when (response){
is Resource.Success -> {
response.data?.let {newsResponse ->
//Submits the data to the recyclerview
newsAdapter.differ.submitList(newsResponse.articles.toList())
val totalPages = newsResponse.totalResults / Constants.QUERY_PAGE_SIZE + 2
isLastPage = viewModel.searchNewsPage == totalPages
}
}
is Resource.Error -> {
response.message?.let {
Log.e("Tech","An error occured $it")
}
}
is Resource.Loading -> {
...}
I concluded that the error must be due to the response argument in the when statement, which refers to the searchNews variable in the viewmodel
val searchNews: MutableLiveData<Resource<ArticleList>> = MutableLiveData()
fun getsearchNews(countryCode: String) = viewModelScope.launch {
Log.e("viewmodel", "in the viewmodel getsearchNews")
searchNews.postValue(Resource.Loading())
Log.e("viewmodel", "getting data")
val response = newsRepository.getSearchNews(countryCode, breakingNewsPage)
searchNews.postValue(handleSearchNewsResponse(response))
}
private fun handleSearchNewsResponse(response: Response<ArticleList>): Resource<ArticleList> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
searchNewsPage++
if (searchNewsResponse == null) {
searchNewsResponse = resultResponse
} else {
val olArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
olArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
From the handleSearchNewsResponse above, the Response from the ArticleList is a failure, the ArticleList class is just a kotlin class that represents the Response from the WebService
data class ArticleList(
val articles: MutableList<Article>,
val status: String,
val totalResults: Int
)
I'm trying to map a network object to a domain object with a .map in the response, but compiler says me that .map is an unresolved reference. What is wrong?
suspend fun getAllPlayersFromNetwork(): PlayersDomainModel {
val response: PlayersNetworkModel = api.getAllPlayers()
**return response.**map** { it.toDomain() }**
}
This is the data class of the network part
data class PlayersNetworkModel(
#SerializedName("data") var data: MutableList<DataNetworkModel> = mutableListOf(),
#SerializedName("meta") var meta: MetaNetworkModel? = MetaNetworkModel()
){
constructor() : this(mutableListOf(), MetaNetworkModel())
}
This is the object of the domain
data class PlayersDomainModel(
var data: MutableList<DataDomainModel> = mutableListOf(),
var meta: MetaDomainModel? = MetaDomainModel()
)
and this is the transform method to map
fun PlayersNetworkModel.toDomain(): PlayersDomainModel {
val mapMeta : MetaDomainModel
meta.let {
mapMeta = MetaDomainModel(meta?.totalPages, meta?.currentPage, meta?.nextPage, meta?.perPage, meta?.totalCount)
}
val mapData : MutableList<DataDomainModel> = mutableListOf()
data.let {
for(item in data){
val mapTeam : TeamDomainModel = TeamDomainModel(item.team?.id, item.team?.abbreviation, item.team?.city, item.team?.conference, item.team?.division, item.team?.fullName, item.team?.name)
mapData.add(DataDomainModel(item.id, item.firstName, item.heightFeet, item.heightInches, item.lastName, item.position, mapTeam, item.weightPounds))
}
}
return PlayersDomainModel(mapData, mapMeta)
}
I'm not sure why simple data class suppose to have a map method.
Instead of mapping:
return response.map { it.toDomain() }
just use toDomain() on the response:
return response.toDomain()
I'm trying to create this MockController with mockk to avoid create a new class for testing.
Is possible to do that?
class MockController : IController {
override lateinit var output: (String) -> Unit
override fun start() {
output("OK")
}
}
Class to test:
class ClassToTest(
private val controller: IController,
private val output: (String) -> Unit
){
fun start() {
controller.output = { result ->
output(result)
}
controller.start()
}
}
Then I use like this TEST example:
#Test
fun checkOutputIsCalled() {
runBlocking {
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(MockController(), outputClassToTest)
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
I'm trying to update:
#Test
fun checkOutputIsCalled() {
runBlocking {
val controller = spyk<IController>()
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(controller, outputClassToTest)
every { controller.start() } answers {
controller.output.invoke("OK")
} //When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
How this could be after the output assign?
Thanks!
You should mock your output object and your Controller. Once done, tell your mocked controller to return the mocked output when property is called. Right after the start() invocation you can verify that output lambda was invoked. Please note that all your mocks must be relaxed.
class ClassToTestTesting {
companion object {
const val INVOCATION_PARAM = "OK"
}
#Test
fun test() = runBlocking {
val paramOutput: (String) -> Unit = mockk(relaxed = true)
val controller: IController = mockk(relaxed = true) {
every { output } returns paramOutput
every { start() } answers { output.invoke(INVOCATION_PARAM) }
}
val classToTest = ClassToTest(
controller,
paramOutput
)
classToTest.start()
verify { paramOutput(INVOCATION_PARAM) }
}
}
I am testing this code and I got success in the success response code but I am not able to reach the onError code
internal class MaintenanceStatusResponseHandler {
fun getMaintenanceResponse (voiceAiServices: VoiceAiServices, header: String): MaintenanceStatus {
val maintenanceStatus = MaintenanceStatus()
voiceAiServices.getMaintenanceStatus(header)
.subscribe { response: MaintenanceStatus.Content, error: Throwable? ->
error.let {
if (error != null) {
maintenanceStatus.error = error
}
}
response.let {
maintenanceStatus.content = response
}
}
return maintenanceStatus
}
}
repository class
class RetrofitRepository() {
val TAG = RetrofitRepository::class.java.canonicalName
fun getRetrofit(baseUrl: String?): VoiceAiServices {
val voiceAiServices: VoiceAiServices = Retrofit.Builder()
.baseUrl(baseUrl!!)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(VoiceAiServices::class.java)
return voiceAiServices
}
}
interface
interface VoiceAiServices {
#GET("/v1/api/status")
fun getMaintenanceStatus(#Header("Authorization")header: String): Single<MaintenanceStatus.Content>
}
Pojo class
data class MaintenanceStatus(
var error: Throwable? = null,
var content: Content? = null
) {
data class Content(
val enabled: Boolean,
val maintenanceMsg: String
)
}
Below test code for Success response which is working fine
fun mockedObservableErrorSuccess(): Single<MaintenanceStatus.Content> {
return Single.create { e ->
e.onSuccess(MaintenanceStatus.Content(true, "Under error maintenance"))
e.onError(Throwable("Error message"))
}
}
#Test
fun testMaintenanceSuccessResponse() {
val voiceAiService: VoiceAiServices = mock(VoiceAiServices::class.java)
val maintenanceStatusHandler = MaintenanceStatusResponseHandler()
val content = MaintenanceStatus.Content(true, "Under maintenance")
`when`(voiceAiService.getMaintenanceStatus(Utilities.getHeader(voiceAIConfig))).thenReturn(just(content))
val testObserver: TestObserver<MaintenanceStatus.Content> = TestObserver.create()
val observer = mockedObservableErrorSuccess()
observer.subscribe(testObserver)
maintenanceStatusHandler.getMaintenanceResponse(voiceAiService, "header")
testObserver.awaitTerminalEvent()
testObserver.onComplete()
}
Below is code that is not able to reach to Error method and for this code I need help
I really appreciate any help you can provide.
#Test(expected = java.lang.Exception::class)
fun testMaintenanceErrorResponse() {
val voiceAiService: VoiceAiServices = mock(VoiceAiServices::class.java)
val maintenanceStatusHandler = MaintenanceStatusResponseHandler()
`when`(voiceAiService.getMaintenanceStatus(Utilities.getHeader(voiceAIConfig))).thenReturn(error(Throwable("Error message")))
val testObserver: TestObserver<MaintenanceStatus.Content> = TestObserver.create()
val observer = mockedObservableErrorSuccess()
observer.subscribe(testObserver)
maintenanceStatusHandler.getMaintenanceResponse(voiceAiService, Utilities.getHeader(voiceAIConfig))
testObserver.awaitTerminalEvent()
testObserver.assertError(Throwable::class.java)
// testObserver.onError(Throwable()) //Also tried this method
}
Caused by: java.lang.AssertionError: No errors (latch = 0, values = 1, errors = 0, completions = 1)
I am using Single.zip() and Function 5 to consolidate 5 API calls into one Single:
private fun loadProfileAndBalances() {
registerSubscription(
Single.zip<AvailableFundsResult, IncomingFundsResult, TotalEarnedResult, TotalDonatedResult, ProfileResult, Unit>(
Interactors.api.paymentsApiClient.getAvailableFunds()
.map<AvailableFundsResult> {
Timber.d("Available Result [${it.amount}]") <------ DollarAmount object with null amount
AvailableFundsResult.Amount(it) }
.onErrorReturn { AvailableFundsResult.Error(it) }
.scheduleIOUI(),
Interactors.api.paymentsApiClient.getIncomingFunds()
.map<IncomingFundsResult> { IncomingFundsResult.Amount(it) }
.onErrorReturn { IncomingFundsResult.Error(it) }
.scheduleIOUI(),
Interactors.api.paymentsApiClient.getTotalEarned()
.map<TotalEarnedResult> { TotalEarnedResult.Amount(it) }
.onErrorReturn { TotalEarnedResult.Error(it) }
.scheduleIOUI(),
Interactors.api.paymentsApiClient.getTotalDonated()
.map<TotalDonatedResult> { TotalDonatedResult.Amount(it) }
.onErrorReturn { TotalDonatedResult.Error(it) }
.scheduleIOUI(),
Interactors.profileManager.getNotCachedProfile()
.map<ProfileResult> { ProfileResult.Profile(it) }
.onErrorReturn { ProfileResult.Error(it) }
.scheduleIOUI(),
Function5 { availableFunds: AvailableFundsResult, incomingFunds: IncomingFundsResult, totalEarned: TotalEarnedResult, totalDonated: TotalDonatedResult, profileResult: ProfileResult ->
availableTotal = when (availableFunds) {
is AvailableFundsResult.Amount ->
availableFunds.result.amount
is AvailableFundsResult.Error -> {
Timber.w(availableFunds.throwable, "Error while fetching sponsorships")
"0.00"
}
}
incomingTotal = when (incomingFunds) {
is IncomingFundsResult.Amount -> incomingFunds.result.amount
is IncomingFundsResult.Error -> {
Toast.makeText(activity, getString(R.string.payout_main_error_loading_totals), Toast.LENGTH_SHORT).show()
"0.00"
}
}
earnedTotal = when (totalEarned) {
is TotalEarnedResult.Amount -> totalEarned.result.amount
is TotalEarnedResult.Error -> {
Toast.makeText(activity, getString(R.string.payout_main_error_loading_totals), Toast.LENGTH_SHORT).show()
"0.00"
}
}
donatedTotal = when (totalDonated) {
is TotalDonatedResult.Amount -> totalDonated.result.amount
is TotalDonatedResult.Error -> {
Toast.makeText(activity, getString(R.string.payout_main_error_loading_totals), Toast.LENGTH_SHORT).show()
"0.00"
}
}
onboardingComplete = when (profileResult) {
is ProfileResult.Profile ->
profileResult.result.isOnboardingCompleted
is ProfileResult.Error -> {
Timber.e(profileResult.throwable, "Error fetching profile")
true
}
}
}
).ignoreElement()
.subscribe(::updateViews) {
it.printStackTrace()
availableTotal = ""
incomingTotal = ""
earnedTotal = ""
donatedTotal = ""
onboardingComplete= false
vWalletRefresher.isRefreshing = false
internetConnectionError(it)
})
}
Each of these API calls is succeeding with code 200. The call made with Interactors.api.paymentsApiClient.getAvailableFunds() is returning {"amount":264.69}, which is parsed into an object of this class:
internal data class DollarAmount(#SerializedName("amount") val amount: String)
The paymentsApiClient repeatedly referenced is built like this:
private fun createNewPaymentsClient(authRefreshClient: AuthRefreshClient,
preferencesInteractor: PreferencesInteractor): PaymentsApiClient {
val loggingInterceptor = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
val okHttpClient = createHttpClientBuilder()
.addInterceptor(createSessionRequestInterceptor())
.addInterceptor(createUserAgentInterceptor(context))
.addInterceptor(loggingInterceptor)
.authenticator(RefreshUserAuthenticator(authRefreshClient, preferencesInteractor,
UnauthorizedNavigator(SDKInternal.appContext, Interactors.preferences)))
.build()
val gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().setLenient().create()
return Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl(Interactors.apiEndpoint)
.build()
.create(PaymentsApiClient::class.java)
}
Despite the API call being successful, the log statement that is printed in Single.zip for a successful retrieval of a DollarAmount via Interactors.api.paymentsApiClient.getAvailableFunds() the is showing its amount as being null, rather than 264.69. What is wrong with my parsing that is making this null?
Edit: As someone noticed, I am wrapping the results in another set of classes:
private sealed class AvailableFundsResult {
data class Error(val throwable: Throwable) : AvailableFundsResult()
data class Amount(val result: DollarAmount) : AvailableFundsResult()
}
private sealed class IncomingFundsResult {
data class Error(val throwable: Throwable) : IncomingFundsResult()
data class Amount(val result: DollarAmount) : IncomingFundsResult()
}
private sealed class TotalEarnedResult {
data class Error(val throwable: Throwable) : TotalEarnedResult()
data class Amount(val result: DollarAmount) : TotalEarnedResult()
}
private sealed class TotalDonatedResult {
data class Error(val throwable: Throwable) : TotalDonatedResult()
data class Amount(val result: DollarAmount) : TotalDonatedResult()
}
private sealed class ProfileResult {
data class Error(val throwable: Throwable) : ProfileResult()
data class Profile(val result: InfluencerProfileDto) : ProfileResult()
}
I don't think this is relevant though because it the amount is coming back null before I even spit out an instance of these wrapper classes.
Edit 2: Class that gets returned from 5th API call:
#JsonClass(generateAdapter = true)
internal data class InfluencerProfileDto(
#Json(name = "id") val id: String,
#Json(name = "emailAddress") val email: String?,
#Json(name = "phoneNumber") val phoneNumber: PhoneNumberDto?,
#Json(name = "isPhoneNumberVerified") val isPhoneNumberVerified: Boolean,
#Json(name = "isEmailVerified") val isEmailVerified: Boolean,
#Json(name = "notificationTimePreference") val notificationTimePreference: String,
#Json(name = "isInstagramConnected") val isInstagramConnected: Boolean,
#Json(name = "isFacebookConnected") val isFacebookConnected: Boolean,
#Json(name = "isTwitterConnected") val isTwitterConnected: Boolean,
#Json(name = "currencyIsoSymbol") val currencyIsoSymbol: String,
#Json(name = "currencySymbol") val currency: String,
#Json(name = "birthDate") val birthDate: Date?,
#Json(name = "name") val name: String?,
#Json(name = "profilePictureUri") val avatarUrl: String?,
#Json(name = "gender") val gender: Gender?)
{
val isOnboardingCompleted: Boolean
get() = gender!= Gender.UNKNOWN && birthDate!= null && !!notificationTimePreference.isNullOrEmpty() && isPhoneNumberVerified && !email.isNullOrEmpty()
}
my approach is in java
first of all we need two libraries that you might not be using in your project ie.
gson => for pasing objects from string and vice versa
volley => for string http requests
please google their official github pages and install the latest versions
then we ll be creating a common result object to bear results
class Result {
boolean error; // this field can be used by the api to tell if there was some error while processing the request
String message; // here api can give you some extra message if there was some error
String result; // here you get the result. you can make it a map as well if you want to receive an object and later parse to a unique object class
}
this is a function you can use for HTTP requets
public static Single<Result> getHttpResult(String api, Map<Stirng, String> params){
return Single.create(emitter -> {
StringRequest stringRequest = new StringRequest(Request.Method.POST, api, response -> {
Log.i("RESPONSE :", response);
try {
Result result = new Gson().fromJson(response, Result.java);
emitter.onSuccess(result);
} catch (JSONException e) {
emitter.onError(e);
}
}, emitter::onError) {
#Override
protected Map<String, String> getParams() {
return params;
}
};
stringRequest.setRetryPolicy(new DefaultRetryPolicy(0, 0, 0));
RequestQueue requestQueue = Volley.newRequestQueue(context);
requestQueue.add(stringRequest);
});
}
this is how you can get multiple results at once and i recommend you also check the internet connectivity if user has enough internet then only make parallel requests else it wont produce good UX
...
List<Single<Result>> tasks = new ArrayList();
tasks.add(getHttpResult("https://..."), /**here you can add a map with params that your api might require for auth and other purposes**/);
tasks.add(...);
tasks.add(...);
tasks.add(...);
Single.zip(tasks, objects -> {
List<Result> results = new ArrayList();
for(Object object : objects){
results.add((Result)object)
}
return results;
}).subscribe(results -> {
// this result object have all the results of http requests
// for distinguising results from each other you can create a field on the result object
});
...
hope my approach is helpful to you ;)