Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เชี่ยวชาญการทดสอบ Proto DataStore: คู่มือปฏิบัติจริง

ในบทความก่อนหน้า ผมอธิบายวิธีใช้ Proto DataStore ในแอปพลิเคชันของคุณ ผมเขียนบทความนั้นเป็นส่วนหนึ่งของประสบการณ์ในการใช้ Proto DataStore ในหนึ่งในแอปของผม

หลังจากนั้น ผมอยากดูว่าการเขียนทดสอบสำหรับ Proto DataStore ในแอปนั้นจะเป็นอย่างไร โดยใช้ความรู้ที่ผมได้รับ

การค้นหาออนไลน์เพื่อคำแนะนำไม่ได้ช่วยอะไรมากนัก ดังนั้นผมจึงคิดว่าจะแบ่งปันความรู้สำหรับผู้ที่อาจกำลังมองหา ในกรณีแย่ที่สุด จะเป็นสำหรับลูกหลานของผม

ในการค้นหา ผมพบบทความนี้ แต่เน้นส่วนใหญ่ที่การทดสอบ Preferences DataStore ไม่ใช่ Proto DataStore มันระบุว่า:

“อย่างไรก็ตาม จงจำไว้ว่าคุณสามารถใช้วัสดุนี้สำหรับการตั้งค่า Proto DataStore testing ได้ เนื่องจากมันจะคล้าย Preferences มาก”

แต่หลังจากทำตาม ผมพบว่า นอกจาก dependencies แล้ว ไม่มีความคล้ายคลึงกันมากนัก และคุณต้องนำตรรกะแยกต่างหากมาใช้เพื่อทดสอบ Proto DataStore ของคุณเอง

การตั้งค่า (Setting Things Up)

ในไฟล์ build.gradle ระดับแอปพลิเคชันของคุณ เพิ่ม dependencies ต่อไปนี้:

dependencies {
  ///.....
  androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
  debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
}

$compose_version คือตัวแปรที่คุณกำหนดในไฟล์ build.gradle ระดับโปรเจกต์

จากนั้น ไปที่ directory androidTest และสร้างไฟล์ใหม่ ปกติคุณจะมี repository class ที่โต้ตอบกับ Proto DataStore ของคุณ ดังนั้นคุณสามารถตั้งชื่อไฟล์นี้ว่า YourRepositoryClassNameTest เราจะใช้ชื่อ MyRepositoryTest

ก่อนที่เราจะเจาะลึกการทดสอบ Proto DataStore เอง เราต้อง instantiate มัน หากคุณไปหาเอกสารออนไลน์เกี่ยวกับเรื่องนี้ มันค่อนข้างน้อย

การ instantiate Proto DataStore ใช้กับ global Context แบบนี้ (เมื่อไม่ได้ใช้ในสถานการณ์ทดสอบ):

private val Context.myDataStore: DataStore<MyItem> by dataStore(
  fileName = DATA_STORE_FILE_NAME,
  serializer = MyItemSerializer
)

حسناً คุณไม่สามารถทำได้ภายใน test class เพราะ แม้คุณจะ copy-paste โค้ดข้างบน คุณจะไม่สามารถ เข้าถึง DataStore object ได้ คุณสามารถได้ application context แบบนี้:

ApplicationProvider.getApplicationContext()

แต่ object myDataStore ของเราจะไม่พร้อมใช้งานผ่านมัน

那我们能做什么?

ในบทความที่ลิงก์ข้างบน มีตัวอย่างว่าสร้าง Preference DataStore ได้อย่างไรด้วยเมธอด PreferenceDataStoreFactory.create

fun create(
  corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
  migrations: List<DataMigration<Preferences>> = listOf(),
  scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
  produceFile: () -> File): DataStore<Preferences>

แต่เพราะเราไม่ได้ใช้ Preference DataStore นั่นเลยไม่ได้ใช้กับเรา สิ่งที่จะทำงานคือการใช้เมธอด DataStoreFactory.create แบบนี้:

fun <T : Any?> create(
  serializer: Serializer<T>,
  corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
  migrations: List<DataMigration<T>> = listOf(),
  scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
  produceFile: () -> File): DataStore<T>

มีอาร์กิวเมนต์หลายตัวสำหรับเมธอดนี้ (และบางตัวมีค่าเริ่มต้น) แต่เราไม่ต้องส่งทั้งหมด เราจะส่ง:

  • Serializer class ของเรา
  • Lambda method สำหรับสร้างไฟล์สำหรับ Proto DataStore ของเรา
dataStore = DataStoreFactory.create(
  produceFile = { testContext.dataStoreFile(TEST_DATA_STORE_FILE_NAME) },
  serializer = MyItemSerializer
)

เราได้ testContext โดย:

private val testContext: Context = ApplicationProvider.getApplicationContext()

หลังจากสร้าง Proto DataStore สำเร็จ เราสามารถเขียนทดสอบสำหรับมันได้ จำไว้ว่าคุณมี repository class ที่รับ instance ของ Proto DataStore เป็น dependency ดังนั้นหลังจากสร้าง Proto DataStore เราต้องสร้าง instance ของ repository class ของเรา

private val repository = MyRepository(datastore)

ก่อนอื่น มาสร้างทดสอบเพื่อตรวจสอบสถานะเริ่มต้นของ Proto DataStore Proto DataStore เอง expose flow ที่เราสามารถใช้ได้

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun repository_testFetchInitialState() {
  runTest {
    testScope.launch {
      val dataStoreObject = repository.myFlow.first()
      // ใส่สิ่งที่เราต้องการ assert จาก Proto DataStore ของเรา
      // เช่น flag ที่ค่าเริ่มต้นเป็น false
      assert(dataStoreObject.myFlag == false)
    }
  }
}

☝️ คุณอาจสังเกตเห็นก่อนหน้านี้ว่าเราใช้ OptIn annotation ที่นี่ เพราะ (ปัจจุบัน) APIs ที่เราใช้เป็น experimental และต้องทำเครื่องหมายเมื่อเราใช้

เนื่องจากเราเข้าถึง flow ของ DataStore เราต้องห่อมันใน testScope TestScope สร้างโดยการทำ:

@OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = TestCoroutineDispatcher()
@OptIn(ExperimentalCoroutinesApi::class)
private val testScope = TestCoroutineScope(dispatcher)

รันมันและเพลิดเพลินกับการทดสอบ Proto DataStore ครั้งแรกของคุณ

นี่สนุกประมาณ 2 วินาที

ลองทำอะไรที่มีความหมายมากกว่านี้

ลองจินตนาการว่า Proto DataStore ของเรามีรายการของ objects ภายใน และเราอยากทดสอบสถานะของมันเมื่อเราเพิ่มไอเท็มเข้าไป

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun repository_testAdditionOfItem() {
  runTest {
    testScope.launch {
      // 1
      val item: MyItem = MyItem.newBuilder().setItemId(UUID.randomUUID().toString())
        .setItemDescription(TEST_ITEM_DESCRIPTION).build()
      // 2
      repository.updateItem(item)
      // 3
      val items = repository.myFlow.first().itemsList
      assert(items.size == 1)
      // 4
      assert(items[0].itemDescription.equals(TEST_ITEM_DESCRIPTION))
    }
  }
}
  1. เราสร้าง test item โดยใช้ API ที่ expose จาก protobuff
  2. เราเพิ่มไอเท็มนี้เข้า Proto DataStore โดยใช้เมธอดที่เรา expose บน class MyRepository
  3. เราดึงรายการไอเท็มจาก flow ที่ Proto DataStore expose
  4. เราตรวจสอบว่าไอเท็มที่พบใน Proto DataStore ตรงกับไอเท็มที่เราสร้างก่อนหน้านี้

DataStore ของคุณมีรั่ว (Your DataStore Has a Leak in it)

หากคุณพยายามรันทดสอบข้างบนในครั้งเดียว คุณจะได้รับข้อผิดพลาดระหว่าง runtime ไปเร็วๆ นี้:

There are multiple DataStores active for the same file: /data/user/0/com.example.app/files/datastore/dataStore_filename.pb. You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).

นั่นเป็นปัญหา เราสร้าง DataStore instance เพียงหนึ่งใน test class ของเรา

เกิดอะไรขึ้น?

เพราะเราไม่ได้ใช้ property delegate เพื่อสร้าง DataStore (หมายถึง Context.datastore) จึงไม่ได้รับการรับรองว่า DataStore object ของเราเป็น singleton ทุกครั้งที่เราเข้าถึงมัน

เพื่อหลีกเลี่ยงสถานการณ์นี้ ผมพบว่าวิธีหนึ่งคือ ลบและสร้าง DataStore ใหม่สำหรับแต่ละ test case ในการลบ DataStore เราสามารถทำได้ดังนี้:

@After
fun cleanup() {
  File(testContext.filesDir, "datastore").deleteRecursively()
}

และก่อนทุก test เราสร้างใหม่:

@Before
fun setup() {
  dataStore = DataStoreFactory.create(
    produceFile = { testContext.dataStoreFile(TEST_DATA_STORE_FILE_NAME) },
    serializer = MyItemSerializer
  )
}

เพื่อดูตัวอย่างเต็ม คุณสามารถไปที่นี่

ในบทความนี้ ผมอยากแสดงโครงร่างว่าสามารถทดสอบ Proto DataStore ได้อย่างไร

แม้ผมจะผ่าน 2 test cases แล้ว แต่ขึ้นอยู่กับ DataStore และ types ที่คุณกำหนดไว้ อาจมี test cases และ scenarios มากกว่านี้ที่ต้องเขียน building blocks อยู่ที่นี่แล้ว คุณแค่ต้องปรับให้เหมาะกับความต้องการของคุณ

เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพนซอร์สของ freeCodeCamp ช่วยให้ผู้คนมากกว่า 40,000 คนได้งานเป็นนักพัฒนา เริ่มต้นเลยวันนี้