- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 788
In \app\build.gradle.kts I have added:
buildFeatures {
dataBinding = true
}
Sync gradle files.
Add Kotlin class "ButtonViewModel":
package com.milosev.mymvvmexample
import androidx.lifecycle.ViewModel
class ButtonViewModel : ViewModel() {
fun onButtonClick() {
println("Hello, Kotlin!")
}
}
In activity_main.xml add layout and data so it looks like:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.milosev.mymvvmexample.ButtonViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.onButtonClick()}"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MainActivity.kt:
package com.milosev.mymvvmexample
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.milosev.mymvvmexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProvider(this)[ButtonViewModel::class.java]
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
Example download from here.
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 769
var context: Context = Mockito.mock(Context::class.java)
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 997
//MockWebServer
testImplementation('com.squareup.okhttp3:mockwebserver:5.0.0-alpha.11')
testImplementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.11")
//RetroFit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
My example I have to change a little bit to be testable.
Api service:
interface IPostSomethingApiService {
@Headers("Content-Type: text/json")
@POST("/api/UpdateCoordinates")
fun postMethod(@Body value: String): Call<String>
}
In order to be able to test onResponse and onFailure asynchronous callbacks of Retrofit, I have introduced new interface:
interface IPostSomethingCallbacks {
fun onSuccess(message: String)
fun onFailure(message: String)
}
Class to accept post:
package com.example.mockwebserverexample
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class PostSomething (var postSomethingApiService: IPostSomethingApiService, var postSomethingCallbacks: IPostSomethingCallbacks) {
private lateinit var webApiRequest: Call<String>
fun myHttpPost(value: String) {
webApiRequest = postSomethingApiService.postMethod(value)
webApiRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
if (!response.isSuccessful) {
postSomethingCallbacks.onSuccess(response.errorBody()!!.charStream().readText())
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
postSomethingCallbacks.onFailure(t.message.toString())
}
})
}
}
Unit test:
package com.example.mockwebserverexample
import junit.framework.TestCase
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
//MockWebServer
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
//Retrofit
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
class RetrofitUnitTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var postSomethingApiService: IPostSomethingApiService
private lateinit var postSomething: PostSomething
private lateinit var postSomethingCallbacks: PostSomethingCallbacks
@Before
fun setUp() {
mockWebServer = MockWebServer()
val client = OkHttpClient.Builder()
.build()
postSomethingApiService = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.build().create(IPostSomethingApiService::class.java)
postSomethingCallbacks = PostSomethingCallbacks()
postSomething = PostSomething(postSomethingApiService, postSomethingCallbacks)
mockWebServer.start()
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `check if 400 response results in an error state`() {
val response = MockResponse()
.setBody("400 error test")
.setResponseCode(400)
mockWebServer.enqueue(response)
postSomething.myHttpPost("test")
runBlocking {
postSomethingApiService.postMethod("test 400")
}
}
}
class PostSomethingCallbacks: IPostSomethingCallbacks {
override fun onSuccess(message: String) {
TestCase.assertEquals("400 error test",message)
}
override fun onFailure(message: String) {
TestCase.assertEquals("400 error test",message)
}
}
Notice code:
postSomething = PostSomething(postSomethingApiService, postSomethingCallbacks)And class where are my asserts:
class PostSomethingCallbacks: IPostSomethingCallbacks {
override fun onSuccess(message: String) {
TestCase.assertEquals("400 error test",message)
}
override fun onFailure(message: String) {
TestCase.assertEquals("400 error test",message)
}
}
Example download from here.
Update: Here is maybe better solution without MockWebServer which gave me ChatGPT:
import com.nhaarman.mockitokotlin2.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MyCallbackTest {
private lateinit var mockedCall: Call
private lateinit var myCallback: MyCallback
private lateinit var mockedResponse: Response
private lateinit var mockedResponseBody: String
@Before
fun setup() {
mockedCall = mock()
myCallback = MyCallback()
mockedResponse = mock()
mockedResponseBody = "response body"
}
@Test
fun `onResponse success`() {
// Given
whenever(mockedResponse.isSuccessful).thenReturn(true)
whenever(mockedResponse.body()).thenReturn(mockedResponseBody)
whenever(mockedResponse.code()).thenReturn(200)
// When
myCallback.onResponse(mockedCall, mockedResponse)
// Then
assertEquals(mockedResponseBody, myCallback.responseBody)
}
@Test
fun `onResponse failure`() {
// Given
val errorResponseBodyString = "error response body"
val errorCode = 404
whenever(mockedResponse.isSuccessful).thenReturn(false)
whenever(mockedResponse.code()).thenReturn(errorCode)
whenever(mockedResponse.errorBody()?.string()).thenReturn(errorResponseBodyString)
// When
myCallback.onResponse(mockedCall, mockedResponse)
// Then
assertEquals(errorResponseBodyString, myCallback.errorBody)
assertEquals(errorCode, myCallback.errorCode)
}
private inner class MyCallback : Callback {
var responseBody: String? = null
var errorBody: String? = null
var errorCode: Int? = null
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
responseBody = response.body()
} else {
errorBody = response.errorBody()?.string()
errorCode = response.code()
}
}
override fun onFailure(call: Call, t: Throwable) {
// Not needed for this test
}
}
}
Gradle:
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
// Optional -- Robolectric environment
testImplementation 'androidx.test:core:1.6.0-alpha01'
// Optional -- Mockito framework
testImplementation 'org.mockito:mockito-core:5.2.0'
// Optional -- mockito-kotlin
testImplementation 'org.mockito.kotlin:mockito-kotlin:4.1.0'
// Optional -- Mockk framework
testImplementation 'io.mockk:mockk:1.13.4'
testImplementation 'junit:junit:4.13.2'
testImplementation('com.squareup.okhttp3:mockwebserver:5.0.0-alpha.11')
testImplementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.11")
// required if you want to use Mockito for Android tests
androidTestImplementation 'org.mockito:mockito-android:5.2.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.google.android.gms:play-services-location:21.0.1'
implementation 'com.google.code.gson:gson:2.10.1'
- Details
- Written by: Stanko Milosev
- Category: Android
- Hits: 1148
using Microsoft.AspNetCore.Mvc;
namespace WebApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet]
public string Get()
{
return "test";
}
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
[HttpPost]
public string Post([FromBody] string value)
{
return $"Sent: {value}";
}
}
Now example in Kotlin.
In \app\src\main\AndroidManifest.xml I have added internet permissions:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />In \app\src\main\res\layout\activity_main.xml I have added the button:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button"
android:onClick="postHttp"
tools:layout_editor_absoluteX="166dp"
tools:layout_editor_absoluteY="441dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
/>
</androidx.constraintlayout.widget.ConstraintLayout>
In \app\build.gradle I have added
implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.okhttp3:okhttp:4.9.0" implementation 'com.squareup.retrofit2:converter-scalars:2.1.0'Code to trust all certificates taken from here.
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.i(MainActivity::class.simpleName, "checkClientTrusted")
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
Log.i(MainActivity::class.simpleName, "checkServerTrusted")
}
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
})
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
val sslSocketFactory = sslContext.socketFactory
// connect to server
val client = OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager).hostnameVerifier{ _, _ -> true }.build()
Next building Retrofit:
val retro = Retrofit.Builder()
.baseUrl("https://10.0.2.2:7037")
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.build()
Here notice baseUrl: https://10.0.2.2:7037 - 10.0.2.2 is localhost for Android Studio emulator
Add WebApiService interface:
interface WebApiService {
@Headers("Content-Type: text/json")
@POST("/api/Values")
fun postMethod(@Body value: String): Call<String>
}
Create request:
val service = retro.create(WebApiService::class.java)
val webApiRequest = service.postMethod("\"test\"")
Here notice "\"test\"" since I am trying to send a raw string, otherwise would be rejected.
Last but not least, onResponse and onFailure:
val alertDialogBuilder = AlertDialog.Builder(this@MainActivity)
webApiRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
if (!response.isSuccessful) {
alertDialogBuilder.setMessage(response.errorBody()!!.charStream().readText())
.setCancelable(false)
.setNeutralButton("OK", DialogInterface.OnClickListener { dialog, _ ->
dialog.dismiss()
})
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
}
else {
alertDialogBuilder.setMessage("Response: ${response.body().toString()}")
.setCancelable(false)
.setNeutralButton("OK", DialogInterface.OnClickListener { dialog, _ ->
dialog.dismiss()
})
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
alertDialogBuilder.setMessage(t.message)
.setCancelable(false)
.setNeutralButton("OK", DialogInterface.OnClickListener {
dialog, _ -> dialog.dismiss()
})
val alert = alertDialogBuilder.create()
alert.setTitle("Error")
alert.show()
}
})
.NET solution download from here and Android source code download from here here.