คุณควรทดสอบตรรกะการนำทางของแอปก่อนที่จะเผยแพร่เพื่อ ยืนยันว่าแอปพลิเคชันทำงานได้ตามที่คาดไว้
คอมโพเนนต์การนำทางจะจัดการงานทั้งหมดในการจัดการการนำทางระหว่างปลายทาง การส่งอาร์กิวเมนต์ และการทำงานกับ FragmentManager ความสามารถเหล่านี้ได้รับการทดสอบอย่างเข้มงวดแล้ว จึงไม่จำเป็นต้องทดสอบอีกครั้งในแอป สิ่งที่สำคัญที่ควรทดสอบคือการโต้ตอบระหว่างโค้ดเฉพาะของแอปใน Fragment กับ NavController
ทดสอบแบบแยก
หากต้องการทดสอบการโต้ตอบของ Fragment กับ NavController แยกกัน
Navigation 2.3 ขึ้นไปมี TestNavHostController ที่มี API สำหรับ
การตั้งค่าปลายทางปัจจุบันและการยืนยัน Back Stack หลังจาก
การดำเนินการ NavController.navigate()
คุณเพิ่มอาร์ติแฟกต์การทดสอบการนำทางลงในโปรเจ็กต์ได้โดยเพิ่มทรัพยากร Dependency ต่อไปนี้ในไฟล์ build.gradle ของโมดูลแอป
Groovy
dependencies { def nav_version = "2.9.8" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.8" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
ลองเล่นเกมตอบคำถาม เกมจะเริ่มต้นด้วยหน้าจอชื่อและไปยังหน้าจอในเกมเมื่อผู้ใช้คลิกเล่น
Fragment ที่แสดง title_screen อาจมีลักษณะดังนี้
Kotlin
class TitleScreen : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_title_screen, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.play_btn).setOnClickListener {
view.findNavController().navigate(R.id.action_title_screen_to_in_game)
}
}
}
Java
public class TitleScreen extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_title_screen, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.play_btn).setOnClickListener(v -> {
Navigation.findNavController(view).navigate(R.id.action_title_screen_to_in_game);
});
}
}
หากต้องการทดสอบว่าแอปนำทางผู้ใช้ไปยังหน้าจอ in_game อย่างถูกต้องเมื่อผู้ใช้คลิกเล่น การทดสอบของคุณต้องยืนยันว่า Fragment นี้ย้าย NavController ไปยังหน้าจอ R.id.in_game อย่างถูกต้อง
การใช้ชุดค่าผสมของ FragmentScenario, Espresso
และ TestNavHostController คุณสามารถสร้างเงื่อนไขที่จำเป็นต่อการทดสอบ
สถานการณ์นี้ได้ ดังตัวอย่างต่อไปนี้
Kotlin
@RunWith(AndroidJUnit4::class)
class TitleScreenTest {
@Test
fun testNavigationToInGameScreen() {
// Create a TestNavHostController
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
// Create a graphical FragmentScenario for the TitleScreen
val titleScenario = launchFragmentInContainer<TitleScreen>()
titleScenario.onFragment { fragment ->
// Set the graph on the TestNavHostController
navController.setGraph(R.navigation.trivia)
// Make the NavController available using the findNavController() APIs
Navigation.setViewNavController(fragment.requireView(), navController)
}
// Verify that performing a click changes the NavController's state
onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)
}
}
Java
@RunWith(AndroidJUnit4::class)
public class TitleScreenTestJava {
@Test
fun testNavigationToInGameScreen() {
// Create a TestNavHostController
TestNavHostController navController = new TestNavHostController(
ApplicationProvider.getApplicationContext());
// Create a graphical FragmentScenario for the TitleScreen
FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);
titleScenario.onFragment(fragment ->
// Set the graph on the TestNavHostController
navController.setGraph(R.navigation.trivia);
// Make the NavController available using the findNavController() APIs
Navigation.setViewNavController(fragment.requireView(), navController)
);
// Verify that performing a click changes the NavController's state
onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click());
assertThat(navController.currentDestination.id).isEqualTo(R.id.in_game);
}
}
ตัวอย่างก่อนหน้าจะสร้างอินสแตนซ์ของ TestNavHostController และกำหนดให้กับ Fragment จากนั้นจะใช้ Espresso เพื่อขับเคลื่อน UI และยืนยันว่ามีการดำเนินการนำทางที่เหมาะสม
เช่นเดียวกับ NavController จริงๆ คุณต้องเรียกใช้ setGraph เพื่อเริ่มต้น TestNavHostController ในตัวอย่างนี้ ส่วนที่กำลังทดสอบคือจุดเริ่มต้นของกราฟ TestNavHostController มีวิธีการ
setCurrentDestination ที่ช่วยให้คุณตั้งค่าปลายทางปัจจุบัน (และอาร์กิวเมนต์สำหรับปลายทางนั้น (ไม่บังคับ)) เพื่อให้ NavController อยู่ในสถานะที่ถูกต้องก่อนที่การทดสอบจะเริ่มขึ้น
NavHostController ซึ่งNavHostFragmentจะใช้
TestNavHostControllerไม่ทริกเกอร์ลักษณะการทำงานของnavigate()พื้นฐาน (เช่น FragmentTransaction ที่FragmentNavigatorทำ) เมื่อคุณเรียกใช้ navigate() แต่จะอัปเดตสถานะของTestNavHostControllerเท่านั้น
ทดสอบ NavigationUI ด้วย FragmentScenario
ในตัวอย่างก่อนหน้า ระบบจะเรียกใช้การเรียกกลับที่ระบุไว้ใน
titleScenario.onFragment() หลังจากที่ Fragment ได้ย้ายผ่านวงจร
การทำงานไปยังสถานะ RESUMED เมื่อถึงเวลานี้ ระบบได้สร้างและแนบมุมมองของ
Fragment แล้ว จึงอาจสายเกินไป
ในวงจรของ Fragment ที่จะทดสอบอย่างถูกต้อง เช่น เมื่อใช้ NavigationUI กับ
มุมมองใน Fragment เช่น กับ Toolbar ที่ควบคุมโดย Fragment
คุณจะเรียกใช้เมธอดการตั้งค่าด้วย NavController ก่อนที่ Fragment
จะไปถึงสถานะ RESUMED ได้ ดังนั้น คุณจึงต้องมีวิธีตั้งค่า
TestNavHostControllerตั้งแต่เนิ่นๆ ในวงจร
คุณเขียน Fragment ที่เป็นเจ้าของ Toolbar ของตัวเองได้ดังนี้
Kotlin
class TitleScreen : Fragment(R.layout.fragment_title_screen) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = view.findNavController()
view.findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController)
}
}
Java
public class TitleScreen extends Fragment {
public TitleScreen() {
super(R.layout.fragment_title_screen);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(view);
view.findViewById(R.id.toolbar).setupWithNavController(navController);
}
}
ในที่นี้ เราต้องมี NavController ที่สร้างขึ้นเมื่อมีการเรียกใช้ onViewCreated()
การใช้แนวทางก่อนหน้าของ onFragment() จะตั้งค่า
TestNavHostController ช้าเกินไปในวงจร ทำให้การเรียกใช้
findNavController() ล้มเหลว
FragmentScenario มีอินเทอร์เฟซ FragmentFactory
ซึ่งใช้เพื่อลงทะเบียนการเรียกกลับสำหรับเหตุการณ์วงจรของแอปได้ ซึ่งสามารถใช้ร่วมกับ Fragment.getViewLifecycleOwnerLiveData() เพื่อรับ
การเรียกกลับที่เกิดขึ้นหลังจาก onCreateView() ทันที ดังที่แสดงในตัวอย่างต่อไปนี้
Kotlin
val scenario = launchFragmentInContainer {
TitleScreen().also { fragment ->
// In addition to returning a new instance of our Fragment,
// get a callback whenever the fragment's view is created
// or destroyed so that we can set the NavController
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
// The fragment's view has just been created
navController.setGraph(R.navigation.trivia)
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
}
}
Java
FragmentScenario<TitleScreen> scenario =
FragmentScenario.launchInContainer(
TitleScreen.class, null, new FragmentFactory() {
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader,
@NonNull String className,
@Nullable Bundle args) {
TitleScreen titleScreen = new TitleScreen();
// In addition to returning a new instance of our fragment,
// get a callback whenever the fragment's view is created
// or destroyed so that we can set the NavController
titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
@Override
public void onChanged(LifecycleOwner viewLifecycleOwner) {
// The fragment's view has just been created
if (viewLifecycleOwner != null) {
navController.setGraph(R.navigation.trivia);
Navigation.setViewNavController(titleScreen.requireView(), navController);
}
}
});
return titleScreen;
}
});
การใช้เทคนิคนี้ทำให้ NavController พร้อมใช้งานก่อนที่จะเรียกใช้ onViewCreated() ซึ่งช่วยให้ Fragment ใช้เมธอด NavigationUI ได้โดยไม่เกิดข้อขัดข้อง
การทดสอบการโต้ตอบกับรายการใน Back Stack
เมื่อโต้ตอบกับรายการใน Back Stack TestNavHostController จะช่วยให้คุณเชื่อมต่อตัวควบคุมกับเทสต์ของตัวเอง LifecycleOwner, ViewModelStore และ OnBackPressedDispatcher ได้โดยใช้ API ที่รับช่วงมาจาก NavHostController
เช่น เมื่อทดสอบ Fragment ที่ใช้ ViewModel ที่กำหนดขอบเขตการนำทาง คุณต้องเรียก setViewModelStore ใน TestNavHostController
Kotlin
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())
Java
TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());
// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())
หัวข้อที่เกี่ยวข้อง
- สร้างการทดสอบ 1 หน่วยแบบมีเครื่องควบคุม - ดูวิธีตั้งค่าชุดทดสอบแบบมีเครื่องควบคุมและเรียกใช้การทดสอบในอุปกรณ์ที่ใช้ Android
- Espresso - ทดสอบ UI ของแอปด้วย Espresso
- กฎ JUnit4 ที่มี AndroidX Test - ใช้กฎ JUnit 4 กับไลบรารี AndroidX Test เพื่อเพิ่มความยืดหยุ่นและลดโค้ด Boilerplate ที่จำเป็นในการทดสอบ
- ทดสอบ Fragment ของแอป - ดูวิธีทดสอบ Fragment ของแอปแยกกันด้วย
FragmentScenario - ตั้งค่าโปรเจ็กต์สำหรับ AndroidX Test - ดูวิธีประกาศไลบรารีที่จำเป็นในไฟล์โปรเจ็กต์ของแอปเพื่อใช้ AndroidX Test