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

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

คอมโพเนนต์การนำทางจะจัดการงานทั้งหมดในการจัดการการนำทางระหว่าง ปลายทาง การส่งอาร์กิวเมนต์ และการทำงานกับ FragmentManager ความสามารถเหล่านี้ได้รับการทดสอบที่เข้มงวดแล้ว คุณจึงไม่จำเป็นต้องทดสอบ อีกครั้งในแอปของคุณ แต่สิ่งสำคัญในการทดสอบคือการโต้ตอบ ระหว่างโค้ดเฉพาะแอปใน Fragment ของคุณและของ NavController คู่มือนี้จะอธิบายสถานการณ์การนําทางที่พบบ่อยบางส่วนและวิธีทดสอบการใช้งาน

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

หากต้องการทดสอบการโต้ตอบส่วนย่อยด้วย NavController แบบแยกเดี่ยว การนำทาง 2.3 ขึ้นไปมอบ TestNavHostController ที่มี API สำหรับการตั้งค่าปลายทางปัจจุบันและยืนยันด้านหลัง ซ้อนหลัง NavController.navigate()

คุณสามารถเพิ่มอาร์ติแฟกต์การทดสอบการนำทางลงในโครงการของคุณได้โดยการเพิ่ม ทรัพยากร Dependency ต่อไปนี้ในไฟล์ build.gradle ของโมดูลแอป

Groovy

dependencies {
  def nav_version = "2.8.4"

  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies {
  val nav_version = "2.8.4"

  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

สมมติว่าคุณกำลังสร้างเกมทายปัญหา เกมจะเริ่มต้นด้วย title_screen และไปยังหน้าจอ in_game เมื่อผู้ใช้คลิก ก็เล่นได้

ส่วนย่อยที่แสดง 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 ได้อย่างถูกต้องเมื่อ ผู้ใช้คลิกเล่น การทดสอบต้องยืนยันว่าส่วนนี้ ย้าย 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 via 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
    public void 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 via 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 และกำหนดอินสแตนซ์ ลงในส่วนย่อย จากนั้นใช้ Espresso ในการขับเคลื่อน UI และยืนยันว่า การนำทางที่เหมาะสม

เช่นเดียวกับ NavController จริง คุณต้องเรียกใช้ setGraph เพื่อเริ่มต้น TestNavHostController ในตัวอย่างนี้ ส่วนย่อยที่ทดสอบคือ ปลายทางเริ่มต้นของกราฟ TestNavHostController มอบ setCurrentDestination ที่ช่วยให้คุณตั้งค่าปลายทางปัจจุบัน (และ อาร์กิวเมนต์สำหรับปลายทางนั้น) เพื่อให้ NavController อยู่ใน สถานะที่ถูกต้องก่อนที่การทดสอบจะเริ่มต้น

ซึ่งแตกต่างจากอินสแตนซ์ NavHostController ที่ NavHostFragment จะใช้ TestNavHostController จะไม่ทริกเกอร์ navigate() ที่เกี่ยวข้อง พฤติกรรม (เช่น FragmentTransaction ที่ FragmentNavigator ทำ) เมื่อคุณเรียกใช้ navigate() ระบบจะอัปเดตสถานะของ TestNavHostController

ทดสอบ NavigationUI ด้วย Fragmentสถานการณ์

ในตัวอย่างก่อนหน้านี้ Callback ที่ให้ไว้กับ titleScenario.onFragment() จะมีการเรียกหลังจากที่ส่วนย่อยได้เคลื่อนที่ผ่านวงจรไปยัง RESUMED ถึงตอนนี้ ระบบได้สร้างมุมมองของส่วนย่อยและแนบไว้แล้ว จึงอาจสายเกินไปที่จะทดสอบอย่างถูกต้อง เช่น เมื่อใช้ NavigationUI ที่มีการดูในส่วนย่อยของคุณ เช่น ด้วย Toolbar ที่ควบคุม ตามส่วนย่อยของคุณ คุณสามารถเรียกใช้วิธีการตั้งค่าด้วย NavController ได้ก่อนวันที่ ส่วนย่อยถึงสถานะ RESUMED คุณจึงต้องหาวิธีตั้งค่า เร็วขึ้น TestNavHostController ในวงจร

ส่วนย่อยที่เป็นเจ้าของ 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 อินเทอร์เฟซที่ใช้ลงทะเบียน Callback สำหรับเหตุการณ์ในวงจรได้ วิธีนี้ รวมกับ Fragment.getViewLifecycleOwnerLiveData() เพื่อรับ Callback ที่ตามหลัง 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() ทำให้ส่วนย่อยสามารถใช้เมธอด NavigationUI ได้ โดยไม่ขัดข้อง

การทดสอบการโต้ตอบกับรายการ Back Stack

เมื่อโต้ตอบกับรายการแบ็กสแต็ก TestNavHostControllerช่วยให้คุณเชื่อมต่อตัวควบคุมเข้ากับ ทดสอบ LifecycleOwner, ViewModelStore และ OnBackPressedDispatcher โดย โดยใช้ API ที่รับค่าเดิมมา NavHostController

ตัวอย่างเช่น เมื่อทดสอบส่วนย่อยที่ใช้องค์ประกอบ 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())