টেস্ট ডাবলস এবং ডিপেনডেন্সি ইনজেকশনের ভূমিকা

এই কোডল্যাবটি অ্যাডভান্সড অ্যান্ড্রয়েড ইন কোটলিন কোর্সের অংশ। আপনি যদি কোডল্যাবগুলি ক্রমানুসারে কাজ করেন তবে আপনি এই কোর্সের সর্বাধিক মূল্য পাবেন, তবে এটি বাধ্যতামূলক নয়৷ সমস্ত কোর্স কোডল্যাবগুলি কোটলিন কোডল্যাবস ল্যান্ডিং পৃষ্ঠায় অ্যাডভান্সড অ্যান্ড্রয়েডে তালিকাভুক্ত করা হয়েছে।

ভূমিকা

এই দ্বিতীয় টেস্টিং কোডল্যাবটি পরীক্ষার দ্বিগুণ সম্পর্কে: কখন এগুলিকে অ্যান্ড্রয়েডে ব্যবহার করতে হবে এবং নির্ভরতা ইনজেকশন, পরিষেবা লোকেটার প্যাটার্ন এবং লাইব্রেরিগুলি ব্যবহার করে কীভাবে সেগুলি প্রয়োগ করতে হবে৷ এটি করার সময়, আপনি কীভাবে লিখতে হয় তা শিখবেন:

  • সংগ্রহস্থল ইউনিট পরীক্ষা
  • টুকরো এবং ভিউ মডেল ইন্টিগ্রেশন পরীক্ষা
  • খণ্ড নেভিগেশন পরীক্ষা

আপনি ইতিমধ্যে কি জানা উচিত

আপনার সাথে পরিচিত হওয়া উচিত:

  • কোটলিন প্রোগ্রামিং ভাষা
  • প্রথম কোডল্যাবে কভার করা পরীক্ষার ধারণাগুলি: অ্যান্ড্রয়েডে ইউনিট পরীক্ষা লেখা এবং চালানো, JUnit, Hamcrest, AndroidX পরীক্ষা, Robolectric, পাশাপাশি LiveData পরীক্ষা করা
  • নিম্নলিখিত মূল অ্যান্ড্রয়েড জেটপ্যাক লাইব্রেরি: ViewModel , LiveData এবং নেভিগেশন উপাদান
  • অ্যাপ্লিকেশান আর্কিটেকচার, অ্যাপ আর্কিটেকচার এবং অ্যান্ড্রয়েড ফান্ডামেন্টাল কোডল্যাবগুলির গাইড থেকে প্যাটার্ন অনুসরণ করে
  • অ্যান্ড্রয়েডে কোরোটিনের মূল বিষয়

আপনি কি শিখবেন

  • কিভাবে একটি পরীক্ষার কৌশল পরিকল্পনা
  • কিভাবে টেস্ট ডাবল তৈরি এবং ব্যবহার করতে হয়, যেমন নকল এবং উপহাস
  • ইউনিট এবং ইন্টিগ্রেশন পরীক্ষার জন্য অ্যান্ড্রয়েডে ম্যানুয়াল নির্ভরতা ইনজেকশন কীভাবে ব্যবহার করবেন
  • কিভাবে সার্ভিস লোকেটার প্যাটার্ন প্রয়োগ করবেন
  • কিভাবে সংগ্রহস্থল, টুকরা, দেখুন মডেল এবং নেভিগেশন উপাদান পরীক্ষা করতে হয়

আপনি নিম্নলিখিত লাইব্রেরি এবং কোড ধারণা ব্যবহার করবেন:

আপনি কি করবেন

  • একটি পরীক্ষা ডবল এবং নির্ভরতা ইনজেকশন ব্যবহার করে একটি সংগ্রহস্থলের জন্য ইউনিট পরীক্ষা লিখুন।
  • একটি পরীক্ষা ডবল এবং নির্ভরতা ইনজেকশন ব্যবহার করে একটি ভিউ মডেলের জন্য ইউনিট পরীক্ষা লিখুন।
  • Espresso UI টেস্টিং ফ্রেমওয়ার্ক ব্যবহার করে টুকরো এবং তাদের ভিউ মডেলগুলির জন্য ইন্টিগ্রেশন পরীক্ষা লিখুন।
  • Mockito এবং Espresso ব্যবহার করে নেভিগেশন পরীক্ষা লিখুন।

কোডল্যাবগুলির এই সিরিজে, আপনি TO-DO Notes অ্যাপের সাথে কাজ করবেন৷ অ্যাপটি আপনাকে কাজগুলি সম্পূর্ণ করার জন্য লিখতে দেয় এবং সেগুলি একটি তালিকায় প্রদর্শন করে। তারপরে আপনি সেগুলিকে সম্পূর্ণ বা না হিসাবে চিহ্নিত করতে পারেন, সেগুলি ফিল্টার করতে পারেন বা মুছতে পারেন৷

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

কোডটি ডাউনলোড করুন

শুরু করতে, কোড ডাউনলোড করুন:

জিপ ডাউনলোড করুন

বিকল্পভাবে, আপনি কোডের জন্য Github সংগ্রহস্থল ক্লোন করতে পারেন:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

নীচের নির্দেশাবলী অনুসরণ করে কোডের সাথে নিজেকে পরিচিত করতে কিছুক্ষণ সময় নিন।

ধাপ 1: নমুনা অ্যাপ চালান

একবার আপনি TO-DO অ্যাপটি ডাউনলোড করলে, এটিকে অ্যান্ড্রয়েড স্টুডিওতে খুলুন এবং চালান। এটা কম্পাইল করা উচিত. নিম্নলিখিতগুলি করে অ্যাপটি অন্বেষণ করুন:

  • প্লাস ফ্লোটিং অ্যাকশন বোতাম দিয়ে একটি নতুন টাস্ক তৈরি করুন। প্রথমে একটি শিরোনাম লিখুন, তারপর টাস্ক সম্পর্কে অতিরিক্ত তথ্য লিখুন। সবুজ চেক FAB দিয়ে এটি সংরক্ষণ করুন।
  • কাজের তালিকায়, আপনি যে কাজটি সম্পূর্ণ করেছেন তার শিরোনামে ক্লিক করুন এবং বাকি বিবরণ দেখতে সেই কাজের জন্য বিস্তারিত স্ক্রীনটি দেখুন।
  • তালিকায় বা বিস্তারিত স্ক্রিনে, সেই টাস্কের চেকবক্সে চেক করুন যাতে সেটির স্থিতি সম্পূর্ণ হয়।
  • টাস্ক স্ক্রিনে ফিরে যান, ফিল্টার মেনু খুলুন এবং সক্রিয় এবং সম্পূর্ণ স্থিতি দ্বারা কাজগুলি ফিল্টার করুন।
  • নেভিগেশন ড্রয়ার খুলুন এবং পরিসংখ্যান ক্লিক করুন।
  • ওভারভিউ স্ক্রিনে ফিরে আসি, এবং নেভিগেশন ড্রয়ার মেনু থেকে, সম্পূর্ণ স্থিতি সহ সমস্ত কাজ মুছে ফেলতে সাফ সম্পন্ন নির্বাচন করুন

ধাপ 2: নমুনা অ্যাপ কোড অন্বেষণ করুন

TO-DO অ্যাপটি জনপ্রিয় আর্কিটেকচার ব্লুপ্রিন্ট টেস্টিং এবং আর্কিটেকচার নমুনা (নমুনার প্রতিক্রিয়াশীল আর্কিটেকচার সংস্করণ ব্যবহার করে) এর উপর ভিত্তি করে তৈরি। অ্যাপটি একটি গাইড থেকে অ্যাপ আর্কিটেকচারের আর্কিটেকচার অনুসরণ করে। এটি ফ্র্যাগমেন্টস, একটি সংগ্রহস্থল এবং রুম সহ ভিউ মডেল ব্যবহার করে। আপনি যদি নীচের যেকোন উদাহরণের সাথে পরিচিত হন তবে এই অ্যাপটির একটি অনুরূপ আর্কিটেকচার রয়েছে:

যেকোন একটি স্তরে যুক্তির গভীর বোঝার চেয়ে অ্যাপটির সাধারণ আর্কিটেকচার বোঝার চেয়ে এটি আরও গুরুত্বপূর্ণ।

এখানে আপনি যে প্যাকেজগুলি পাবেন তার সারাংশ:

প্যাকেজ: com.example.android.architecture.blueprints.todoapp

.addedittask

একটি টাস্ক স্ক্রীন যোগ বা সম্পাদনা করুন: একটি টাস্ক যোগ বা সম্পাদনা করার জন্য UI স্তর কোড।

.data

ডাটা লেয়ার: এটি কাজের ডাটা লেয়ার নিয়ে কাজ করে। এতে ডাটাবেস, নেটওয়ার্ক এবং রিপোজিটরি কোড রয়েছে।

.statistics

পরিসংখ্যান স্ক্রীন: পরিসংখ্যান পর্দার জন্য UI স্তর কোড।

.taskdetail

টাস্ক ডিটেইল স্ক্রিন: একটি টাস্কের জন্য UI লেয়ার কোড।

.tasks

টাস্ক স্ক্রিন: সমস্ত কাজের তালিকার জন্য UI লেয়ার কোড।

.util

ইউটিলিটি ক্লাস: অ্যাপের বিভিন্ন অংশে ব্যবহৃত শেয়ার্ড ক্লাস, যেমন একাধিক স্ক্রিনে ব্যবহৃত সোয়াইপ রিফ্রেশ লেআউটের জন্য।

ডেটা স্তর (.ডেটা)

এই অ্যাপটিতে একটি সিমুলেটেড নেটওয়ার্কিং স্তর রয়েছে, দূরবর্তী প্যাকেজে এবং স্থানীয় প্যাকেজে একটি ডাটাবেস স্তর রয়েছে৷ সরলতার জন্য, এই প্রজেক্টে নেটওয়ার্কিং লেয়ারটি প্রকৃত নেটওয়ার্ক অনুরোধ করার পরিবর্তে বিলম্ব সহ একটি HashMap সাথে সিমুলেট করা হয়েছে।

DefaultTasksRepository নেটওয়ার্কিং স্তর এবং ডাটাবেস স্তরের মধ্যে সমন্বয় বা মধ্যস্থতা করে এবং এটিই UI স্তরে ডেটা ফেরত দেয়।

UI স্তর (.addedittask, .statistics, .taskdetail, .tasks)

প্রতিটি UI স্তর প্যাকেজে একটি খণ্ড এবং একটি ভিউ মডেল রয়েছে, সাথে UI-এর জন্য প্রয়োজনীয় অন্যান্য ক্লাসের সাথে (যেমন টাস্ক তালিকার জন্য একটি অ্যাডাপ্টার)। TaskActivity হল সেই ক্রিয়াকলাপ যাতে সমস্ত অংশ থাকে।

নেভিগেশন

অ্যাপের জন্য নেভিগেশন নেভিগেশন উপাদান দ্বারা নিয়ন্ত্রিত হয়। এটি nav_graph.xml ফাইলে সংজ্ঞায়িত করা হয়েছে। Event ক্লাস ব্যবহার করে ভিউ মডেলগুলিতে নেভিগেশন ট্রিগার করা হয়; ভিউ মডেলগুলিও নির্ধারণ করে যে কোন আর্গুমেন্টগুলি পাস করতে হবে। Event ইভেন্টগুলি পর্যবেক্ষণ করে এবং পর্দার মধ্যে প্রকৃত নেভিগেশন করে।

এই কোডল্যাবে, আপনি শিখবেন কিভাবে রিপোজিটরি পরীক্ষা করতে হয়, মডেল দেখতে হয় এবং টেস্ট ডবলস এবং ডিপেন্ডেন্সি ইনজেকশন ব্যবহার করে টুকরোগুলো দেখতে হয়। সেগুলি কী তা নিয়ে আপনি ডুব দেওয়ার আগে, আপনি এই পরীক্ষাগুলি কী এবং কীভাবে লিখবেন তা নির্দেশ করবে এমন যুক্তি বোঝা গুরুত্বপূর্ণ।

এই বিভাগে সাধারণভাবে পরীক্ষার কিছু সর্বোত্তম অনুশীলন কভার করা হয়েছে, কারণ সেগুলি Android এ প্রযোজ্য।

টেস্টিং পিরামিড

একটি পরীক্ষার কৌশল সম্পর্কে চিন্তা করার সময়, তিনটি সম্পর্কিত পরীক্ষার দিক রয়েছে:

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

এই দিকগুলির মধ্যে সহজাত ট্রেড-অফ রয়েছে। উদাহরণস্বরূপ, গতি এবং বিশ্বস্ততা একটি ট্রেড-অফ—যত দ্রুত পরীক্ষা, সাধারণত, কম বিশ্বস্ততা এবং তদ্বিপরীত। স্বয়ংক্রিয় পরীক্ষাগুলিকে এই তিনটি বিভাগে ভাগ করার একটি সাধারণ উপায় হল:

  • ইউনিট পরীক্ষা -এগুলি অত্যন্ত ফোকাসড পরীক্ষা যা একটি একক ক্লাসে চলে, সাধারণত সেই ক্লাসে একটি একক পদ্ধতি। যদি একটি ইউনিট পরীক্ষা ব্যর্থ হয়, আপনি আপনার কোডে সমস্যাটি ঠিক কোথায় তা জানতে পারবেন। বাস্তব বিশ্বে তাদের বিশ্বস্ততা কম, আপনার অ্যাপটি একটি পদ্ধতি বা শ্রেণির সম্পাদনের চেয়ে অনেক বেশি জড়িত। আপনি যখনই আপনার কোড পরিবর্তন করেন তখন তারা চালানোর জন্য যথেষ্ট দ্রুত। এগুলি প্রায়শই স্থানীয়ভাবে পরীক্ষা করা হবে ( test উত্স সেটে)। উদাহরণ: ভিউ মডেল এবং সংগ্রহস্থলে একক পদ্ধতি পরীক্ষা করা।
  • ইন্টিগ্রেশন পরীক্ষা -এগুলি একসাথে ব্যবহার করার সময় তারা প্রত্যাশা অনুযায়ী আচরণ করে তা নিশ্চিত করতে বেশ কয়েকটি ক্লাসের মিথস্ক্রিয়া পরীক্ষা করে। ইন্টিগ্রেশন টেস্ট গঠনের একটি উপায় হল তাদের একটি একক বৈশিষ্ট্য পরীক্ষা করা, যেমন একটি টাস্ক সংরক্ষণ করার ক্ষমতা। তারা ইউনিট পরীক্ষার তুলনায় কোডের একটি বৃহত্তর সুযোগ পরীক্ষা করে, কিন্তু এখনও সম্পূর্ণ বিশ্বস্ততা থাকার বিপরীতে দ্রুত চালানোর জন্য অপ্টিমাইজ করা হয়। পরিস্থিতির উপর নির্ভর করে এগুলি স্থানীয়ভাবে বা যন্ত্র পরীক্ষা হিসাবে চালানো যেতে পারে। উদাহরণ: একটি একক খণ্ডের সমস্ত কার্যকারিতা পরীক্ষা করা এবং মডেল জোড়া দেখুন।
  • এন্ড টু এন্ড টেস্ট (E2e) — একসাথে কাজ করা বৈশিষ্ট্যের সমন্বয় পরীক্ষা করুন। তারা অ্যাপের বড় অংশ পরীক্ষা করে, বাস্তব ব্যবহার ঘনিষ্ঠভাবে অনুকরণ করে এবং তাই সাধারণত ধীর হয়। তাদের সর্বোচ্চ বিশ্বস্ততা রয়েছে এবং আপনাকে বলে যে আপনার আবেদনটি আসলে সামগ্রিকভাবে কাজ করে। সর্বোপরি, এই পরীক্ষাগুলি হবে যন্ত্রযুক্ত পরীক্ষা ( androidTest উত্স সেটে)
    উদাহরণ: পুরো অ্যাপটি শুরু করা এবং কয়েকটি বৈশিষ্ট্য একসাথে পরীক্ষা করা।

এই পরীক্ষাগুলির প্রস্তাবিত অনুপাত প্রায়শই একটি পিরামিড দ্বারা প্রতিনিধিত্ব করা হয়, যার বেশিরভাগ পরীক্ষাই ইউনিট পরীক্ষা।

আর্কিটেকচার এবং টেস্টিং

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

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



এই পাঠে, আপনি দেখতে পাবেন কীভাবে উপরের আর্কিটেকচারের অংশগুলি সঠিকভাবে বিচ্ছিন্নভাবে পরীক্ষা করা যায়:

  1. প্রথমে আপনি সংগ্রহস্থলটি ইউনিট পরীক্ষা করবেন।
  2. তারপর আপনি ভিউ মডেলে একটি টেস্ট ডাবল ব্যবহার করবেন, যা ভিউ মডেলের ইউনিট টেস্টিং এবং ইন্টিগ্রেশন পরীক্ষার জন্য প্রয়োজনীয়।
  3. এর পরে, আপনি টুকরো এবং তাদের ভিউ মডেলগুলির জন্য ইন্টিগ্রেশন পরীক্ষা লিখতে শিখবেন।
  4. অবশেষে, আপনি নেভিগেশন উপাদান অন্তর্ভুক্ত ইন্টিগ্রেশন পরীক্ষা লিখতে শিখবেন।

শেষ থেকে শেষ পরীক্ষা পরবর্তী পাঠে কভার করা হবে।

আপনি যখন একটি ক্লাসের একটি অংশের জন্য একটি ইউনিট পরীক্ষা লেখেন (একটি পদ্ধতি বা পদ্ধতির একটি ছোট সংগ্রহ), আপনার লক্ষ্য শুধুমাত্র সেই ক্লাসের কোডটি পরীক্ষা করা

একটি নির্দিষ্ট ক্লাস বা ক্লাসে শুধুমাত্র কোড পরীক্ষা করা কঠিন হতে পারে। এর একটি উদাহরণ তাকান. main উৎস সেটে data.source.DefaultTaskRepository ক্লাস খুলুন। এটি অ্যাপের সংগ্রহস্থল, এবং এটি সেই ক্লাস যা আপনি পরবর্তী ইউনিট পরীক্ষা লিখবেন।

আপনার লক্ষ্য হল সেই ক্লাসে শুধুমাত্র কোড পরীক্ষা করা। তবুও, DefaultTaskRepository অন্যান্য ক্লাসের উপর নির্ভর করে, যেমন LocalTaskDataSource এবং RemoteTaskDataSource কাজ করার জন্য। এটি বলার আরেকটি উপায় হল LocalTaskDataSource এবং RemoteTaskDataSource হল DefaultTaskRepository এর নির্ভরতা

তাই DefaultTaskRepository এর প্রতিটি মেথড ডেটা সোর্স ক্লাসে মেথড কল করে, যা অন্য ক্লাসে কল মেথডকে ডাটাবেসে তথ্য সংরক্ষণ করতে বা নেটওয়ার্কের সাথে যোগাযোগ করে।



উদাহরণস্বরূপ, DefaultTasksRepo তে এই পদ্ধতিটি দেখুন।

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks হল সবচেয়ে "বেসিক" কলগুলির মধ্যে একটি যা আপনি আপনার সংগ্রহস্থলে করতে পারেন। এই পদ্ধতিতে একটি SQLite ডাটাবেস থেকে পড়া এবং নেটওয়ার্ক কল করা (আপডেট করার জন্য কল updateTasksFromRemoteDataSource ) অন্তর্ভুক্ত রয়েছে। এটি শুধুমাত্র সংগ্রহস্থল কোডের চেয়ে অনেক বেশি কোড জড়িত।

এখানে আরও কিছু নির্দিষ্ট কারণ রয়েছে কেন সংগ্রহস্থল পরীক্ষা করা কঠিন:

  • এই সংগ্রহস্থলের জন্য এমনকি সবচেয়ে সহজ পরীক্ষা করার জন্য আপনাকে একটি ডাটাবেস তৈরি এবং পরিচালনা করার বিষয়ে চিন্তাভাবনা করতে হবে। এটি "এটি কি স্থানীয় বা যন্ত্রযুক্ত পরীক্ষা হওয়া উচিত?" এর মতো প্রশ্নগুলি নিয়ে আসে? এবং যদি আপনার একটি সিমুলেটেড অ্যান্ড্রয়েড পরিবেশ পেতে AndroidX টেস্ট ব্যবহার করা উচিত।
  • কোডের কিছু অংশ, যেমন নেটওয়ার্কিং কোড, চালানোর জন্য দীর্ঘ সময় নিতে পারে, বা মাঝে মাঝে এমনকি ব্যর্থ হতে পারে, দীর্ঘক্ষণ চলমান, ফ্ল্যাকি পরীক্ষা তৈরি করে।
  • আপনার পরীক্ষাগুলি পরীক্ষার ব্যর্থতার জন্য কোন কোডটি ত্রুটিযুক্ত তা নির্ণয় করার ক্ষমতা হারাতে পারে। আপনার পরীক্ষাগুলি নন-রিপোজিটরি কোড পরীক্ষা করা শুরু করতে পারে, তাই, উদাহরণস্বরূপ, আপনার অনুমিত "রিপোজিটরি" ইউনিট পরীক্ষাগুলি ডাটাবেস কোডের মতো কিছু নির্ভরশীল কোডে একটি সমস্যার কারণে ব্যর্থ হতে পারে।

টেস্ট ডাবলস

এর সমাধান হল আপনি যখন রিপোজিটরি পরীক্ষা করছেন, তখন প্রকৃত নেটওয়ার্কিং বা ডাটাবেস কোড ব্যবহার করবেন না , পরিবর্তে একটি টেস্ট ডাবল ব্যবহার করুন। একটি টেস্ট ডবল হল পরীক্ষার জন্য বিশেষভাবে তৈরি করা ক্লাসের একটি সংস্করণ। এটি পরীক্ষায় একটি ক্লাসের আসল সংস্করণ প্রতিস্থাপন করার জন্য বোঝানো হয়েছে। স্টান্ট ডবল একজন অভিনেতা যে স্টান্টে পারদর্শী এবং বিপজ্জনক ক্রিয়াকলাপের জন্য প্রকৃত অভিনেতাকে প্রতিস্থাপন করে তার সাথে এটি একই রকম।

এখানে কিছু ধরণের টেস্ট ডাবল রয়েছে:

নকল

একটি টেস্ট ডাবল যার ক্লাসের একটি "কাজ" বাস্তবায়ন রয়েছে, কিন্তু এটি এমনভাবে প্রয়োগ করা হয়েছে যা এটিকে পরীক্ষার জন্য ভাল করে তোলে কিন্তু উৎপাদনের জন্য অনুপযুক্ত।

উপহাস

একটি টেস্ট ডবল যা ট্র্যাক করে তার কোন পদ্ধতিগুলিকে বলা হয়েছিল৷ তারপরে এটির পদ্ধতিগুলি সঠিকভাবে বলা হয়েছিল কিনা তার উপর নির্ভর করে এটি একটি পরীক্ষা পাস করে বা ব্যর্থ হয়।

অসম্পূর্ণ

একটি পরীক্ষা দ্বিগুণ যাতে কোন যুক্তি নেই এবং শুধুমাত্র আপনি যা প্রোগ্রাম করেন তা ফেরত দেয়। একটি StubTaskRepository উদাহরন স্বরূপ getTasks থেকে নির্দিষ্ট কিছু কাজের সমন্বয় ফেরানোর জন্য প্রোগ্রাম করা যেতে পারে।

ডামি

একটি পরীক্ষা দ্বিগুণ যা চারপাশে পাস করা হয় কিন্তু ব্যবহার করা হয় না, যেমন আপনাকে শুধুমাত্র একটি প্যারামিটার হিসাবে এটি প্রদান করতে হবে। আপনার যদি একটি NoOpTaskRepository , তবে এটি কোনো পদ্ধতিতে কোনো কোড ছাড়াই TaskRepository বাস্তবায়ন করবে।

গুপ্তচর

একটি টেস্ট ডবল যা কিছু অতিরিক্ত তথ্যের ট্র্যাক রাখে; উদাহরণস্বরূপ, আপনি যদি একটি SpyTaskRepository তৈরি করেন, তাহলে এটি addTask পদ্ধতিটি কতবার কল করা হয়েছে তার ট্র্যাক রাখতে পারে।

টেস্ট ডাবল সম্পর্কে আরও তথ্যের জন্য, টয়লেটে টেস্টিং দেখুন: আপনার টেস্ট ডাবলস জানুন

অ্যান্ড্রয়েডে ব্যবহৃত সবচেয়ে সাধারণ টেস্ট ডাবল হল ফেকস এবং মকস

এই টাস্কে, আপনি একটি FakeDataSource টেস্ট তৈরি করতে যাচ্ছেন যা ইউনিট টেস্ট DefaultTasksRepository প্রকৃত ডেটা উৎস থেকে ডিকপল করা হয়েছে।

ধাপ 1: FakeDataSource ক্লাস তৈরি করুন

এই ধাপে আপনি FakeDataSouce নামক একটি ক্লাস তৈরি করতে যাচ্ছেন, যা একটি LocalDataSource এবং RemoteDataSource এর দ্বিগুণ পরীক্ষা হবে।

  1. পরীক্ষার উৎস সেটে, নতুন -> প্যাকেজ নির্বাচন করুন ডান ক্লিক করুন।

  1. ভিতরে একটি উৎস প্যাকেজ সহ একটি ডেটা প্যাকেজ তৈরি করুন।
  2. ডেটা/সোর্স প্যাকেজে FakeDataSource নামে একটি নতুন ক্লাস তৈরি করুন।

ধাপ 2: TasksDataSource ইন্টারফেস বাস্তবায়ন করুন

আপনার নতুন ক্লাস FakeDataSource একটি টেস্ট ডাবল হিসাবে ব্যবহার করতে সক্ষম হতে, এটি অবশ্যই অন্যান্য ডেটা উত্সগুলি প্রতিস্থাপন করতে সক্ষম হবে। এই ডেটা উত্সগুলি হল TasksLocalDataSource এবং TasksRemoteDataSource

  1. লক্ষ্য করুন কিভাবে এই দুটিই TasksDataSource ইন্টারফেস বাস্তবায়ন করে।
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. FakeDataSource কার্যকর করুন TasksDataSource :
class FakeDataSource : TasksDataSource {

}

Android স্টুডিও অভিযোগ করবে যে আপনি TasksDataSource জন্য প্রয়োজনীয় পদ্ধতি প্রয়োগ করেননি।

  1. দ্রুত-সমাধান মেনু ব্যবহার করুন এবং সদস্যদের বাস্তবায়ন নির্বাচন করুন।


  1. সমস্ত পদ্ধতি নির্বাচন করুন এবং ঠিক আছে টিপুন।

ধাপ 3: FakeDataSource এ getTasks পদ্ধতি প্রয়োগ করুন

FakeDataSource হল একটি নির্দিষ্ট ধরনের টেস্ট ডবল যাকে জাল বলা হয়। একটি নকল হল একটি টেস্ট ডবল যার ক্লাসের "কাজ" বাস্তবায়ন রয়েছে, কিন্তু এটি এমনভাবে প্রয়োগ করা হয়েছে যা এটি পরীক্ষার জন্য ভাল কিন্তু উত্পাদনের জন্য অনুপযুক্ত। "ওয়ার্কিং" বাস্তবায়নের অর্থ হল ক্লাসটি ইনপুট দেওয়া বাস্তবসম্মত আউটপুট তৈরি করবে।

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

একটি FakeDataSource

  • আপনি একটি বাস্তব ডাটাবেস বা নেটওয়ার্কের উপর নির্ভর করার প্রয়োজন ছাড়াই DefaultTasksRepository এ কোড পরীক্ষা করতে দেয়।
  • পরীক্ষার জন্য একটি "বাস্তব-পর্যাপ্ত" বাস্তবায়ন প্রদান করে।
  1. FakeDataSource কন্সট্রাক্টর পরিবর্তন করে একটি var তৈরি করতে হবে যার নাম tasks যা একটি MutableList<Task>? একটি খালি পরিবর্তনযোগ্য তালিকার একটি ডিফল্ট মান সহ।
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


এটি একটি ডাটাবেস বা সার্ভার প্রতিক্রিয়া হিসাবে "জাল" কাজের তালিকা। আপাতত, লক্ষ্য হল সংগ্রহস্থলের getTasks পদ্ধতি পরীক্ষা করা। এটি ডেটা উত্সের getTasks , deleteAllTasks এবং saveTask পদ্ধতিগুলিকে কল করে৷

এই পদ্ধতিগুলির একটি জাল সংস্করণ লিখুন:

  1. getTasks লিখুন : যদি tasks null না হয়, একটি Success ফলাফল ফেরত দিন। যদি tasks null হয়, একটি Error ফলাফল ফেরত দিন।
  2. deleteAllTasks লিখুন: পরিবর্তনযোগ্য কাজের তালিকা সাফ করুন।
  3. saveTask লিখুন: তালিকায় টাস্ক যোগ করুন।

এই পদ্ধতিগুলি, FakeDataSource এর জন্য প্রয়োগ করা হয়েছে, নীচের কোডের মতো দেখতে৷

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

প্রয়োজন হলে এখানে আমদানি বিবৃতি আছে:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

এটি প্রকৃত স্থানীয় এবং দূরবর্তী ডেটা উত্সগুলি কীভাবে কাজ করে তার অনুরূপ।

এই ধাপে, আপনি ম্যানুয়াল ডিপেন্ডেন্সি ইনজেকশন নামক একটি কৌশল ব্যবহার করতে যাচ্ছেন যাতে আপনি এইমাত্র তৈরি করা জাল পরীক্ষা দ্বিগুণ ব্যবহার করতে পারেন।

প্রধান সমস্যা হল যে আপনার কাছে একটি FakeDataSource আছে, কিন্তু আপনি কীভাবে পরীক্ষায় এটি ব্যবহার করবেন তা স্পষ্ট নয়। এটি TasksRemoteDataSource এবং TasksLocalDataSource প্রতিস্থাপন করতে হবে, কিন্তু শুধুমাত্র পরীক্ষায়। TasksRemoteDataSource এবং TasksLocalDataSource উভয়ই DefaultTasksRepository এর নির্ভরতা, যার অর্থ এই ক্লাসগুলি চালানোর জন্য DefaultTasksRepositories প্রয়োজন বা "নির্ভর করে"।

এই মুহূর্তে, DefaultTasksRepository এর init পদ্ধতির মধ্যে নির্ভরতাগুলি তৈরি করা হয়েছে।

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

যেহেতু আপনি DefaultTasksRepository-এর ভিতরে taskLocalDataSource এবং tasksRemoteDataSource তৈরি এবং বরাদ্দ DefaultTasksRepository , সেগুলি মূলত হার্ড কোডেড। আপনার টেস্ট ডাবলে অদলবদল করার কোন উপায় নেই।

আপনি এর পরিবর্তে যা করতে চান তা হল এই ডেটা উত্সগুলিকে হার্ড-কোডিংয়ের পরিবর্তে ক্লাসে সরবরাহ করা। নির্ভরতা প্রদান করা নির্ভরতা ইনজেকশন হিসাবে পরিচিত। নির্ভরতা প্রদানের বিভিন্ন উপায় রয়েছে এবং তাই বিভিন্ন ধরনের নির্ভরতা ইনজেকশন রয়েছে।

কনস্ট্রাক্টর ডিপেনডেন্সি ইনজেকশন আপনাকে কনস্ট্রাক্টরে পাস করে টেস্ট ডবলে অদলবদল করতে দেয়।

ইনজেকশন নেই

ইনজেকশন

ধাপ 1: DefaultTasksRepository-এ কনস্ট্রাক্টর ডিপেন্ডেন্সি ইনজেকশন ব্যবহার করুন

  1. DefaultTaskRepository এর কনস্ট্রাক্টরকে একটি Application গ্রহণ করা থেকে ডেটা উত্স এবং coroutine প্রেরণকারী উভয়ই গ্রহণ করতে পরিবর্তন করুন (যা আপনাকে আপনার পরীক্ষার জন্য অদলবদল করতে হবে - এটি coroutines-এর তৃতীয় পাঠ বিভাগে আরও বিশদে বর্ণনা করা হয়েছে)।

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. যেহেতু আপনি নির্ভরতা পাস করেছেন, init পদ্ধতিটি সরান। আপনাকে আর নির্ভরতা তৈরি করতে হবে না।
  2. এছাড়াও পুরানো উদাহরণ ভেরিয়েবল মুছে দিন। আপনি কনস্ট্রাক্টরে তাদের সংজ্ঞায়িত করছেন:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. অবশেষে, নতুন কনস্ট্রাক্টর ব্যবহার করতে getRepository পদ্ধতি আপডেট করুন:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

আপনি এখন কনস্ট্রাক্টর নির্ভরতা ইনজেকশন ব্যবহার করছেন!

ধাপ 2: আপনার পরীক্ষায় আপনার FakeDataSource ব্যবহার করুন

এখন আপনার কোড কনস্ট্রাক্টর নির্ভরতা ইনজেকশন ব্যবহার করছে, আপনি আপনার DefaultTasksRepository পরীক্ষা করতে আপনার জাল ডেটা উৎস ব্যবহার করতে পারেন।

  1. DefaultTasksRepository ক্লাসের নামের উপর ডান-ক্লিক করুন এবং জেনারেট নির্বাচন করুন, তারপর পরীক্ষা করুন।
  2. পরীক্ষার উৎস সেটে DefaultTasksRepositoryTest তৈরি করতে প্রম্পটগুলি অনুসরণ করুন।
  3. আপনার নতুন DefaultTasksRepositoryTest ক্লাসের শীর্ষে, আপনার জাল ডেটা উত্সগুলিতে ডেটা উপস্থাপন করতে নীচে সদস্য ভেরিয়েবলগুলি যুক্ত করুন৷

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. তিনটি ভেরিয়েবল তৈরি করুন, দুটি FakeDataSource সদস্য ভেরিয়েবল (আপনার সংগ্রহস্থলের জন্য প্রতিটি ডেটা উৎসের জন্য একটি) এবং DefaultTasksRepository এর জন্য একটি পরিবর্তনশীল যা আপনি পরীক্ষা করবেন।

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

একটি পরীক্ষাযোগ্য DefaultTasksRepository সেট আপ এবং আরম্ভ করার জন্য একটি পদ্ধতি তৈরি করুন। এই DefaultTasksRepository আপনার টেস্ট ডবল, FakeDataSource ব্যবহার করবে।

  1. createRepository নামে একটি পদ্ধতি তৈরি করুন এবং @Before দিয়ে এটি টীকা করুন।
  2. remoteTasks এবং localTasks লিস্ট ব্যবহার করে আপনার জাল ডেটা সোর্স ইনস্ট্যান্টিয়েট করুন।
  3. আপনার তৈরি করা দুটি জাল ডেটা উত্স এবং Dispatchers.Unconfined ব্যবহার করে আপনার কাজগুলি রিপোজিটরিকে ইনস্ট্যান্ট করুন৷ tasksRepository

চূড়ান্ত পদ্ধতিটি নীচের কোডের মতো হওয়া উচিত।

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

ধাপ 3: DefaultTasksRepository getTasks() টেস্ট লিখুন

একটি DefaultTasksRepository পরীক্ষা লেখার সময়!

  1. সংগ্রহস্থলের getTasks পদ্ধতির জন্য একটি পরীক্ষা লিখুন। আপনি যখন getTasks কে true দিয়ে কল করেন (অর্থাৎ এটি দূরবর্তী ডেটা উৎস থেকে পুনরায় লোড করা উচিত) তখন পরীক্ষা করুন যে এটি দূরবর্তী ডেটা উত্স থেকে ডেটা ফেরত দেয় (স্থানীয় ডেটা উত্সের বিপরীতে)।

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

আপনি getTasks: কল করলে আপনি একটি ত্রুটি পাবেন :

ধাপ 4: রানব্লকিং টেস্ট যোগ করুন

coroutine ত্রুটি প্রত্যাশিত কারণ getTasks হল একটি suspend ফাংশন এবং এটিকে কল করার জন্য আপনাকে একটি coroutine চালু করতে হবে৷ এর জন্য, আপনার একটি করুটিন সুযোগ প্রয়োজন। এই ত্রুটিটি সমাধান করার জন্য, আপনার পরীক্ষায় লঞ্চিং কোরোটিনগুলি পরিচালনা করার জন্য আপনাকে কিছু গ্রেডেল নির্ভরতা যুক্ত করতে হবে।

  1. টেস্ট ইমপ্লিমেন্টেশন ব্যবহার করে সেট করা পরীক্ষার উৎসে testImplementation পরীক্ষার জন্য প্রয়োজনীয় নির্ভরতা যোগ করুন।

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

সিঙ্ক করতে ভুলবেন না!

kotlinx-coroutines-test হল coroutines টেস্ট লাইব্রেরি, বিশেষ করে coroutines পরীক্ষা করার জন্য। আপনার পরীক্ষা চালানোর জন্য, runBlockingTest ফাংশনটি ব্যবহার করুন। এটি coroutines টেস্ট লাইব্রেরি দ্বারা প্রদত্ত একটি ফাংশন। এটি কোডের একটি ব্লক নেয় এবং তারপরে কোডের এই ব্লকটিকে একটি বিশেষ কোরাউটিন প্রসঙ্গে চালায় যা সিঙ্ক্রোনাসভাবে এবং অবিলম্বে চলে, যার অর্থ ক্রিয়াগুলি একটি নির্ধারক ক্রমে ঘটবে। এটি মূলত আপনার কোরোটিনগুলিকে নন-করোটিনের মতো চালায়, তাই এটি কোড পরীক্ষার জন্য বোঝানো হয়েছে।

আপনি যখন একটি suspend ফাংশন কল করছেন তখন আপনার পরীক্ষার ক্লাসে runBlockingTest ব্যবহার করুন। রানব্লকিংটেস্ট কীভাবে কাজ করে এবং এই সিরিজের পরবর্তী কোডল্যাবে কীভাবে runBlockingTest পরীক্ষা করা যায় সে সম্পর্কে আপনি আরও শিখবেন।

  1. ক্লাসের উপরে @ExperimentalCoroutinesApi যোগ করুন। এটি প্রকাশ করে যে আপনি জানেন যে আপনি ক্লাসে একটি পরীক্ষামূলক coroutine api ( runBlockingTest ) ব্যবহার করছেন। এটি ছাড়া, আপনি একটি সতর্কতা পাবেন।
  2. আপনার DefaultTasksRepositoryTest এ ফিরে আসুন, runBlockingTest যোগ করুন যাতে এটি আপনার পুরো পরীক্ষায় কোডের "ব্লক" হিসেবে নেয়।

এই চূড়ান্ত পরীক্ষা নীচের কোড মত দেখায়.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. আপনার নতুন getTasks_requestsAllTasksFromRemoteDataSource পরীক্ষা চালান এবং নিশ্চিত করুন যে এটি কাজ করে এবং ত্রুটিটি চলে গেছে!

আপনি এইমাত্র দেখেছেন কিভাবে একটি সংগ্রহস্থলের ইউনিট পরীক্ষা করা যায়। এই পরবর্তী ধাপগুলিতে, আপনি আবার নির্ভরতা ইনজেকশন ব্যবহার করতে যাচ্ছেন এবং আরেকটি পরীক্ষা দ্বিগুণ তৈরি করতে যাচ্ছেন—এবার আপনার ভিউ মডেলের জন্য ইউনিট এবং ইন্টিগ্রেশন পরীক্ষা কীভাবে লিখতে হয় তা দেখানোর জন্য।

ইউনিট পরীক্ষাগুলি শুধুমাত্র সেই শ্রেণী বা পদ্ধতির পরীক্ষা করা উচিত যেটিতে আপনি আগ্রহী। এটি বিচ্ছিন্নতার পরীক্ষা হিসাবে পরিচিত, যেখানে আপনি আপনার "ইউনিট" পরিষ্কারভাবে বিচ্ছিন্ন করেন এবং শুধুমাত্র সেই ইউনিটের অংশ যে কোডটি পরীক্ষা করেন।

তাই TasksViewModelTest শুধুমাত্র TasksViewModel কোড পরীক্ষা করা উচিত-এটি ডাটাবেস, নেটওয়ার্ক বা সংগ্রহস্থলের ক্লাসে পরীক্ষা করা উচিত নয়। তাই আপনার ভিউ মডেলগুলির জন্য, যেমন আপনি আপনার সংগ্রহস্থলের জন্য করেছেন, আপনি একটি জাল সংগ্রহস্থল তৈরি করবেন এবং আপনার পরীক্ষায় এটি ব্যবহার করার জন্য নির্ভরতা ইনজেকশন প্রয়োগ করবেন।

এই টাস্কে, আপনি মডেলগুলি দেখার জন্য নির্ভরতা ইনজেকশন প্রয়োগ করেন।

ধাপ 1. একটি TasksRepository ইন্টারফেস তৈরি করুন

কনস্ট্রাক্টর নির্ভরতা ইনজেকশন ব্যবহার করার প্রথম ধাপ হল জাল এবং আসল ক্লাসের মধ্যে ভাগ করা একটি সাধারণ ইন্টারফেস তৈরি করা।

এই অনুশীলনে কিভাবে দেখায়? TasksRemoteDataSource , TasksLocalDataSource এবং FakeDataSource দেখুন এবং লক্ষ্য করুন যে তারা সবাই একই ইন্টারফেস ভাগ করে: TasksDataSource । এটি আপনাকে DefaultTasksRepository এর কনস্ট্রাক্টরে বলতে দেয় যা আপনি একটি TasksDataSource এ নেন।

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

এটিই আমাদেরকে আপনার FakeDataSource অদলবদল করতে দেয়!

এরপরে, DefaultTasksRepository এর জন্য একটি ইন্টারফেস তৈরি করুন, যেমন আপনি ডেটা উত্সের জন্য করেছিলেন। এটি DefaultTasksRepository এর সমস্ত পাবলিক পদ্ধতি (পাবলিক API পৃষ্ঠ) অন্তর্ভুক্ত করতে হবে।

  1. DefaultTasksRepository খুলুন এবং ক্লাসের নামের উপর ডান-ক্লিক করুন। তারপর রিফ্যাক্টর -> এক্সট্রাক্ট -> ইন্টারফেস নির্বাচন করুন।

  1. আলাদা ফাইল করার জন্য এক্সট্রাক্ট নির্বাচন করুন।

  1. এক্সট্র্যাক্ট ইন্টারফেস উইন্ডোতে, ইন্টারফেসের নামটি TasksRepository এ পরিবর্তন করুন।
  2. মেম্বারস টু ফর্ম ইন্টারফেস বিভাগে, দুই সহচর সদস্য এবং ব্যক্তিগত পদ্ধতি ব্যতীত সমস্ত সদস্য পরীক্ষা করুন।


  1. রিফ্যাক্টরে ক্লিক করুন। নতুন TasksRepository ইন্টারফেস ডেটা/সোর্স প্যাকেজে উপস্থিত হওয়া উচিত।

এবং DefaultTasksRepository এখন TasksRepository প্রয়োগ করে।

  1. সবকিছু এখনও কার্যকরী ক্রমে আছে তা নিশ্চিত করতে আপনার অ্যাপটি চালান (পরীক্ষা নয়)।

ধাপ 2. FakeTestRepository তৈরি করুন

এখন আপনার ইন্টারফেস আছে, আপনি DefaultTaskRepository টেস্ট ডবল তৈরি করতে পারেন।

  1. টেস্ট সোর্স সেটে, ডেটা/সোর্সে Kotlin ফাইল তৈরি করুন এবং FakeTestRepository.kt ক্লাস করুন এবং TasksRepository ইন্টারফেস থেকে প্রসারিত করুন।

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

আপনাকে বলা হবে যে আপনাকে ইন্টারফেস পদ্ধতিগুলি বাস্তবায়ন করতে হবে।

  1. যতক্ষণ না আপনি পরামর্শ মেনুটি দেখতে পাচ্ছেন ততক্ষণ ত্রুটিটির উপরে হোভার করুন, তারপরে ক্লিক করুন এবং সদস্য প্রয়োগ করুন নির্বাচন করুন।
  1. সমস্ত পদ্ধতি নির্বাচন করুন এবং ঠিক আছে টিপুন।

ধাপ 3. FakeTestRepository পদ্ধতি প্রয়োগ করুন

আপনার কাছে এখন "বাস্তবায়িত নয়" পদ্ধতি সহ একটি FakeTestRepository ক্লাস আছে। আপনি যেভাবে FakeDataSource প্রয়োগ করেছেন তার অনুরূপ, FakeTestRepository স্থানীয় এবং দূরবর্তী ডেটা উত্সগুলির মধ্যে একটি জটিল মধ্যস্থতার সাথে মোকাবিলা করার পরিবর্তে একটি ডেটা কাঠামো দ্বারা সমর্থন করা হবে৷

মনে রাখবেন যে আপনার FakeTestRepository এর FakeDataSource s বা এরকম কিছু ব্যবহার করার দরকার নেই; এটি শুধুমাত্র প্রদত্ত ইনপুট বাস্তবসম্মত জাল আউটপুট ফেরত প্রয়োজন. আপনি কাজের তালিকা সংরক্ষণ করতে একটি LinkedHashMap এবং আপনার পর্যবেক্ষণযোগ্য কাজের জন্য একটি MutableLiveData ব্যবহার করবেন।

  1. FakeTestRepository এ, একটি LinkedHashMap ভেরিয়েবল যোগ করুন যা বর্তমান কাজের তালিকা এবং আপনার পর্যবেক্ষণযোগ্য কাজের জন্য একটি MutableLiveData উপস্থাপন করে।

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

নিম্নলিখিত পদ্ধতিগুলি প্রয়োগ করুন:

  1. getTasks — এই পদ্ধতিটি tasksServiceData নিতে হবে এবং এটিকে tasksServiceData.values.toList() ব্যবহার করে একটি তালিকায় পরিণত করতে হবে এবং তারপর এটিকে Success ফলাফল হিসাবে ফিরিয়ে দিতে হবে।
  2. refreshTasksgetTasks() দ্বারা যা ফেরত দেওয়া হয় তা হতে observableTasks টাস্কের মান আপডেট করে।
  3. observeTasks — runBlocking ব্যবহার করে একটি runBlocking তৈরি করে এবং observableTasks চালায়, তারপর refreshTasks ফেরত দেয়।

নীচে সেই পদ্ধতিগুলির জন্য কোড রয়েছে।

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

ধাপ 4. অ্যাড টাস্কে পরীক্ষার জন্য একটি পদ্ধতি যোগ করুন

পরীক্ষা করার সময়, আপনার Tasks ইতিমধ্যে কিছু কাজ থাকা ভাল। আপনি saveTask বেশ কয়েকবার কল করতে পারেন, কিন্তু এটি সহজ করতে, বিশেষভাবে পরীক্ষার জন্য একটি সহায়ক পদ্ধতি যোগ করুন যা আপনাকে কাজগুলি যোগ করতে দেয়।

  1. addTasks মেথড যোগ করুন, যা অনেকগুলো টাস্ক HashMap , প্রত্যেকটিকে vararg এ যোগ করে, এবং তারপর কাজগুলো রিফ্রেশ করে।

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

এই মুহুর্তে আপনার কাছে বাস্তবায়িত কয়েকটি মূল পদ্ধতির সাথে পরীক্ষার জন্য একটি জাল সংগ্রহস্থল রয়েছে। পরবর্তী, আপনার পরীক্ষায় এটি ব্যবহার করুন!

এই টাস্কে আপনি একটি ViewModel এর ভিতরে একটি জাল ক্লাস ব্যবহার করেন। TasksViewModel এর কনস্ট্রাক্টরে একটি TasksRepository ভেরিয়েবল যোগ করে কন্সট্রাক্টর নির্ভরতা ইনজেকশনের মাধ্যমে দুটি ডেটা উত্স গ্রহণ করতে কনস্ট্রাক্টর নির্ভরতা ইনজেকশন ব্যবহার করুন।

ভিউ মডেলগুলির সাথে এই প্রক্রিয়াটি একটু ভিন্ন কারণ আপনি সেগুলি সরাসরি নির্মাণ করেন না। উদাহরণ স্বরূপ:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


উপরের কোডের মতো, আপনি viewModel's সম্পত্তি প্রতিনিধি ব্যবহার করছেন যা ভিউ মডেল তৈরি করে। ভিউ মডেলটি কীভাবে তৈরি করা হয় তা পরিবর্তন করতে, আপনাকে একটি ViewModelProvider.Factory যোগ করতে হবে এবং ব্যবহার করতে হবে। আপনি যদি ViewModelProvider.Factory সাথে পরিচিত না হন তবে আপনি এখানে এটি সম্পর্কে আরও জানতে পারেন।

ধাপ 1. TasksViewModel-এ একটি ViewModelFactory তৈরি করুন এবং ব্যবহার করুন

আপনি Tasks স্ক্রিনের সাথে সম্পর্কিত ক্লাস এবং পরীক্ষা আপডেট করে শুরু করুন।

  1. TasksViewModel খুলুন
  2. TasksViewModel TasksRepository ভিতরে তৈরি না করে তা নিতে পারেন।

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

যেহেতু আপনি কনস্ট্রাক্টর পরিবর্তন করেছেন, তাই আপনাকে এখন TasksViewModel করতে একটি কারখানা ব্যবহার করতে হবে। TasksViewModel এর মতো একই ফাইলে ফ্যাক্টরি ক্লাস রাখুন, তবে আপনি এটির নিজস্ব ফাইলেও রাখতে পারেন।

  1. TasksViewModel ফাইলের নীচে, ক্লাসের বাইরে, একটি TasksViewModelFactory যোগ করুন যা একটি সাধারণ TasksRepository এ নেয়।

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


ViewModel কীভাবে তৈরি করা হয় তা আপনি পরিবর্তন করার আদর্শ উপায়। এখন যেহেতু আপনার কারখানা আছে, আপনি যেখানেই আপনার ভিউ মডেল তৈরি করেন সেখানেই এটি ব্যবহার করুন।

  1. ফ্যাক্টরি ব্যবহার করতে TasksFragment আপডেট করুন।

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. আপনার অ্যাপ কোড চালান এবং নিশ্চিত করুন যে সবকিছু এখনও কাজ করছে!

ধাপ 2. TasksViewModelTest-এর ভিতরে FakeTestRepository ব্যবহার করুন

এখন আপনার ভিউ মডেল পরীক্ষায় আসল সংগ্রহস্থল ব্যবহার করার পরিবর্তে, আপনি জাল সংগ্রহস্থল ব্যবহার করতে পারেন।

  1. TasksViewModelTest খুলুন
  2. TasksViewModelTest এ একটি FakeTestRepository সম্পত্তি যোগ করুন।

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. তিনটি কাজ সহ একটি FakeTestRepository তৈরি করতে setupViewModel পদ্ধতিটি আপডেট করুন এবং তারপর এই সংগ্রহস্থলের সাথে tasksViewModel তৈরি করুন।

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. যেহেতু আপনি আর AndroidX টেস্ট ApplicationProvider.getApplicationContext কোড ব্যবহার করছেন না, আপনি @RunWith(AndroidJUnit4::class) সরাতে পারেন।
  2. Run your tests, make sure they all still work!

By using constructor dependency injection, you've now removed the DefaultTasksRepository as a dependency and replaced it with your FakeTestRepository in the tests.

Step 3. Also Update TaskDetail Fragment and ViewModel

Make the exact same changes for the TaskDetailFragment and TaskDetailViewModel . This will prepare the code for when you write TaskDetail tests next.

  1. Open TaskDetailViewModel .
  2. Update the constructor:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. At the bottom of the TaskDetailViewModel file, outside the class, add a TaskDetailViewModelFactory .

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Update TasksFragment to use the factory.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Run your code and make sure everything is working.

You are now able to use a FakeTestRepository instead of the real repository in TasksFragment and TasksDetailFragment .

Next you'll write integration tests to test your fragment and view-model interactions. You'll find out if your view model code appropriately updates your UI. To do this you use

  • the ServiceLocator pattern
  • the Espresso and Mockito libraries

Integration tests test the interaction of several classes to make sure they behave as expected when used together. These tests can be run either locally ( test source set) or as instrumentation tests ( androidTest source set).

In your case you'll be taking each fragment and writing integration tests for the fragment and view model to test the main features of the fragment.

Step 1. Add Gradle Dependencies

  1. Add the following gradle dependencies.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

These dependencies include:

  • junit:junit —JUnit, which is necessary for writing basic test statements.
  • androidx.test:core —Core AndroidX test library
  • kotlinx-coroutines-test —The coroutines testing library
  • androidx.fragment:fragment-testing —AndroidX test library for creating fragments in tests and changing their state.

Since you'll be using these libraries in your androidTest source set, use androidTestImplementation to add them as dependencies.

Step 2. Make a TaskDetailFragmentTest class

The TaskDetailFragment shows information about a single task.

You'll start by writing a fragment test for the TaskDetailFragment since it has fairly basic functionality compared to the other fragments.

  1. Open taskdetail.TaskDetailFragment .
  2. Generate a test for TaskDetailFragment , as you've done before. Accept the default choices and put it in the androidTest source set (NOT the test source set).

  1. Add the following annotations to the TaskDetailFragmentTest class.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

The purpose of these annotation is:

Step 3. Launch a fragment from a test

In this task, you're going to launch TaskDetailFragment using the AndroidX Testing library . FragmentScenario is a class from AndroidX Test that wraps around a fragment and gives you direct control over the fragment's lifecycle for testing. To write tests for fragments, you create a FragmentScenario for the fragment you're testing ( TaskDetailFragment ).

  1. Copy this test into TaskDetailFragmentTest .

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

This code above:

This is not a finished test yet, because it's not asserting anything. For now, run the test and observe what happens.

  1. This is an instrumented test, so make sure the emulator or your device is visible.
  2. Run the test.

A few things should happen.

  • First, because this is an instrumented test, the test will run on either your physical device (if connected) or an emulator.
  • It should launch the fragment.
  • Notice how it doesn't navigate through any other fragment or have any menus associated with the activity - it is just the fragment.

Finally, look closely and notice that the fragment says "No data" as it doesn't successfully load up the task data.

Your test both needs to load up the TaskDetailFragment (which you've done) and assert the data was loaded correctly. Why is there no data? This is because you created a task, but you didn't save it to the repository.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

You have this FakeTestRepository , but you need some way to replace your real repository with your fake one for your fragment . You'll do this next!

In this task, you'll provide your fake repository to your fragment using a ServiceLocator . This will allow you to write your fragment and view model integration tests.

You can't use constructor dependency injection here, as you did before, when you needed to provide a dependency to the view model or repository. Constructor dependency injection requires that you construct the class. Fragments and activities are examples of classes that you don't construct and generally don't have access to the constructor of.

Since you don't construct the fragment, you can't use constructor dependency injection to swap the repository test double ( FakeTestRepository ) to the fragment. Instead, use the Service Locator pattern. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code. In the regular app code (the main source set), all of these dependencies are the regular app dependencies. For the tests, you modify the Service Locator to provide test double versions of the dependencies.

Not using Service Locator


Using a Service Locator

For this codelab app, do the following:

  1. Create a Service Locator class that is able to construct and store a repository. By default it constructs a "normal" repository.
  2. Refactor your code so that when you need a repository, use the Service Locator.
  3. In your testing class, call a method on the Service Locator which swaps out the "normal" repository with your test double.

Step 1. Create the ServiceLocator

Let's make a ServiceLocator class. It'll live in the main source set with the rest of the app code because it's used by the main application code.

Note: The ServiceLocator is a singleton, so use the Kotlin object keyword for the class.

  1. Create the file ServiceLocator.kt in the top level of the main source set.
  2. Define an object called ServiceLocator .
  3. Create database and repository instance variables and set both to null .
  4. Annotate the repository with @Volatile because it could get used by multiple threads ( @Volatile is explained in detail here ).

Your code should look as a shown below.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Right now the only thing your ServiceLocator needs to do is know how to return a TasksRepository . It'll return a pre-existing DefaultTasksRepository or make and return a new DefaultTasksRepository , if needed.

Define the following functions:

  1. provideTasksRepository —Either provides an already existing repository or creates a new one. This method should be synchronized on this to avoid, in situations with multiple threads running, ever accidentally creating two repository instances.
  2. createTasksRepository —Code for creating a new repository. Will call createTaskLocalDataSource and create a new TasksRemoteDataSource .
  3. createTaskLocalDataSource —Code for creating a new local data source. Will call createDataBase .
  4. createDataBase —Code for creating a new database.

The completed code is below.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Step 2. Use ServiceLocator in Application

You're going to make a change to your main application code (not your tests) so that you create the repository in one place, your ServiceLocator .

It's important that you only ever make one instance of the repository class. To ensure this, you'll use the Service locator in my Application class.

  1. At the top level of your package hierarchy, open TodoApplication and create a val for your repository and assign it a repository that is obtained using ServiceLocator.provideTaskRepository .

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Now that you have created a repository in the application, you can remove the old getRepository method in DefaultTasksRepository .

  1. Open DefaultTasksRepository and delete the companion object.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Now everywhere you were using getRepository , use the application's taskRepository instead. This ensures that instead of making the repository directly, you are getting whatever repository the ServiceLocator provided.

  1. Open TaskDetailFragement and find the call to getRepository at the top of the class.
  2. Replace this call with a call that gets the repository from TodoApplication .

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Do the same for TasksFragment .

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. For StatisticsViewModel and AddEditTaskViewModel , update the code that acquires the repository to use the repository from the TodoApplication .

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Run your application (not the test)!

Since you only refactored, the app should run the same without issue.

Step 3. Create FakeAndroidTestRepository

You already have a FakeTestRepository in the test source set. You cannot share test classes between the test and androidTest source sets by default. So, you need to make a duplicate FakeTestRepository class in the androidTest source set, and call it FakeAndroidTestRepository .

  1. Right-click the androidTest source set and make a data package. Right-click again and make a source package.
  2. Make a new class in this source package called FakeAndroidTestRepository.kt .
  3. Copy the following code to that class.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Step 4. Prepare your ServiceLocator for Tests

Okay, time to use the ServiceLocator to swap in test doubles when testing. To do that, you need to add some code to your ServiceLocator code.

  1. Open ServiceLocator.kt .
  2. Mark the setter for tasksRepository as @VisibleForTesting . This annotation is a way to express that the reason the setter is public is because of testing.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Whether you run your test alone or in a group of tests, your tests should run exactly the same. What this means is that your tests should have no behavior that is dependent on one another (which means avoiding sharing objects between tests).

Since the ServiceLocator is a singleton, it has the possibility of being accidentally shared between tests. To help avoid this, create a method that properly resets the ServiceLocator state between tests.

  1. Add an instance variable called lock with the Any value.

ServiceLocator.kt

private val lock = Any()
  1. Add a testing-specific method called resetRepository which clears out the database and sets both the repository and database to null.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Step 5. Use your ServiceLocator

In this step, you use the ServiceLocator .

  1. Open TaskDetailFragmentTest .
  2. Declare a lateinit TasksRepository variable.
  3. Add a setup and a tear down method to set up a FakeAndroidTestRepository before each test and clean it up after each test.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Wrap the function body of activeTaskDetails_DisplayedInUi() in runBlockingTest .
  2. Save activeTask in the repository before launching the fragment.
repository.saveTask(activeTask)

The final test looks like this code below.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Annotate the whole class with @ExperimentalCoroutinesApi .

When finished, the code will look like this.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Run the activeTaskDetails_DisplayedInUi() test.

Much like before, you should see the fragment, except this time, because you properly set up the repository, it now shows the task information.


In this step, you'll use the Espresso UI testing library to complete your first integration test. You have structured your code so you can add tests with assertions for your UI. To do that, you'll use the Espresso testing library .

Espresso helps you:

  • Interact with views, like clicking buttons, sliding a bar, or scrolling down a screen.
  • Assert that certain views are on screen or are in a certain state (such as containing particular text, or that a checkbox is checked, etc.).

Step 1. Note Gradle Dependency

You'll already have the main Espresso dependency since it is included in Android projects by default.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core —This core Espresso dependency is included by default when you make a new Android project. It contains the basic testing code for most views and actions on them.

Step 2. Turn off animations

Espresso tests run on a real device and thus are instrumentation tests by nature. One issue that arises is animations: If an animation lags and you try to test if a view is on screen, but it's still animating, Espresso can accidentally fail a test. This can make Espresso tests flaky.

For Espresso UI testing, it's best practice to turn animations off (also your test will run faster!):

  1. On your testing device, go to Settings > Developer options .
  2. Disable these three settings: Window animation scale , Transition animation scale , and Animator duration scale .

Step 3. Look at an Espresso test

Before you write an Espresso test, take a look at some Espresso code.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

What this statement does is find the checkbox view with the id task_detail_complete_checkbox , clicks it, then asserts that it is checked.

The majority of Espresso statements are made up of four parts:

1. Static Espresso method

onView

onView is an example of a static Espresso method that starts an Espresso statement. onView is one of the most common ones, but there are other options, such as onData .

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId is an example of a ViewMatcher which gets a view by its ID. There are other view matchers which you can look up in the documentation .

3. ViewAction

perform(click())

The perform method which takes a ViewAction . A ViewAction is something that can be done to the view, for example here, it's clicking the view.

4. ViewAssertion

check(matches(isChecked()))

check which takes a ViewAssertion . ViewAssertion s check or asserts something about the view. The most common ViewAssertion you'll use is the matches assertion. To finish the assertion, use another ViewMatcher , in this case isChecked .

Note that you don't always call both perform and check in an Espresso statement. You can have statements that just make an assertion using check or just do a ViewAction using perform .

  1. Open TaskDetailFragmentTest.kt .
  2. Update the activeTaskDetails_DisplayedInUi test.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Here are the import statements, if needed:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Everything after the // THEN comment uses Espresso. Examine the test structure and the use of withId and check to make assertions about how the detail page should look.
  2. Run the test and confirm it passes.

Step 4. Optional, Write your own Espresso Test

Now write a test yourself.

  1. Create a new test called completedTaskDetails_DisplayedInUi and copy this skeleton code.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Looking at the previous test, complete this test.
  2. Run and confirm the test passes.

The finished completedTaskDetails_DisplayedInUi should look like this code.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

In this last step you'll learn how to test the Navigation component , using a different type of test double called a mock, and the testing library Mockito .

In this codelab you've used a test double called a fake. Fakes are one of many types of test doubles. Which test double should you use for testing the Navigation component ?

Think about how navigation happens. Imagine pressing one of the tasks in the TasksFragment to navigate to a task detail screen.

Here's code in TasksFragment that navigates to a task detail screen when it is pressed.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


The navigation occurs because of a call to the navigate method. If you needed to write an assert statement, there isn't a straightforward way to test whether you've navigated to TaskDetailFragment . Navigating is a complicated action that doesn't result in a clear output or state change, beyond initializing TaskDetailFragment .

What you can assert is that the navigate method was called with the correct action parameter. This is exactly what a mock test double does—it checks whether specific methods were called.

Mockito is a framework for making test doubles. While the word mock is used in the API and name, it is not for just making mocks. It can also make stubs and spies.

You will be using Mockito to make a mock NavigationController which can assert that the navigate method was called correctly.

Step 1. Add Gradle Dependencies

  1. Add the gradle dependencies.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core —This is the Mockito dependency.
  • dexmaker-mockito —This library is required to use Mockito in an Android project. Mockito needs to generate classes at runtime. On Android, this is done using dex byte code, and so this library enables Mockito to generate objects during runtime on Android.
  • androidx.test.espresso:espresso-contrib —This library is made up of external contributions (hence the name) which contain testing code for more advanced views, such as DatePicker and RecyclerView . It also contains Accessibility checks and class called CountingIdlingResource that is covered later.

Step 2. Create TasksFragmentTest

  1. Open TasksFragment .
  2. Right-click on the TasksFragment class name and select Generate then Test . Create a test in the androidTest source set.
  3. Copy this code to the TasksFragmentTest .

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

This code looks similar to the TaskDetailFragmentTest code you wrote. It sets up and tears down a FakeAndroidTestRepository . Add a navigation test to test that when you click on a task in the task list, it takes you to the correct TaskDetailFragment .

  1. Add the test clickTask_navigateToDetailFragmentOne .

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Use Mockito's mock function to create a mock.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

To mock in Mockito, pass in the class you want to mock.

Next, you need to associate your NavController with the fragment. onFragment lets you call methods on the fragment itself.

  1. Make your new mock the fragment's NavController .
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Add the code to click on the item in the RecyclerView that has the text "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions is part of the espresso-contrib library and lets you perform Espresso actions on a RecyclerView .

  1. Verify that navigate was called, with the correct argument.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Mockito's verify method is what makes this a mock—you're able to confirm the mocked navController called a specific method ( navigate ) with a parameter ( actionTasksFragmentToTaskDetailFragment with the ID of "id1").

The complete test looks like this:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Run your test!

In summary, to test navigation you can:

  1. Use Mockito to create a NavController mock.
  2. Attach that mocked NavController to the fragment.
  3. Verify that navigate was called with the correct action and parameter(s).

Step 3. Optional, write clickAddTaskButton_navigateToAddEditFragment

To see if you can write a navigation test yourself, try this task.

  1. Write the test clickAddTaskButton_navigateToAddEditFragment which checks that if you click on the + FAB, you navigate to the AddEditTaskFragment .

The answer is below.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

Click here to see a diff between the code you started and the final code.

To download the code for the finished codelab, you can use the git command below:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


Alternatively you can download the repository as a Zip file, unzip it, and open it in Android Studio.

জিপ ডাউনলোড করুন

This codelab covered how to set up manual dependency injection, a service locator, and how to use fakes and mocks in your Android Kotlin apps. In particular:

  • What you want to test and your testing strategy determine the kinds of test you are going to implement for your app. Unit tests are focused and fast. Integration tests verify interaction between parts of your program. End-to-end tests verify features, have the highest fidelity, are often instrumented, and may take longer to run.
  • The architecture of your app influences how hard it is to test.
  • TDD or Test Driven Development is a strategy where you write the tests first, then create the feature to pass the tests.
  • To isolate parts of your app for testing, you can use test doubles. A test double is a version of a class crafted specifically for testing. For example, you fake getting data from a database or the internet.
  • Use dependency injection to replace a real class with a testing class, for example, a repository or a networking layer.
  • Use i nstrumented testing ( androidTest ) to launch UI components.
  • When you can't use constructor dependency injection, for example to launch a fragment, you can often use a service locator. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code.

Udacity course:

অ্যান্ড্রয়েড বিকাশকারী ডকুমেন্টেশন:

ভিডিও:

Other:

এই কোর্সে অন্যান্য কোডল্যাবগুলির লিঙ্কগুলির জন্য, কোটলিন কোডল্যাবগুলির ল্যান্ডিং পৃষ্ঠাতে উন্নত Android দেখুন৷