1 of 32

The Journey of Unit Testing in Android Development

Jakarta, Indonesia

admokonugroho.com

2 of 32

Introduction

Jakarta, Indonesia

3 of 32

Detail Transaction

  1. Discount = Fare x percent of discount
  2. Actual Fare = Fare - Discount
  3. Total = Actual Fare + Extra Fee + Tip

Rp 110.705

Rp 11.070

Rp 99.635

Rp 0

Rp 0

Rp 99.635

Rp 99.635

Fare x 10%

Rp 11.070,50

Rp 99.634,50

Rp 99.634,50

4 of 32

  1. What is data type to store prices? Long or double
  2. Both data type have decimal precision issues

The Challenge

5 of 32

Impact on User Experience

6 of 32

Lessons Learned

  1. Importance of through testing, including edge cases, to catch unexpected behavior
  2. Choosing appropriate data types for numerical precision
  3. Strategies for ensuring accuracy in front-end calculations

7 of 32

“A test is manual or automatic procedure used to evaluate if the System Under Test behave correctly

8 of 32

The Test Pyramid

Jakarta, Indonesia

9 of 32

THE TEST PYRAMID

UI TEST

INTEGRATION TEST

UNIT TEST

Slower

Faster

More Integration

More Isolation

10 of 32

UI Test

  1. The test on this layer is to check if the UI works correctly
  2. UI reacts correctly when user input something.
  3. Data shown correctly to the user.
  4. Run in Android Devices

11 of 32

Integration Test

  • Test how the code interacts with each other
  • Code interaction with Android Framework / Third Party
  • Run under JVM / Android Devices

12 of 32

Unit Test

  1. Focus on class-level isolation
  2. Assume class dependencies are already working correctly
  3. Independent from Android framework / third party
  4. Run under JVM

13 of 32

Clean Architecture

domain

data

presenters

UI

App

Repositories

Database

datasource

Network

datasource

14 of 32

Unit Test - Detail Transaction

class DetailTransaction(

val fare : Double,

val discount: Double,

val maxDiscount: Double

) {

fun calculateDiscount() : Int {

return (fare * discount).roundToInt()

}

}

15 of 32

Unit Test - Detail Transaction

@Test

fun whenCalculateDiscount_shouldCalculateAndRoundUpCorrectly() {

// given

val sut = DetailTransaction(

fare = 12145.2,

discount = 0.1

)

// when

val result = sut.calculateDiscount()

// then

Assertions.assertEquals(1215, result)

}

16 of 32

Unit Test - Detail Transaction

@Test

fun whenCalculateDiscount_noDiscount_shouldCalculateAndRoundDownCorrectly() {

// given

val sut = DetailTransaction(

fare = 12142.2,

discount = 0.0

)

// when

val result = sut.calculateDiscount()

// then

Assertions.assertEquals(12142, result)

}

Caption

17 of 32

Unit Test - Detail Transaction

class DetailTransaction(

val fare : Double,

val discount: Double,

val maxDiscount: Double

) {

fun calculateDiscount() : Int {

return (fare * discount).roundToInt()

}

}

class DetailTransaction(

val fare : Double,

val discount: Double,

val maxDiscount: Double? = null

) {

fun calculateDiscount() : Int {

val result = if (discount > 0) {

(fare * discount)

} else fare

return result.roundToInt()

}

}

18 of 32

What if the fare Rp (any) and discount 20% and max discount 15.000?

19 of 32

Refactor

Update Your Code

Pick a requirement

Write a test to meet

the requirement

Run Test

Test

Succeed?

Modify

/ Refactor Code

No

Yes

20 of 32

Mock

Jakarta, Indonesia

Mockk.io & Junit5

21 of 32

Mock - Detail Transaction

class TransactionRepository @Inject constructor(

val factory: TransactionEntityDataFactory

) {

fun getDetailTransaction(

orderId: String

) : Single<DetailTransaction> {

return factory.createData(Source.NETWORK)

.getDetailTransaction(orderId)

}

}

22 of 32

Mock - Detail Transaction

class TransactionRepositoryTest {

private val factory =

mockk<TransactionEntityDataFactory>()

private val network =

mockk<TransactionEntityData>()

private val local =

mockk<TransactionEntityData>()

private val repository =

TransactionRepository(factory)

@BeforeEach

fun setup() {

clearMocks(factory, local, network)

every {

factory.createData(Source.LOCAL)

} returns local

every {

factory.createData(Source.NETWORK)

} returns network

}

23 of 32

Mock - Detail Transaction

@Test

fun whenGetDetailTransaction_shouldReturnData() {

//Given

val orderId = "order_id_00"

val result = DetailTransaction(

fare = 20000.0,

discount = 0.0

)

every {

network.getDetailTransaction(orderId)

} returns Single.just(

result

)

//When

repository.getDetailTransaction(orderId)

.test()

.apply {

this.assertValueAt(0, result)

}

.dispose()

//Then

verify {

factory.createData(Source.NETWORK)

network.getDetailTransaction(orderId)

}

verify {

local wasNot called

}

}

24 of 32

Continuous Integration

Jakarta, Indonesia

25 of 32

26 of 32

27 of 32

Pull Request

developer

UI / Integration / Unit Test

auto

Pass Quality Gate?

no

auto

Continuous Integration

Merge

Continuous Delivery

Deploy To Test

Acceptance Testing

Continuous Deployment

Deploy / Promote to Production

28 of 32

Conclusion

Jakarta, Indonesia

29 of 32

Conclusion

  1. Comprehensive Testing: Including unit testing, to detect unforeseen issues like precision errors early in the development cycle
  2. Automation for Efficiency: Reducing manual errors, and speeding up the testing process to catch issues before they reach users.
  3. CI/CD Impact: Continuous integration and deployment, encouraging developers to implement and maintain unit tests and ensuring minimum code coverage standards.

30 of 32

Reference

  1. Test-Driven Development for Android, Cassandra Shum​
  2. Android Test Driven Development by Tutorial 2nd Editor 2021,​ Lance Glaeson, Victoria Gonda & Fernando Sprovero​

31 of 32

32 of 32