Unit Test Cases

Asked by At

I am an android developer working on kotlin unit test cases and have a function

class ExpiryDateValidator @Inject constructor() {
var isValid = false

fun isValidExpiryDate(expiryDate: String): Boolean {
    if (expiryDate.isNotEmpty() && expiryDate.length == EXPIRY_DATE_LENGTH) {
        val dateFormat = DateTimeFormat.forPattern(EXPIRY_DATE_FORMAT)
        val dateFormatted = dateFormat.parseDateTime(expiryDate)
        val firstOfNextMonth = dateFormatted.plusMonths(1)
        val lastOfSelectedMonth = firstOfNextMonth.minusDays(1)
        isValid = lastOfSelectedMonth.withTimeAtStartOfDay().isAfterNow
    }
    return isValid
}

companion object {
    const val EXPIRY_DATE_FORMAT = "MM/YY"
    const val EXPIRY_DATE_LENGTH = 5

}}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    interface EnterCardDetailsViewModelInputs : PaymentsViewModelInputs {
        fun onTextFieldChanged(name: String, cardNumber: String, expiryDate: String, cvv: String, issueNumber: String)
        fun onExpiryDateChanged(expiryDate: String)
        fun onButtonClicked(card: NewCardUIModel, cvv: String, saveCard: Boolean)
    }

    interface EnterCardDetailsViewModelOutputs : PaymentsViewModelOutputs {
        fun payButtonEnabled(): Observable<Boolean>
        fun expiryDateFormatted(): Observable<String>
        fun paymentSuccessful(): Observable<String>
        fun addCardSuccessful(): Observable<Unit>
        fun getViewsText(): Observable<Pair<Int, Int>>
        fun getExtraFieldsVisibility(): Observable<Int>
    }

    class EnterCardDetailsViewModel @Inject constructor(application: Application) : PaymentsViewModel(application),
        EnterCardDetailsViewModelInputs,
        EnterCardDetailsViewModelOutputs {

        override val inputs: EnterCardDetailsViewModelInputs
            get() = this

        override val outputs: EnterCardDetailsViewModelOutputs
            get() = this

        private val payButtonEnabled = PublishSubject.create<Boolean>()
        private val expiryDateFormatted = PublishSubject.create<String>()
        private val paymentSuccessful = PublishSubject.create<String>()
        private val getViewsText = PublishSubject.create<Pair<Int, Int>>()
        private val getSwitchVisibility = PublishSubject.create<Int>()
        private val addCardSuccessful = PublishSubject.create<Unit>()

        @Inject
        lateinit var premiseUseCase: PremiseUseCase

        @Inject
        lateinit var userActionUseCase: UserDataActionUseCase

        @Inject
        lateinit var expiryDateValidator: ExpiryDateValidator

        override fun setAmount(paymentAmount: String?) {
            super.setAmount(paymentAmount)
            if (paymentAmount == null) {
                getViewsText.onNext(Pair(R.string.enter_card_details_add_title, R.string.enter_card_details_save_button))
                getSwitchVisibility.onNext(View.GONE)
                return
            }
            getSwitchVisibility.onNext(View.VISIBLE)
            getViewsText.onNext(Pair(R.string.enter_card_details_pay_title, R.string.enter_card_details_pay_button))
        }

        override fun onTextFieldChanged(
            name: String,
            cardNumber: String,
            expiryDate: String,
            cvv: String,
            issueNumber: String
        ) {
            val nameTrimmed = name.trim()
            val validName = isValidName(nameTrimmed)
            val validCardNumber = isValidCardNumber(cardNumber)
            val validExpiryDate = expiryDateValidator.isValidExpiryDate(expiryDate)
            val validCvv = isValidCVV(cvv)
            val validIssueNumber = isValidIssueNumber(issueNumber)
            payButtonEnabled.onNext(validName && validCardNumber && validExpiryDate && validCvv && validIssueNumber)
        }

        override fun onExpiryDateChanged(expiryDate: String) {
            if (expiryDate.matches(EXPIRY_DATE_REGEX_ONE) && expiryDate.length == 2) {
                // it adds symbol / when there are 2 digits inputted in order to get MM/YY
                expiryDateFormatted.onNext("$expiryDate/")
            }
        }

        override fun expiryDateFormatted(): Observable<String> {
            return expiryDateFormatted.observeOn(schedulerProvider.ui()).hide()
        }

        override fun onButtonClicked(card: NewCardUIModel, cvv: String, saveCard: Boolean) {
            val cardUI = CardUIModel(
                cardDigits = card.cardNumber,
                cardType = "",
                cardExpiry = card.expiryDate,
                validFrom = "",
                cardToken = "",
                isAutoTopUp = "",
                cvv = cvv,
                amount = paymentAmount
            )

            if (paymentAmount == null) {
                paymentsUseCase.saveCard(cardUI)
                    .doOnSubscribe { refreshing.onNext(true) }
                    .doFinally { refreshing.onNext(false) }
                    .compose(schedulerProvider.doOnIoObserveOnMainSingle())
                    .subscribe({
                        addCardSuccessful.onNext(Unit)
                    }, {
                        jarvis.trackEvent(
                            ErrorAnalyticsEvent(
                                ErrorAnalyticsEvent.ERROR_ENTER_CARD_SAVE,
                                ErrorAnalyticsEvent.IS_MSG
                            )
                        )
                        jarvis.logError("Failed  saving card", it)
                        if (it is HttpException && it.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                            error.onNext(R.string.enter_card_details_save_card_error)
                        } else {
                            error.onNext(R.string.enter_card_details_save_card_error_generic)
                        }
                    })
                    .also { subscriptions.add(it) }
                return
            }

            val expiryDateFormatted = formatCardExpiryDate(card.expiryDate.replace("/", ""))

            paymentsUseCase.registerAndPay(cardUI, saveCard, expiryDateFormatted)
                .doOnSubscribe { refreshing.onNext(true) }
                .doFinally { refreshing.onNext(false) }
                .flatMap { paymentSuccessful ->
                    if (paymentSuccessful) {
                        userActionUseCase.payment()
                        [email protected] premiseUseCase.getContractStatus()
                    } else {
                        throw PaymentFlowImpl.PaymentFlowException(
                            PaymentFlowImpl.PaymentFlowError.PAYMENT_ERROR, Exception("Failed  making a Payment")
                        )
                    }
                }
                .compose(schedulerProvider.doOnIoObserveOnMainSingle())
                .subscribe(
                    {
                        paymentSuccessful.onNext(it.accountBalance)
                    }, {
                        if (it is PaymentFlowImpl.PaymentFlowException) {

                            if (it.paymentFlowError == PaymentFlowImpl.PaymentFlowError.PAYMENT_ERROR) {
                                //error paying
                                jarvis.trackEvent(
                                    ErrorAnalyticsEvent(
                                        ErrorAnalyticsEvent.ERROR_ENTER_CARD_PAYMENT,
                                        ErrorAnalyticsEvent.IS_MSG
                                    )
                                )
                                jarvis.logError("Failed  making a Payment", it)
                                jarvis.trackEvent(
                                    ErrorDisplayedAnalyticsEvent(
                                        errorMsg = ErrorDisplayedAnalyticsEvent.PAYMENT_NOT_SUCCESSFUL,
                                        errorProcess = ErrorDisplayedAnalyticsEvent.PAYMENT
                                    )
                                )
                                if (it.exception is HttpException && it.exception.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
                                    error.onNext(R.string.enter_card_details_save_card_error)
                                } else {
                                    error.onNext(R.string.enter_card_details_save_card_error_generic)
                                }

                            } else {
                                //error saving card
                                userActionUseCase.payment()
                                paymentSuccessful.onNext("")
                            }
                        } else {
                            jarvis.logError("Failed  making a Payment", it)
                            jarvis.trackEvent(
                                ErrorDisplayedAnalyticsEvent(
                                    errorMsg = ErrorDisplayedAnalyticsEvent.PAYMENT_NOT_SUCCESSFUL,
                                    errorProcess = ErrorDisplayedAnalyticsEvent.PAYMENT
                                )
                            )
                            error.onNext(R.string.enter_card_details_save_card_error_generic)
                        }
                    })
                .also { subscriptions.add(it) }
        }

        override fun paymentSuccessful(): Observable<String> {
            return paymentSuccessful.observeOn(schedulerProvider.ui()).hide()
        }

        override fun payButtonEnabled(): Observable<Boolean> {
            return payButtonEnabled.observeOn(schedulerProvider.ui()).hide()
        }

        override fun getViewsText(): Observable<Pair<Int, Int>> {
            return getViewsText.observeOn(schedulerProvider.ui()).hide()
        }

        override fun getExtraFieldsVisibility(): Observable<Int> {
            return getSwitchVisibility.observeOn(schedulerProvider.ui()).hide()
        }

        override fun addCardSuccessful(): Observable<Unit> {
            return addCardSuccessful.observeOn(schedulerProvider.ui()).hide()
        }

        fun isValidName(nameOnCard: String): Boolean {
            return nameOnCard.isNotEmpty().and(nameOnCard.matches(NAME_REGEX))
        }

        fun isValidCardNumber(cardNumber: String): Boolean {
            return cardNumber.length in 11..19
        }

        fun isValidIssueNumber(issueNumber: String): Boolean {
            return issueNumber.isEmpty().or(issueNumber.matches(NUMBER_REGEX))
        }

        companion object {
            val NUMBER_REGEX = Regex("[0-9]+")
            val NAME_REGEX = Regex("[a-zA-Z ]+")
            val EXPIRY_DATE_REGEX_ONE = Regex("[0-9]")
        }
    }

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class EnterCardDetailsViewModelSpec : Spek({

    val viewModel by memoized {
        EnterCardDetailsViewModel(mockApplication()).apply {
            mock(this)
            this.expiryDateValidator = Mockito.mock(ExpiryDateValidator::class.java)
        }
    }

    val viewModelSpy = Mockito.spy(viewModel)
    lateinit var boolTestSub: TestObserver<Boolean>
    val subscriberError by memoized { TestObserver.create<Int>() }

    beforeEachTest {
        Mockito.clearInvocations(viewModelSpy)
    }

    describe("isValidPaymentAmount") {

        mapOf(
            "a" to true,
            "" to false,
            "a2" to false,
            "a 2" to false,
            "name surname" to true,
            "name surname." to false,
            "1name surname" to false,
            ",: ][" to false,
            "9999" to false
        ).forEach { map ->
            on("nameOnCard: ${map.key}, isvalid: ${map.value}") {
                it("should validate name on card") {
                    Assert.assertEquals(map.value, viewModelSpy.isValidName(map.key))
                }
                it("should not emit errors") {
                    subscriberError.assertNoValues()
                }
            }
        }
    }

    describe("isValidCardNumber") {

        mapOf(
            "1" to false,
            "" to false,
            "a2" to false,
            "a 2" to false,
            "1111222233334444" to true,
            "11112222333344442222" to false,
            "   22" to false,
            ",: ][" to false,
            "9999" to false
        ).forEach { map ->
            on("cardNumber: ${map.key}, isvalid: ${map.value}") {
                it("should validate card number") {
                    Assert.assertEquals(map.value, viewModelSpy.isValidCardNumber(map.key))
                }
                it("should not emit errors") {
                    subscriberError.assertNoValues()
                }
            }
        }
    }

    describe("isValidCVV") {
        viewModelSpy.setAmount("1.00")

        mapOf(
            "1" to false,
            "" to false,
            "a2" to false,
            "11" to false,
            "111" to true,
            "   22" to false,
            ",: ][" to false,
            "9999" to true
        ).forEach { map ->
            on("cvv: ${map.key}, isvalid: ${map.value}") {
                it("should validate cvv") {
                    Assert.assertEquals(map.value, viewModelSpy.isValidCVV(map.key))
                }
                it("should not emit errors") {
                    subscriberError.assertNoValues()
                }
            }
        }
    }

    describe("isValidIssueNumber") {

        mapOf(
            "1" to true,
            "" to true,
            "a2" to false,
            "11" to true,
            "111" to true,
            "   22" to false,
            ",: ][" to false,
            "9999" to true
        ).forEach { map ->
            on("IssueNumber: ${map.key}, isvalid: ${map.value}") {
                it("should validate IssueNumber") {
                    Assert.assertEquals(map.value, viewModelSpy.isValidIssueNumber(map.key))
                }
                it("should not emit errors") {
                    subscriberError.assertNoValues()
                }
            }
        }
    }


    describe("isValidExpiryDate") {
        mapOf(
            "12/20" to true,
            "12/201" to false,
            "" to false,
            "19/20" to false,
            "/" to false,
            "/22" to false
        ).forEach { map ->
            on("Date: ${map.key}, isvalid: ${map.value}") {
                it("should validate ExpiryDate") {
                    Assert.assertEquals(map.value, viewModelSpy.expiryDateValidator.isValidExpiryDate(map.key))
                }
                it("should not emit errors") {
                    subscriberError.assertNoValues()
                }
            }
        }
    }

    describe("onTextFieldChanged") {
        beforeEachTest {
            Mockito.clearInvocations(viewModelSpy)
            boolTestSub = viewModelSpy.outputs.payButtonEnabled().test()
        }
        mapOf(
            listOf("name", "1111222244445555", "12/20", "111", "asd") to false,
            listOf("name", "1111222244445555", "12/20", "111", "") to true,
            listOf("name1", "1111222244445555", "12/20", "111", "") to false,
            listOf("name", "11112222444455551111", "12/20", "111", "") to false,
            listOf("name", "1111222244445555", "12/201", "111", "") to false,
            listOf("name", "1111222244445555", "12/20", "11", "") to false,
            listOf("", "1111222244445555", "12/20", "111", "") to false,
            listOf("name", "", "12/20", "111", "asd") to false,
            listOf("name", "1111222244445555", "", "111", "asd") to false,
            listOf("name", "1111222244445555", "12/20", "", "asd") to false,
            listOf("", "", "", "", "") to false

        ).forEach { params, isValid ->
            on(
                "name: ${params[0]}, cardNumber: ${params[1]}, expiryDate: ${params[2]}," +
                        " validCvv: ${params[3]}, validIssueNumber: ${params[4]},isValid: $isValid"
            ) {
                viewModelSpy.inputs.onTextFieldChanged(params[0], params[1], params[2], params[3], params[4])

                it("should emit continue button state") {
                    boolTestSub.assertValue(isValid)
                }
            }
        }
    }
})

Basically when i am adding the test case isValidExpiryDate my test are failing on the bitrise and i am not able to build the android build successfully. Please guide me as i am new to android and not able to figure out why it is failing on the bitrise.

0 Answers