DEV Community

Cover image for Retrofit2: An Appium Implementation to Upload APKs to Browserstack
Manuel Enrique Mariñez
Manuel Enrique Mariñez

Posted on • Edited on

Retrofit2: An Appium Implementation to Upload APKs to Browserstack

All right the following implementation will be done based on a Gradle project using Kotlin but others build automation tools may follow a similar approach. With that taken out of the way let's start with the dependencies you are going to need in order to use retrofit2.

Dependencies:

implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
Enter fullscreen mode Exit fullscreen mode

Once that is all set "sync" your gradle file for this changes to be applied.

Now that retrofit2 is up and running in our project let's go to our "Driver" class to setup our service generator that is going to help us interact with the requests needed to upload our APK to the Browserstack platform.

First lets import our libraries and start our service:

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.ResponseBody
import retrofit2.Response
Enter fullscreen mode Exit fullscreen mode
private val service = ServiceGenerator().createService(FileUploadService::class.java)
Enter fullscreen mode Exit fullscreen mode

By now you might be wondering "why are we using okhttp3? isn't this about retrofit2? What is this ServiceGenerator() class? Did I miss a page?"

These are questions I too struggled a lot to understand at first, so we will go step by step.

OkHttp3 is used for making HTTP requests and managing network connections. It simplifies the process of sending requests, handling responses, and managing the underlying network infrastructure. Retrofit2 is also a network handler capable of building and executing HTTP request. Both of these technologies are develop by the same company Square INC and they complement each other, while OkHttp focuses on managing low-level HTTP connections, Retrofit is a higher-level library that simplifies the process of interacting with RESTful web services.

The ServiceGenerator() class is a utility class used in Retrofit implementations to create and configure instances of Retrofit service interfaces. This class typically encapsulates the setup of the Retrofit client and the creation of service instances.

Now we will go ahead and create our kotlin class for the Service Generator. It should look like the following:

import okhttp3.Credentials
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import utils.AuthenticationInterceptor

class ServiceGenerator {
    private val baseURL = "https://api-cloud.browserstack.com/"
    private val userName: String = System.getenv("BROWSERSTACK_USERNAME")
    private val accessKey: String = System.getenv("BROWSERSTACK_ACCESS_KEY")
    private val authToken = Credentials.basic(userName, accessKey)
    private val interceptor = AuthenticationInterceptor(authToken)
    private val httpClient = OkHttpClient.Builder().addInterceptor(interceptor)

    private val builder: Retrofit.Builder = Retrofit.Builder()
        .baseUrl(baseURL)
        .addConverterFactory(GsonConverterFactory.create())

    private var retrofit: Retrofit = builder.build()

    fun <S> createService(serviceClass: Class<S>): S {
        retrofit = builder.client(httpClient.build()).build()
        return retrofit.create(serviceClass)
    }
}
Enter fullscreen mode Exit fullscreen mode

What are we setting up here?

  1. Base URL for the Browserstack URL
  2. Browserstack credentials (recommended to be stored in environment variables)
  3. Basic Credentials handler by the Okhttp3 library
  4. Authorization interceptor (also known to be our header handler we will see more of it next for the implementation)
  5. Finally the rest of the code shows how to setup the httpClient to handle all the data we are going to send trough the service in order to execute the requests we need for the apk upload where lastly there is the createService method that will return the retrofit service we need.

Now as previously mention in step 4, we are going to need a hearder handler and for that we are going to create yet another kotlin class that contains the following:

import java.io.IOException
import okhttp3.Interceptor
import okhttp3.Response


class AuthenticationInterceptor(private val authToken: String) : Interceptor {
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val builderWithAuth = original.newBuilder()
            .header("Authorization", authToken)
            .header("Content-Type", "multipart/form-data")
            .build()
        return chain.proceed(builderWithAuth)
    }
}
Enter fullscreen mode Exit fullscreen mode

In my particular case I end up calling this class AuthenticationInterceptor because it's main function for me was to handle my browser credentials but something like HeaderInterceptor would be more appropriate since it able to manage any required header parameter. But by now authorization and content type is all we need. Authorization for our Browserstack credentials and Content-type to let know the server what content we will be uploading in our request.

Well we are almost done with the setup and there is one last thing we need to take into consideration, at the beginning the service call you might have notice that we are passing FileUploadService as an argument for our service, that is our next step, the interface that will manage our RESTAPI methods (Get, Post, etc...).

This time we are going to create an Interface file that manages Get and Post requests (we will mainly need Post for the upload but later on we will need Get for upload validation).

import okhttp3.MultipartBody
import retrofit2.Call
import okhttp3.ResponseBody
import retrofit2.http.*

interface FileUploadService {
    @Multipart
    @POST("app-automate/upload")
    fun upload(
        @Part fileMultiPart: MultipartBody.Part
    ): Call<Void>

    @GET("app-automate/recent_apps") 
    fun getRecentApps(): Call<ResponseBody>
}
Enter fullscreen mode Exit fullscreen mode

In here we can spot the following:

  1. Multipart annotation to highlight that we are going to be handling a multipart file upload (this is why earlier we added the Content-type multipart in the header handler).

  2. Post annotation with the endpoint app-automate/upload, with this we are going to manage our post request and the endpoint we are pointing to in the Browserstack API.

  3. Our upload method that contains the file multipart parameter that we will be passing in the usage

  4. The return type void since we won't need to return any values (This could also be manage with a RequestBody return type).

  5. Finally Our Get manager called getRecentApps with a similar structure as our Post manager but without needed parameters

With this we finally finish with our service set up, now let's go back to our Driver class for the usage.

private val service = ServiceGenerator().createService(FileUploadService::class.java)
Enter fullscreen mode Exit fullscreen mode

With our service up and running we will now create our upload method that will send the requests and indicate our desired APK to bu uploaded.

private fun uploadFileRetrofit() {

        // Get apk file
        val file = File("./src/test/resources/apps/Sample.apk")

        // MultipartBody.part is used to send also the actual file name
        val fileRequest = file.asRequestBody("application/vnd.android.package-archive".toMediaTypeOrNull())

        val apkPart = MultipartBody.Part.createFormData("file", file.name, fileRequest)

        // Execute the Request
        val call = service.upload(apkPart)
        val response = call.execute()

        if (response.isSuccessful) {
            println("APK upload successful")
        } else {
            println("APK upload failed")
            println(response.errorBody()?.string())
        }

    }
Enter fullscreen mode Exit fullscreen mode

Now in here we see the following implementations:

  • We highlight our file location within the project with the File library (imported with import java.io.File)
  • Set up the file as a request body and add it's media type for APKs ("application/vnd.android.package-archive")
  • Multipart body creation with the file name parameter, file name of the actual file we are uploading and the file's request body.

I can't stress enough how important is to add the "file" argument in the file name parameter as this is the argument expected from the Browserstack API. I scratched my head A LOT the first time implementing retrofit2 and I was getting a mislead error message all because I wasn't sending this parameter correctly.

  • We then call our upload method from our service interface and pass our now converted apk multipart (apkPart).
  • Request call execution.
  • Finally the response validation

This is all you need in order to upload a file using retrofit now that the method is set. Below is a little example of how I use it that includes the "Get" request from our Interface previously created.

response = service.getRecentApps().execute()
            responseBody = response.body()!!.string()
            val isApkUploaded = responseBody.contains("\"app_name\":\"Sample.apk\"")

            if (isApkUploaded) {
                println("APK file 'Sample.apk' is already uploaded.")

                val responseJsonArray = JsonParser.parseString(responseBody).asJsonArray
                for(jsonElement in responseJsonArray){
                    val jsonObject = jsonElement.asJsonObject
                    val appId = jsonObject.get("app_id")
                    return appId.asString
                }

            } else {
                println("APK file 'Sample.apk' is not uploaded.")
                uploadFileRetrofit()

                response = service.getRecentApps().execute()
                responseBody = response.body()!!.string()

                val responseJsonArray = JsonParser.parseString(responseBody).asJsonArray
                for(jsonElement in responseJsonArray){
                    val jsonObject = jsonElement.asJsonObject
                    val appId = jsonObject.get("app_id")
                    return appId.asString
                }
            }
Enter fullscreen mode Exit fullscreen mode

And even after all that you might be wondering "Why would I do all this if I can just setup a method that sends the cURL command that I need to upload a file given by the Browserstack documentation itself?", and you are not wrong that was literally my first approach and it WORKS, but I though on sharing this more abstract and programmatically "organized" way to handle file uploads in automation projects for anyone interested in trying new ways adding a more maintainable code. Thank you for your time if you reach this part and see you in the next one.

Top comments (0)