I am running across weird behaviors with HttpLoggingInterceptor. I have noticed that if I use newBuilder() the logging does not work.
// instantiate object (in app)
val okHttpRequestManager: HttpRequestManager = OkHttpRequestManager(OkHttpClient(), null)
// execute request (in app)
okHttpRequestManager.execute(request, callback)
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
client.newBuilder().addInterceptor(httpLoggingInterceptor).build()
}
// perform request below
...
}
}
The above code snippet does not work. However, if I make my parameter a builder, everything works fine. Is using newBuilder() the incorrect way to do this?
// the below works
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient.Builder,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
// no newBuilder() or build() and works like a charm
client.addInterceptor(httpLoggingInterceptor)
}
// perform request below
...
}
}
Anyone have an idea as to why this is?
That's because the method newBuilder() as the name implies, returns the new builder object and when you call build() on it, new instance of OkHttpClient will be returned created from the new builder.
Here is the source code:
/** Prepares the [request] to be executed at some point in the future. */
override fun newCall(request: Request): Call {
return RealCall.newRealCall(this, request, forWebSocket = false)
}
build() method
fun build(): OkHttpClient = OkHttpClient(this)
The newBuilder adds to the attributes of the existing client so you
will have a new client with both the old and new attributes.
If you want to use newBuilder() method then you need to make use of the newly created OkHttpClient.
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
val newClient = client.newBuilder().addInterceptor(httpLoggingInterceptor).build()
}
// perform request below using newClient
...
}
}
Related
I am making an api call using retrofit and I want to write a unit test to check if it returns an exception.
I want to force the retrofit call to return an exception
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
try {
val creditReport = apiServiceInterface.getDataFromApi() // THIS SHOULD RETURN AN EXCEPTION AND I WANT TO CATCH THAT
return CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
return CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
I have written a test case for getCreditReport which should validate the failure scenario
#Test
fun getCreditReportThrowException() {
runBlocking {
val response = dataRepository.getCreditReport()
verify(apiServiceInterface, times(1)).getDataFromApi()
Assert.assertEquals(CreditReportResponse.Status.FAILURE, response.status)
}
}
so to make the above test case pass, I need to force the network call to throw and exception
please suggest
Thanks
R
Actually #Vaibhav Goyal provided a good suggestion to make your testing as easier. Assuming you are using MVVM structure, in your test cases you can inject a "mock" service class to mock the behaviours that you defined in the test cases, so the graph will be like this
Since I am using mockk library at the moment, the actual implementation in your code base would be a little bit different.
#Test
fun test_exception() {
// given
val mockService = mockk<ApiServiceInterface>()
val repository = DataRepository(mockService)
every { mockService.getDataFromApi() } throws Exception("Error")
// when
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
}
But if you want to test the exception thrown from Retrofit, then you might need mockServer library from square to help you to achieve this https://github.com/square/okhttp#mockwebserver
And the graph for this would be like this
You also have to setup the mock server to do so
#Test
fun test_exception_from_retrofit() {
// can put in the setup method / in junit4 rule or junit5 class
val mockWebServer = MockWebServer()
mockWebServer.start()
// given
val service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.build()
.create(ApiServiceInterface::class)
val repository = DataRepository(service)
// when
mockWebServer.enqueue(MockResponse()
.setResponseCode(500)
.setBody("""{"name":"Tony}""") // you can read the json file content and then put it here
)
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
// can put in tearDown / in junit4 rule or juni5 class
mockWebServer.shutdown()
}
SO you can test different exception like json format invalid, 500 status code,data parsing exception
Bonus point
Usually I would put the testing json under test directory and make it almost same as the api path for better maintainence
I have some errors in Retrofit2 and Kotlin Coroutines technologies. I need to dynamically query an info in my service.
For example, the URL is "https://exampleapidomain.com/api/sub_categories/read.php?id=2" I want to change id parameter dynamically.
My service:
interface AltKategoriService {
#GET("alt_kategoriler/" + Const.READ_URL_PIECE)
fun getId(#Query("id") id: String?): Call<Resource<AltKategorilerResponse>>
companion object{
fun build(): AltKategoriService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Const.BASE_URL)
.client(okHttpClient)
.build()
return retrofit.create(AltKategoriService::class.java)
}
}
}
My DataSource file:
class RemoteAltKategorilerDataSource : AltKategorilerDataSource {
override fun getSubCategories(): Flow<Resource<AltKategorilerResponse>> = flow {
try {
emit(Resource.Loading())
val call = AltKategoriService.build().getId("2").execute()
if (call.isSuccessful) {
call.body()?.let {
emit(it)
}
}
} catch (ex: Exception) {
emit(Resource.Error(ex))
ex.printStackTrace()
}
}
}
I get the following error:
Attempt to invoke virtual method 'void androidx.lifecycle.MutableLiveData.postValue(java.lang.Object)' on a null object reference" and then, app crashes.
I'm waiting for your answers and code examples. Thank you!
Edited. My ViewModel:
class SubCategoryViewModel: ViewModel() {
private val altKategoriRepository = AltKategoriRepository()
init {
getUsers()
}
var loading: MutableLiveData<Boolean>? = MutableLiveData()
var altKategoriLiveData = MutableLiveData<AltKategorilerResponse>()
var error = MutableLiveData<Throwable>()
fun getUsers() = viewModelScope.launch {
altKategoriRepository.getSubCategories()
.asLiveData(viewModelScope.coroutineContext).observeForever {
when (it.status) {
ResourceStatus.LOADING -> {
loading?.postValue(true)
}
ResourceStatus.SUCCESS -> {
Log.e("Message", it.data.toString())
altKategoriLiveData.postValue(it.data!!)
loading?.postValue(false)
}
ResourceStatus.ERROR -> {
error.postValue(it.throwable!!)
loading?.postValue(false)
}
}
}
}
}
Kotlin class initialisation takes place in the following order:
primary constructor -> init block -> secondary constructor
As no initialisation is done neither for var loading, var altKategoriLiveData nor var error class members of SubCategoryViewModel by the time getUsers() is called in the init { } block, you get the exception resulting in the app crash.
Regarding your implementation of the MVVM pattern, it contradicts to that of the official Android documentation, where a View is supposed to call a corresponding method of ViewModel explicitly or implicitly.
It should also work by just moving the init { } after the variable declarations or if you declare your loading state directly as LOADING. Also i think it's fine to declare it inside the init block, the documentation doesn't refer to that as being at fault if i'm reading correctly. Also it helps doing the call in init to avoid multiple times loading in Activities or Fragments if you only need to do the call once when the viewmodel is created and avoiding multiple calls by e.g: orientation change or other lifecycle dependent things. but please correct me if i'm wrong.
I know it's possible to test Retrofit request & response with MockWebServer, like this:
interface AppApi {
#GET("/time/")
suspend fun time(): TimeResponse
}
...
class CoinBaseApiClientTest {
private val mockWebServer = MockWebServer()
private fun createClient(): AppApi {
return AppApiFactory.createAppApi(baseUrl = mockWebServer.url("/").toString())
}
#Before
fun setUp() {
mockWebServer.start()
}
#After
fun tearDown() {
mockWebServer.shutdown()
}
#Test
fun fetches_time() = runBlocking {
val timeData: String = """
{
"iso": "2015-01-07T23:47:25.201Z",
"epoch": 1420674445.201
}
"""
mockWebServer.enqueue(MockResponse().mockSuccess(200, timeData))
val timeResponse = createClient().time()
val recordedRequest = mockWebServer.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/time/")
assertThat(timeResponse.iso).isEqualTo("2015-01-07T23:47:25.201Z")
assertThat(timeResponse.epochAsMillis).isEqualTo(1420674445201)
}
However, in my case, I only want to test its request payload, such as path, header... without actually execute the time() API (the reason is the actual timeData is really big). So I set mockWebServer.enqueue(MockResponse()) but it doesn't work - it seems require valid TimeResponse JSON data.
Do you know is it possible to only test Retrofit request payload without actually execute the request?
You could just have it return an error status code (i.e. 500) with empty data.
I'm testing api that returns result using suspending function with MockWebServer, but it does not work with runBlockingTest, testCoroutineDispatcher, testCorounieScope unless a launch builder is used, why?
abstract class AbstractPostApiTest {
internal lateinit var mockWebServer: MockWebServer
private val responseAsString by lazy {
getResourceAsText(RESPONSE_JSON_PATH)
}
#BeforeEach
open fun setUp() {
mockWebServer = MockWebServer()
println("AbstractPostApiTest setUp() $mockWebServer")
}
#AfterEach
open fun tearDown() {
mockWebServer.shutdown()
}
companion object {
const val RESPONSE_JSON_PATH = "posts.json"
}
#Throws(IOException::class)
fun enqueueResponse(
code: Int = 200,
headers: Map<String, String>? = null
): MockResponse {
// Define mock response
val mockResponse = MockResponse()
// Set response code
mockResponse.setResponseCode(code)
// Set headers
headers?.let {
for ((key, value) in it) {
mockResponse.addHeader(key, value)
}
}
// Set body
mockWebServer.enqueue(
mockResponse.setBody(responseAsString)
)
return mockResponse
}
}
class PostApiTest : AbstractPostApiTest() {
private lateinit var postApi: PostApiCoroutines
private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
#BeforeEach
override fun setUp() {
super.setUp()
val okHttpClient = OkHttpClient
.Builder()
.build()
postApi = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(PostApiCoroutines::class.java)
Dispatchers.setMain(testCoroutineDispatcher)
}
#AfterEach
override fun tearDown() {
super.tearDown()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
#Test
fun `Given we have a valid request, should be done to correct url`() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
postApi.getPostsResponse()
advanceUntilIdle()
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
}
Results error: java.lang.IllegalStateException: This job has not completed yet
This test does not work if launch builder is used, and if launch builder is used it does not require testCoroutineDispatcher or testCoroutineScope, what's the reason for this? Normally suspending functions pass without being in another scope even with runBlockingTest
#Test
fun `Given we have a valid request, should be done to correct url`() =
runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
launch {
postApi.getPosts()
}
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
The one above works.
Also the test below pass some of the time.
#Test
fun Given api return 200, should have list of posts() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200)
// WHEN
var posts: List<Post> = emptyList()
launch {
posts = postApi.getPosts()
}
advanceUntilIdle()
// THEN
Truth.assertThat(posts).isNotNull()
Truth.assertThat(posts.size).isEqualTo(100)
}
I tried many combinations invoking posts = postApi.getPosts() without launch, using async, putting enqueueResponse(200) inside async async { enqueueResponse(200) }.await() but tests failed, sometimes it pass sometimes it does not some with each combination.
There is a bug with runBlockTest not waiting for other threads/jobs to complete before completing the coroutine that the test is running in.
I tried using runBlocking with success (I use the awesome port of Hamcrest to Kotlin Hamkrest)
fun `run test` = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(""))
// make HTTP call
val result = mockWebServer.takeRequest(2000L, TimeUnit.MILLISECONDS)
assertThat(result != null, equalTo(true))
}
There's a few things to note here:
The use of thread blocking calls should never be called without a timeout. Always better to fail with nothing, then to block a thread forever.
The use of runBlocking might be considered by some to be no no. However this blog post outlines the different method of running concurrent code, and the different use cases for them. We normally want to use runBlockingTest or (TestCoroutineDispatcher.runBlockingTest) so that our test code and app code are synchronised. By using the same Dispatcher we can make sure that the jobs all finish, etc. TestCoroutineDispatcher also has that handy "clock" feature to make delays disappear. However when testing the HTTP layer of the application, and where there is a mock server running on a separate thread we have a synchronisation point being takeRequest. So we can happily use runBlocking to allow us to use coroutines and a mock server running on a different thread work together with no problems.
I have this code in a fragment:
#ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
create_adoption_btn.setOnClickListener {
val temp = Intent(activity!!.baseContext, AdoptionCreationActivity::class.java)
activity!!.startActivityFromFragment(this, temp, 1)
}
val mLayoutManager = GridLayoutManager(activity!!.baseContext, 1)
recycler_view.layoutManager = mLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
//recycler_view.adapter = adapter
//AppController.instance!!.getAdoptionList().await()
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT) {
Log.i("TestAdapter", "Beginning fetch")
val adapter = AlbumsAdapter(activity!!, AppController.instance!!.getAdoptionList()) //Skips this line, but still executes it
Log.i("TestAdapter", "Adapter: ${adapter.itemCount}")
recycler_view.adapter = adapter
adapter.notifyDataSetChanged()
Log.i("TestAdapter", "Adapter updated on thread")
}
}
And this for a class that extends Application
class AppController : Application() {
private var adoptionCardList: MutableList<AdoptionCard> = mutableListOf()
override fun onCreate() {
super.onCreate()
instance = this
}
fun getAdoptionList(): MutableList<AdoptionCard> {
if(adoptionCardList.count() == 0) {
val service = GetVolley()
val apiController = ApiController(service)
val path = "adoptions/read.php"
apiController.get(path, JSONArray()){ response ->
if (response != null) {
var x = 0
while(x <= response.length() - 1){
val jsonObject = (response[x] as JSONObject)
adoptionCardList.add(AdoptionCard(
jsonObject.getInt("id"),
jsonObject.getString("adoption_title"),
jsonObject.getString("user_id").toBigInteger(),
jsonObject.getString("adoption_created_time")))
x+=1
}
}
}
}
return adoptionCardList
}
private val requestQueue: RequestQueue? = null
get() {
if (field == null) {
return Volley.newRequestQueue(applicationContext)
}
return field
}
fun <T> addToRequestQueue(request: Request<T>, tag: String) {
request.tag = if (TextUtils.isEmpty(tag)) TAG else tag
requestQueue?.add(request)
}
fun <T> addToRequestQueue(request: Request<T>) {
request.tag = TAG
requestQueue?.add(request)
}
fun cancelPendingRequests(tag: Any) {
if (requestQueue != null) {
requestQueue!!.cancelAll(tag)
}
}
companion object {
private val TAG = AppController::class.java.simpleName
#get:Synchronized var instance: AppController? = null
private set
}
The "launch" coroutine should wait until Volley retrieves all information from the server but it just skips that line and the Recycler View doesn't update, since the MutableList is empty. If I reload the Fragment, it will do this successfully since there's an already stored list. I read all documentation I could on Kotlin Coroutines and questions asked but I can't make this work. Could anyone help me?
The debug:
Debug log
On the first load, as you can see, the adapter has 0 elements, so the view gets nothing; on the second load, it already has 3 elements, so the Recycler view loads those 3.
ApiController:
class ApiController constructor(serviceInjection: RESTapi): RESTapi {
private val service: RESTapi = serviceInjection
override fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit) {
service.get(path, params, completionHandler)
}
}
Interface:
interface RESTapi {
fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit)
}
GetVolley class:
class GetVolley : RESTapi {
val TAG = GetVolley::class.java.simpleName
val basePath = "http://192.168.0.161/animals/"
override fun get(path: String, params: JSONArray, completionHandler: (response: JSONArray?) -> Unit) {
val jsonObjReq = object : JsonArrayRequest(Method.GET, basePath + path, params,
Response.Listener<JSONArray> { response ->
Log.d(TAG, "/get request OK! Response: $response")
completionHandler(response)
},
Response.ErrorListener { error ->
VolleyLog.e(TAG, "/get request fail! Error: ${error.message}")
completionHandler(null)
}) {
#Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
val headers = HashMap<String, String>()
headers["Content-Type"] = "application/json"
return headers
}
}
AppController.instance?.addToRequestQueue(jsonObjReq, TAG)
}
Your problem here is that Volley is async by default. What this means is that it creates a new thread to run the call on. Since you're using coroutines, this is pointless. You'll need to force it over on the active thread and do a sync call instead.
This part:
AppController.instance?.addToRequestQueue(jsonObjReq, TAG)
Adds it to a request queue. This means it doesn't execute it instantly, but queues it with other requests (if there are any), and launches it on a separate thread. This is where your problem lies. You need to use a sync request instead. Async simply means "not on this thread", regardless of which thread. So since you're using a different one (coroutine), you'll need to force it to be sync. This makes it sync with the active thread, not the main thread.
I'm not sure if this will even work with coroutines, but since it's async, it should be fine.
In order to block the thread, you can use a RequestFuture<JSONArray> as a replacement for the callbacks. You still need to add it to the request queue, but you can call .get on the RequestFuture, which blocks the thread until the request is complete, or it times out.
val future = RequestFuture.newFuture<JSONArray>() // The future
val jsonObjReq = object : JsonArrayRequest(Method.GET, basePath + path, params,
future, // This is almost identical as earlier, but using the future instead of the different callback
future) {
#Throws(AuthFailureError::class)
override fun getHeaders(): Map<String, String> {
val headers = HashMap<String, String>()
headers["Content-Type"] = "application/json"
return headers
}
}
AppController.instance?.addToRequestQueue(jsonObjReq, TAG);// Adds it to the queue. **This is very important**
try {
// Timeout is optional, but I highly recommend it. You can rather re-try the request later if it fails
future.get(30, TimeUnit.SECONDS).let { response ->
completionHandler(response)
}
}catch(e: TimeoutException){
completionHandler(null)
// The request timed out; handle this appropriately.
}catch(e: InterruptedException){
completionHandler(null)
// The request timed out; handle this appropriately.
}catch(e: ExecutionException){
completionHandler(null)
// This is the generic exception thrown. Any failure results in a ExecutionException
}
// The rest could be thrown by the handler. I don't recommend a generic `catch(e: Exception)`
This will block the thread until the response is received, or it times out. The reason I added a timeout is in case it can't connect. It's not that important since it's a coroutine, but if it times out, it's better handling it by notifying the user rather than trying over and over and loading forever.
The problem arises in your apiController.get() call, which returns immediately and not after the network operation is complete. You supply your response callback to it. It will run eventually, once the REST call has got its response.
This is how you should adapt your function to coroutines:
suspend fun getAdoptionList(): MutableList<AdoptionCard> {
adoptionCardList.takeIf { it.isNotEmpty() }?.also { return it }
suspendCancellableCoroutine<Unit> { cont ->
ApiController(GetVolley()).get("adoptions/read.php", JSONArray()) { response ->
// fill adoptionCardList from response
cont.resume(Unit)
}
}
return adoptionCardList
}
This is now a suspend fun and it will suspend itself in case the adoption list isn't already populated. In either case the function will ensure that by the time it returns, the list is populated.
I would also advise you to stop using GlobalScope in order to prevent your network calls running in the background, possibly holding on to the entire GUI tree of your activity, after the activity is destroyed. You can read more about structured concurrency from Roman Elizarov and you can follow the basic example in the documentation of CoroutineScope.