ทดสอบการนำทาง Fragment

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

คอมโพเนนต์การนำทางจะจัดการงานทั้งหมดในการจัดการการนำทางระหว่างปลายทาง การส่งอาร์กิวเมนต์ และการทำงานกับ 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")
}

ลองเล่นเกมตอบคำถาม เกมจะเริ่มต้นด้วยหน้าจอชื่อและไปยังหน้าจอในเกมเมื่อผู้ใช้คลิกเล่น

ขั้นตอนการนำทางในเกมทายปัญหา
รูปที่ 1 ขั้นตอนการไปยังส่วนต่างๆ ของเกมทายปัญหา

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())