পরীক্ষা খণ্ড নেভিগেশন

আপনার অ্যাপ্লিকেশনটি প্রত্যাশা অনুযায়ী কাজ করছে কিনা, তা যাচাই করার জন্য অ্যাপটি প্রকাশ করার আগে এর নেভিগেশন লজিক পরীক্ষা করা জরুরি।

নেভিগেশন কম্পোনেন্টটি বিভিন্ন গন্তব্যের মধ্যে নেভিগেশন পরিচালনা, আর্গুমেন্ট পাস করা এবং FragmentManager এর সাথে কাজ করার সমস্ত দায়িত্ব পালন করে। এই ক্ষমতাগুলো ইতিমধ্যেই কঠোরভাবে পরীক্ষা করা হয়েছে, তাই আপনার অ্যাপে এগুলো আবার পরীক্ষা করার কোনো প্রয়োজন নেই। তবে, যা পরীক্ষা করা গুরুত্বপূর্ণ তা হলো আপনার ফ্র্যাগমেন্টগুলোর অ্যাপ-নির্দিষ্ট কোড এবং তাদের NavController মধ্যকার মিথস্ক্রিয়া।

বিচ্ছিন্নভাবে পরীক্ষা

তাদের NavController সাথে ফ্র্যাগমেন্টের মিথস্ক্রিয়াকে বিচ্ছিন্নভাবে পরীক্ষা করার জন্য, Navigation 2.3 এবং এর পরবর্তী সংস্করণগুলোতে একটি TestNavHostController রয়েছে যা NavController.navigate() অপারেশনের পরে বর্তমান গন্তব্য নির্ধারণ এবং ব্যাক স্ট্যাক যাচাই করার জন্য API প্রদান করে।

আপনার অ্যাপ মডিউলের build.gradle ফাইলে নিম্নলিখিত ডিপেন্ডেন্সিটি যোগ করে আপনি আপনার প্রজেক্টে নেভিগেশন টেস্টিং আর্টিফ্যাক্টটি যুক্ত করতে পারেন:

গ্রুভি

dependencies {
  def nav_version = "2.9.8"

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

কোটলিন

dependencies {
  val nav_version = "2.9.8"

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

একটি কুইজ গেমের কথা ভাবুন। গেমটি একটি টাইটেল স্ক্রিন দিয়ে শুরু হয় এবং ব্যবহারকারী 'প্লে' বোতামে ক্লিক করলে একটি ইন-গেম স্ক্রিনে চলে যায়।

ট্রিভিয়া গেম নেভিগেশন প্রবাহ
চিত্র ১. ট্রিভিয়া গেমের নেভিগেশন প্রবাহ।

টাইটেল_স্ক্রিন প্রতিনিধিত্বকারী ফ্র্যাগমেন্টটি দেখতে অনেকটা এইরকম হতে পারে:

কোটলিন

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

জাভা

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);
        });
    }
}

ব্যবহারকারী 'Play' বোতামে ক্লিক করলে অ্যাপটি তাকে সঠিকভাবে in_game স্ক্রিনে নিয়ে যায় কিনা, তা পরীক্ষা করার জন্য আপনার টেস্টকে যাচাই করতে হবে যে এই ফ্র্যাগমেন্টটি NavController কে সঠিকভাবে R.id.in_game স্ক্রিনে নিয়ে যাচ্ছে কিনা।

FragmentScenario , Espresso , এবং TestNavHostController এর সমন্বিত ব্যবহারের মাধ্যমে, আপনি এই সিনারিওটি ​​পরীক্ষা করার জন্য প্রয়োজনীয় শর্তগুলো পুনরায় তৈরি করতে পারেন, যেমনটি নিম্নলিখিত উদাহরণে দেখানো হয়েছে:

কোটলিন

@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)
    }
}

জাভা

@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 এর একটি ইনস্ট্যান্স তৈরি করে এবং সেটিকে ফ্র্যাগমেন্টে অ্যাসাইন করে। এরপর এটি UI চালনা করার জন্য Espresso ব্যবহার করে এবং যথাযথ নেভিগেশন অ্যাকশন নেওয়া হয়েছে কিনা তা যাচাই করে।

একটি আসল NavController মতোই, TestNavHostController কে ইনিশিয়ালাইজ করার জন্য আপনাকে অবশ্যই setGraph কল করতে হবে। এই উদাহরণে, যে ফ্র্যাগমেন্টটি পরীক্ষা করা হচ্ছিল, সেটিই ছিল আমাদের গ্রাফের শুরুর গন্তব্য। TestNavHostController একটি setCurrentDestination মেথড প্রদান করে, যা আপনাকে বর্তমান গন্তব্য (এবং ঐচ্ছিকভাবে, সেই গন্তব্যের জন্য আর্গুমেন্ট) সেট করতে দেয়, যাতে আপনার পরীক্ষা শুরু হওয়ার আগে NavController সঠিক অবস্থায় থাকে।

NavHostController যে NavHostFragment ইনস্ট্যান্স ব্যবহার করে, তার থেকে ভিন্নভাবে, আপনি যখন navigate() কল করেন তখন TestNavHostController অন্তর্নিহিত navigate() আচরণ (যেমন FragmentNavigator এর FragmentTransaction ) ট্রিগার করে না — এটি শুধুমাত্র TestNavHostController এর স্টেট আপডেট করে।

FragmentScenario দিয়ে NavigationUI পরীক্ষা করুন

পূর্ববর্তী উদাহরণে, titleScenario.onFragment() এ প্রদত্ত কলব্যাকটি ফ্র্যাগমেন্টটি তার লাইফসাইকেল অতিক্রম করে RESUMED অবস্থায় পৌঁছানোর পরে কল করা হয়। এই সময়ের মধ্যে, ফ্র্যাগমেন্টের ভিউটি ইতিমধ্যেই তৈরি এবং সংযুক্ত হয়ে গেছে, তাই সঠিকভাবে পরীক্ষা করার জন্য লাইফসাইকেলে অনেক দেরি হয়ে যেতে পারে। উদাহরণস্বরূপ, আপনার ফ্র্যাগমেন্টের ভিউগুলির সাথে NavigationUI ব্যবহার করার সময়, যেমন আপনার ফ্র্যাগমেন্ট দ্বারা নিয়ন্ত্রিত একটি Toolbar ক্ষেত্রে, আপনি ফ্র্যাগমেন্টটি RESUMED অবস্থায় পৌঁছানোর আগেই আপনার NavController সেটআপ মেথডগুলো কল করতে পারেন। সুতরাং, লাইফসাইকেলের আরও আগে আপনার TestNavHostController সেট করার একটি উপায় প্রয়োজন।

যে ফ্র্যাগমেন্টের নিজস্ব Toolbar আছে, তা নিম্নোক্তভাবে লেখা যেতে পারে:

কোটলিন

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

জাভা

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);
    }
}

এখানে আমাদের প্রয়োজন onViewCreated() কল হওয়ার আগেই NavController টি তৈরি হয়ে যাওয়া। আগের পদ্ধতি অর্থাৎ onFragment() ব্যবহার করলে আমাদের TestNavHostController লাইফসাইকেলে অনেক দেরিতে সেট হতো, যার ফলে findNavController() কলটি ব্যর্থ হতো।

FragmentScenario একটি FragmentFactory ইন্টারফেস প্রদান করে যা লাইফসাইকেল ইভেন্টের জন্য কলব্যাক রেজিস্টার করতে ব্যবহার করা যেতে পারে। এটিকে Fragment.getViewLifecycleOwnerLiveData() এর সাথে একত্রিত করে onCreateView() এর ঠিক পরেই একটি কলব্যাক পাওয়া যায়, যেমনটি নিম্নলিখিত উদাহরণে দেখানো হয়েছে:

কোটলিন

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

জাভা

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;
    }
});

এই কৌশলটি ব্যবহার করার ফলে, onViewCreated() কল হওয়ার আগেই NavController উপলব্ধ থাকে, যার ফলে ফ্র্যাগমেন্টটি ক্র্যাশ না করেই NavigationUI মেথডগুলো ব্যবহার করতে পারে।

ব্যাক স্ট্যাক এন্ট্রিগুলির সাথে মিথস্ক্রিয়া পরীক্ষা করা হচ্ছে

ব্যাক স্ট্যাক এন্ট্রিগুলির সাথে ইন্টারঅ্যাক্ট করার সময়, TestNavHostController আপনাকে NavHostController থেকে উত্তরাধিকারসূত্রে প্রাপ্ত API গুলি ব্যবহার করে কন্ট্রোলারটিকে আপনার নিজস্ব টেস্ট LifecycleOwner , ViewModelStore , এবং OnBackPressedDispatcher সাথে সংযুক্ত করার সুযোগ দেয়।

উদাহরণস্বরূপ, নেভিগেশন স্কোপড ViewModel ব্যবহার করে এমন কোনো ফ্র্যাগমেন্ট পরীক্ষা করার সময়, আপনাকে অবশ্যই TestNavHostControllersetViewModelStore কল করতে হবে:

কোটলিন

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

জাভা

TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())