I was developing some business app, which made as backend services of other Android app, which mange the franted content showed to the user.
My app work manage some intent recive fro an API call, doing some action based on this intent, and returning the raw data to the fronted app.
So, in oder to test it, I think in the implemention of a test function, but I get in to the trobble of how invoke the onActivityResult() from the testing context:
I have something like that:
MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var message = getString(R.string.processing)
val request = HioPosRequest.forIntent(intent = intent)
//Put here because we need access to Compose context to manage SharePreferences.
viewModel = hiltViewModel()
viewModel.launchProcessing(request).also {
lifecycleScope.launch(Dispatchers.IO) {
viewModel.hioPosIntentResult.collect() {
if (it != null) {
sendResult(it)
}
}
}
lifecycleScope.launch(Dispatchers.IO) {
viewModel.uiMessage.collect() { state ->
message = state
}
}
lifecycleScope.launch(Dispatchers.IO){
viewModel.finnishMainIntent.collect(){
//if(it) finish()
Log.e("trace", "Invoca el Main Intent")
}
}
}
HioPosConnectorTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Message(message)
}
}
}
}
private fun sendResult(hioPosResponseIntent: HioPosResponseIntent) {
if(hioPosResponseIntent.isError){
this.setResult(Activity.RESULT_CANCELED, hioPosResponseIntent.resultIntent)
this.finish()
}else{
this.setResult(Activity.RESULT_OK, hioPosResponseIntent.resultIntent)
this.finish()
}
}
}
@Composable
fun Message(name: String, modifier: Modifier = Modifier) {
Text(
text = name,
modifier = modifier
.padding(16.dp)
.absoluteOffset(x = 0.dp, y = ((LocalConfiguration.current.screenHeightDp) / 2.5).dp),
textAlign = TextAlign.Center,
style = (MaterialTheme.typography).bodyMedium
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
HioPosConnectorTheme {
Message("Processing")
}
}
MainViewModel.kt
@HiltViewModel
class MainViewModel @Inject constructor(@ApplicationContext context: Context,private val sharedPreferences: SharedPreferences) : ViewModel() {
private var _UiMessage: MutableStateFlow<String> = MutableStateFlow(context.getString(R.string.processing))
val uiMessage: StateFlow<String> = _UiMessage.asStateFlow()
private var _hioPosIntentResult: MutableStateFlow<HioPosResponseIntent?> = MutableStateFlow(null)
val hioPosIntentResult: StateFlow<HioPosResponseIntent?> = _hioPosIntentResult.asStateFlow()
private var _finishMainIntent: MutableStateFlow<Boolean> = MutableStateFlow(false)
val finnishMainIntent : StateFlow<Boolean> = _finishMainIntent.asStateFlow()
val contextPromp: Context = context // This warning is a bug of Hilt, it's doesn't leak: https://stackoverflow.com/questions/66216839/inject-context-with-hilt-this-field-leaks-a-context-object
fun launchProcessing(request: HioPosRequest) {
viewModelScope.launch(Dispatchers.IO) {
try {
val shouldBeFinished: Boolean = when (request.action) {
ActionName.Main -> {
_finishMainIntent.value = true
false
}
ActionName.Initialize -> {
val useCase = InitializeUseCase(
request = request,
sharedPreferences = sharedPreferences
)
useCase.getInitializeTransation().also {
_hioPosIntentResult.value = it
}
true
}
ActionName.ShowSetup -> {
val useCase = ShowSetupUseCase(
request = request
)
useCase.getshowSetup().also {
_hioPosIntentResult.value = it
}
true
}
ActionName.Finalize -> {
val useCase = FinalizeUseCase(
request = request,
pinpadApi = AppModule().providePINPADApi()
)
useCase.getFinalizeProgress().also {
_hioPosIntentResult.value = it
}
true
}
ActionName.GetBehavior -> {
val useCase = GetBehaviorUseCase(
request = request
)
useCase.getBehaviour().also {
_hioPosIntentResult.value = it
}
true
}
ActionName.GetVersion -> {
val useCase = GetVersionUseCase(
request = request
)
useCase.getVersion().also {
_hioPosIntentResult.value = it
}
true
}
ActionName.CustomParams -> {
val useCase = CustomParamsUseCase(
request = request
)
useCase.loadParams(context = contextPromp).also {
_hioPosIntentResult.value = it
}
true
}
ActionName.Transaction -> {
val transactionUseCase = TransactionUseCase(
request = request,
transaccinApi = AppModule().provideTransaccinApi(),
uiMessage = _UiMessage,
context = contextPromp
)
val batchCloseUseCase = BatchCloseUseCase(
request = request,
pinpadApi = AppModule().providePINPADApi()
)
val transaction = request.transactionRequest()
if (transaction.transactionType == TransactionType.BatchClose) {
startShiftClose(batchCloseUseCase)
} else {
startTransaction(transaction, transactionUseCase)
}
true
}
else -> {false}
}
if (shouldBeFinished) {
// Todos los requests deberían finalizar la activity salvo que sea una transacción
// que abre otras activities. Si entra en esta condición es porque no se llamó
// correctamente a sendOkResult o sendErrorResult, revisar que ninguna condición haga
// return antes de setear el resultado.
throw Exception("Error finalizing request, undetermined state")
}
} catch (e: Exception) {
Log.e("error", "error: ${e.message}", e)
}
}
}
/*fun finalizeShiftClose(apiResponse: ApiWorkShiftResponse) {
viewModelScope.launch(Dispatchers.IO) {
val state = getState()
val txnRequest = state.request.transactionRequest()
val txnResponse = txnRequest.makeResponse()
if (apiResponse.result.success) {
txnResponse.batchNumber = (System.currentTimeMillis() / 1000).toString()
txnResponse.amount = apiResponse.result.transactionTotalAmount?.toString()
apiResponse.result.transactionListContent?.let { rt ->
txnResponse.batchReceipt = convertReceipt(rt)
}
txnResponse.setOK()
} else {
txnResponse.setFailed(apiResponse.result.message, "Error")
}
txnResponse.sendResultWith(
state.request.makeResponse(this)
)
}
}*/
private suspend fun startShiftClose(batchCloseUseCase: BatchCloseUseCase) {
val request = ApiWorkShiftRequest(
closeShift = true,
listType = ApiWorkShiftRequest.ListType.terminal,
pinpad = "*",
printTransactionList = false,
showTransactionList = false,
returnTransactionList = true,
transactionListContentType = ApiWorkShiftRequest.TransactionListContentType.text
)
batchCloseUseCase.getFinalizeProgress(request)
//finalizeShiftClose() //TODO
}
private suspend fun startTransaction(
transaction: TransactionRequest,
usecase: TransactionUseCase
) {
//startTransaction(transaction)
val opType = when (transaction.transactionType) {
TransactionType.Sale -> ApiTxnStartRequest.OpType.sale
TransactionType.NegativeSale -> ApiTxnStartRequest.OpType.refund
TransactionType.Refund -> ApiTxnStartRequest.OpType.refund
TransactionType.AdjustTips -> ApiTxnStartRequest.OpType.sale
TransactionType.VoidTransaction -> ApiTxnStartRequest.OpType.cancel
else -> throw NotImplementedException("${transaction.transactionType} not supported")
}
val txnStart = ApiTxnStartRequest(
opType = opType,
requestedAmount = transaction.totalAmount().toCents(),
transactionReference = "HioPos#${transaction.transactionId}",
executeOptions = ExecuteOptions(
ExecuteOptions.Method.custom,
userData = "MSG#HioPos"
),
operationDetails = transaction.transactionData?.toLong()
?.let { ApiTxnStartRequestOperationDetails(it) },
createReceipt = true,
receiptType = ApiTxnStartRequest.ReceiptType.text,
pinpad = "*"
)
usecase.transactionProccess(txnStart).also {
_hioPosIntentResult.value = it.hioPosResponseIntent
_UiMessage.value = usecase.uiMessage.value
// We use the mutableStateFlow, which is passed to useCase in order to update the diferents repsonse which getTransactionPoll will be returning.
}
}
/*
private fun convertReceipt(receiptText: String?): String? {
if (receiptText.isNullOrBlank()) return null
val reader = StringReader(receiptText)
val xml = XmlBuilder("Receipt")
reader.forEachLine { line ->
xml.root.element("ReceiptLine") { n ->
n.attribs["type"] = "TEXT"
n.textElement("Text", line)
}
}
return xml.toString()
}
*/
}
Regarding some simple flow which don't make use of an API call, to simmplify the question, we have this, we set the version of the app:
@ActivityRetainedScoped
class GetVersionUseCase @Inject constructor(
val request: HioPosRequest
) {
fun getVersion():HioPosResponseIntent {
val response = request.makeResponse()
response.setString("Version", APK_VERSION)
return response
}
}
As you see it just set an String resource with the version in the response.
So, in my testing functin how should I call to the generate onActivityResoult methd Which is going to give me the response of my app, on this flow?
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("es.paytef.hioposconnector", appContext.packageName)
}
@Test
fun testVersionCase(){
//todo: Send Intent: ForResult to MainActivity
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val intent = Intent(appContext, MainActivity::class.java)
intent.action = "icg.actions.electronicpayment.paytef.GET_VERSION"
//val _activity = InstrumentationRegistry.getInstrumentation().newActivity(ClassLoader.getSystemClassLoader(),MainActivity::class.java.simpleName,intent)
val activity = ActivityScenario.launchActivityForResult<MainActivity>(intent)
Log.d("result", "result: ${activity.result}")
assertEquals(activity.result.resultData, intent)
}
}
I'm getting this error in testVersionCase():
java.lang.AssertionError: expected:<Intent { act=icg.actions.electronicpayment.paytef.GET_VERSION (has extras) }> but was:<Intent { act=icg.actions.electronicpayment.paytef.GET_VERSION cmp=es.paytef.hioposconnector/.MainActivity }>
Any idea??
Thanks in advance!