এই কোডল্যাবটি অ্যাডভান্সড অ্যান্ড্রয়েড ইন কোটলিন কোর্সের অংশ। আপনি যদি কোডল্যাবগুলি ক্রমানুসারে কাজ করেন তবে আপনি এই কোর্সের সর্বাধিক মূল্য পাবেন, তবে এটি বাধ্যতামূলক নয়৷ সমস্ত কোর্স কোডল্যাবগুলি কোটলিন কোডল্যাবস ল্যান্ডিং পৃষ্ঠায় অ্যাডভান্সড অ্যান্ড্রয়েডে তালিকাভুক্ত করা হয়েছে।
ভূমিকা
এই দ্বিতীয় টেস্টিং কোডল্যাবটি পরীক্ষার দ্বিগুণ সম্পর্কে: কখন এগুলিকে অ্যান্ড্রয়েডে ব্যবহার করতে হবে এবং নির্ভরতা ইনজেকশন, পরিষেবা লোকেটার প্যাটার্ন এবং লাইব্রেরিগুলি ব্যবহার করে কীভাবে সেগুলি প্রয়োগ করতে হবে৷ এটি করার সময়, আপনি কীভাবে লিখতে হয় তা শিখবেন:
- সংগ্রহস্থল ইউনিট পরীক্ষা
- টুকরো এবং ভিউ মডেল ইন্টিগ্রেশন পরীক্ষা
- খণ্ড নেভিগেশন পরীক্ষা
আপনি ইতিমধ্যে কি জানা উচিত
আপনার সাথে পরিচিত হওয়া উচিত:
- কোটলিন প্রোগ্রামিং ভাষা
- প্রথম কোডল্যাবে কভার করা পরীক্ষার ধারণাগুলি: অ্যান্ড্রয়েডে ইউনিট পরীক্ষা লেখা এবং চালানো, 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 অ্যাপটি জনপ্রিয় আর্কিটেকচার ব্লুপ্রিন্ট টেস্টিং এবং আর্কিটেকচার নমুনা (নমুনার প্রতিক্রিয়াশীল আর্কিটেকচার সংস্করণ ব্যবহার করে) এর উপর ভিত্তি করে তৈরি। অ্যাপটি একটি গাইড থেকে অ্যাপ আর্কিটেকচারের আর্কিটেকচার অনুসরণ করে। এটি ফ্র্যাগমেন্টস, একটি সংগ্রহস্থল এবং রুম সহ ভিউ মডেল ব্যবহার করে। আপনি যদি নীচের যেকোন উদাহরণের সাথে পরিচিত হন তবে এই অ্যাপটির একটি অনুরূপ আর্কিটেকচার রয়েছে:
- একটি ভিউ কোডল্যাব সহ রুম
- অ্যান্ড্রয়েড কোটলিন ফান্ডামেন্টাল ট্রেনিং কোডল্যাব
- উন্নত অ্যান্ড্রয়েড প্রশিক্ষণ কোডল্যাব
- অ্যান্ড্রয়েড সূর্যমুখী নমুনা
- Kotlin Udacity প্রশিক্ষণ কোর্সের সাথে Android Apps তৈরি করা
যেকোন একটি স্তরে যুক্তির গভীর বোঝার চেয়ে অ্যাপটির সাধারণ আর্কিটেকচার বোঝার চেয়ে এটি আরও গুরুত্বপূর্ণ।
এখানে আপনি যে প্যাকেজগুলি পাবেন তার সারাংশ:
প্যাকেজ: | |
| একটি টাস্ক স্ক্রীন যোগ বা সম্পাদনা করুন: একটি টাস্ক যোগ বা সম্পাদনা করার জন্য UI স্তর কোড। |
| ডাটা লেয়ার: এটি কাজের ডাটা লেয়ার নিয়ে কাজ করে। এতে ডাটাবেস, নেটওয়ার্ক এবং রিপোজিটরি কোড রয়েছে। |
| পরিসংখ্যান স্ক্রীন: পরিসংখ্যান পর্দার জন্য UI স্তর কোড। |
| টাস্ক ডিটেইল স্ক্রিন: একটি টাস্কের জন্য UI লেয়ার কোড। |
| টাস্ক স্ক্রিন: সমস্ত কাজের তালিকার জন্য UI লেয়ার কোড। |
| ইউটিলিটি ক্লাস: অ্যাপের বিভিন্ন অংশে ব্যবহৃত শেয়ার্ড ক্লাস, যেমন একাধিক স্ক্রিনে ব্যবহৃত সোয়াইপ রিফ্রেশ লেআউটের জন্য। |
ডেটা স্তর (.ডেটা)
এই অ্যাপটিতে একটি সিমুলেটেড নেটওয়ার্কিং স্তর রয়েছে, দূরবর্তী প্যাকেজে এবং স্থানীয় প্যাকেজে একটি ডাটাবেস স্তর রয়েছে৷ সরলতার জন্য, এই প্রজেক্টে নেটওয়ার্কিং লেয়ারটি প্রকৃত নেটওয়ার্ক অনুরোধ করার পরিবর্তে বিলম্ব সহ একটি HashMap
সাথে সিমুলেট করা হয়েছে।
DefaultTasksRepository
নেটওয়ার্কিং স্তর এবং ডাটাবেস স্তরের মধ্যে সমন্বয় বা মধ্যস্থতা করে এবং এটিই UI স্তরে ডেটা ফেরত দেয়।
UI স্তর (.addedittask, .statistics, .taskdetail, .tasks)
প্রতিটি UI স্তর প্যাকেজে একটি খণ্ড এবং একটি ভিউ মডেল রয়েছে, সাথে UI-এর জন্য প্রয়োজনীয় অন্যান্য ক্লাসের সাথে (যেমন টাস্ক তালিকার জন্য একটি অ্যাডাপ্টার)। TaskActivity
হল সেই ক্রিয়াকলাপ যাতে সমস্ত অংশ থাকে।
নেভিগেশন
অ্যাপের জন্য নেভিগেশন নেভিগেশন উপাদান দ্বারা নিয়ন্ত্রিত হয়। এটি nav_graph.xml
ফাইলে সংজ্ঞায়িত করা হয়েছে। Event
ক্লাস ব্যবহার করে ভিউ মডেলগুলিতে নেভিগেশন ট্রিগার করা হয়; ভিউ মডেলগুলিও নির্ধারণ করে যে কোন আর্গুমেন্টগুলি পাস করতে হবে। Event
ইভেন্টগুলি পর্যবেক্ষণ করে এবং পর্দার মধ্যে প্রকৃত নেভিগেশন করে।
এই কোডল্যাবে, আপনি শিখবেন কিভাবে রিপোজিটরি পরীক্ষা করতে হয়, মডেল দেখতে হয় এবং টেস্ট ডবলস এবং ডিপেন্ডেন্সি ইনজেকশন ব্যবহার করে টুকরোগুলো দেখতে হয়। সেগুলি কী তা নিয়ে আপনি ডুব দেওয়ার আগে, আপনি এই পরীক্ষাগুলি কী এবং কীভাবে লিখবেন তা নির্দেশ করবে এমন যুক্তি বোঝা গুরুত্বপূর্ণ।
এই বিভাগে সাধারণভাবে পরীক্ষার কিছু সর্বোত্তম অনুশীলন কভার করা হয়েছে, কারণ সেগুলি Android এ প্রযোজ্য।
টেস্টিং পিরামিড
একটি পরীক্ষার কৌশল সম্পর্কে চিন্তা করার সময়, তিনটি সম্পর্কিত পরীক্ষার দিক রয়েছে:
- ব্যাপ্তি — পরীক্ষার কতটা কোড স্পর্শ করে? পরীক্ষাগুলি একটি একক পদ্ধতিতে, সমগ্র অ্যাপ্লিকেশন জুড়ে বা এর মধ্যে কোথাও চলতে পারে।
- গতি - কত দ্রুত পরীক্ষা চালানো হয়? পরীক্ষার গতি মিলি-সেকেন্ড থেকে কয়েক মিনিট পর্যন্ত পরিবর্তিত হতে পারে।
- বিশ্বস্ততা - কীভাবে "বাস্তব-জগত" পরীক্ষা হয়? উদাহরণ স্বরূপ, আপনি যে কোডটি পরীক্ষা করছেন তার অংশ যদি একটি নেটওয়ার্ক অনুরোধ করার প্রয়োজন হয়, তাহলে পরীক্ষার কোডটি কি আসলেই এই নেটওয়ার্ক অনুরোধ করে, নাকি এটি ফলাফল জাল করে? যদি পরীক্ষাটি আসলে নেটওয়ার্কের সাথে কথা বলে, এর মানে হল এটির বিশ্বস্ততা বেশি। ট্রেড-অফ হল পরীক্ষাটি চালানোর জন্য বেশি সময় লাগতে পারে, নেটওয়ার্ক ডাউন থাকলে ত্রুটি হতে পারে বা ব্যবহার করা ব্যয়বহুল হতে পারে।
এই দিকগুলির মধ্যে সহজাত ট্রেড-অফ রয়েছে। উদাহরণস্বরূপ, গতি এবং বিশ্বস্ততা একটি ট্রেড-অফ—যত দ্রুত পরীক্ষা, সাধারণত, কম বিশ্বস্ততা এবং তদ্বিপরীত। স্বয়ংক্রিয় পরীক্ষাগুলিকে এই তিনটি বিভাগে ভাগ করার একটি সাধারণ উপায় হল:
- ইউনিট পরীক্ষা -এগুলি অত্যন্ত ফোকাসড পরীক্ষা যা একটি একক ক্লাসে চলে, সাধারণত সেই ক্লাসে একটি একক পদ্ধতি। যদি একটি ইউনিট পরীক্ষা ব্যর্থ হয়, আপনি আপনার কোডে সমস্যাটি ঠিক কোথায় তা জানতে পারবেন। বাস্তব বিশ্বে তাদের বিশ্বস্ততা কম, আপনার অ্যাপটি একটি পদ্ধতি বা শ্রেণির সম্পাদনের চেয়ে অনেক বেশি জড়িত। আপনি যখনই আপনার কোড পরিবর্তন করেন তখন তারা চালানোর জন্য যথেষ্ট দ্রুত। এগুলি প্রায়শই স্থানীয়ভাবে পরীক্ষা করা হবে (
test
উত্স সেটে)। উদাহরণ: ভিউ মডেল এবং সংগ্রহস্থলে একক পদ্ধতি পরীক্ষা করা। - ইন্টিগ্রেশন পরীক্ষা -এগুলি একসাথে ব্যবহার করার সময় তারা প্রত্যাশা অনুযায়ী আচরণ করে তা নিশ্চিত করতে বেশ কয়েকটি ক্লাসের মিথস্ক্রিয়া পরীক্ষা করে। ইন্টিগ্রেশন টেস্ট গঠনের একটি উপায় হল তাদের একটি একক বৈশিষ্ট্য পরীক্ষা করা, যেমন একটি টাস্ক সংরক্ষণ করার ক্ষমতা। তারা ইউনিট পরীক্ষার তুলনায় কোডের একটি বৃহত্তর সুযোগ পরীক্ষা করে, কিন্তু এখনও সম্পূর্ণ বিশ্বস্ততা থাকার বিপরীতে দ্রুত চালানোর জন্য অপ্টিমাইজ করা হয়। পরিস্থিতির উপর নির্ভর করে এগুলি স্থানীয়ভাবে বা যন্ত্র পরীক্ষা হিসাবে চালানো যেতে পারে। উদাহরণ: একটি একক খণ্ডের সমস্ত কার্যকারিতা পরীক্ষা করা এবং মডেল জোড়া দেখুন।
- এন্ড টু এন্ড টেস্ট (E2e) — একসাথে কাজ করা বৈশিষ্ট্যের সমন্বয় পরীক্ষা করুন। তারা অ্যাপের বড় অংশ পরীক্ষা করে, বাস্তব ব্যবহার ঘনিষ্ঠভাবে অনুকরণ করে এবং তাই সাধারণত ধীর হয়। তাদের সর্বোচ্চ বিশ্বস্ততা রয়েছে এবং আপনাকে বলে যে আপনার আবেদনটি আসলে সামগ্রিকভাবে কাজ করে। সর্বোপরি, এই পরীক্ষাগুলি হবে যন্ত্রযুক্ত পরীক্ষা (
androidTest
উত্স সেটে)
উদাহরণ: পুরো অ্যাপটি শুরু করা এবং কয়েকটি বৈশিষ্ট্য একসাথে পরীক্ষা করা।
এই পরীক্ষাগুলির প্রস্তাবিত অনুপাত প্রায়শই একটি পিরামিড দ্বারা প্রতিনিধিত্ব করা হয়, যার বেশিরভাগ পরীক্ষাই ইউনিট পরীক্ষা।
আর্কিটেকচার এবং টেস্টিং
টেস্টিং পিরামিডের বিভিন্ন স্তরে আপনার অ্যাপটি পরীক্ষা করার ক্ষমতা আপনার অ্যাপের আর্কিটেকচারের সাথে অন্তর্নিহিতভাবে আবদ্ধ। উদাহরণস্বরূপ, একটি অত্যন্ত দুর্বল-আর্কিটেক্ট অ্যাপ্লিকেশন তার সমস্ত যুক্তি একটি পদ্ধতির ভিতরে রাখতে পারে। আপনি এটির জন্য শেষ থেকে শেষ পরীক্ষা লিখতে সক্ষম হতে পারেন, যেহেতু এই পরীক্ষাগুলি অ্যাপের বড় অংশগুলি পরীক্ষা করে, কিন্তু ইউনিট বা ইন্টিগ্রেশন পরীক্ষা লেখার বিষয়ে কী? এক জায়গায় সমস্ত কোডের সাথে, শুধুমাত্র একটি একক বা বৈশিষ্ট্য সম্পর্কিত কোড পরীক্ষা করা কঠিন।
একটি ভাল পন্থা হবে অ্যাপ্লিকেশন লজিককে একাধিক পদ্ধতি এবং ক্লাসে ভেঙে ফেলা, প্রতিটি অংশকে বিচ্ছিন্নভাবে পরীক্ষা করার অনুমতি দেয়। আর্কিটেকচার হল আপনার কোডকে বিভক্ত ও সংগঠিত করার একটি উপায়, যা সহজে ইউনিট এবং ইন্টিগ্রেশন টেস্টিং করতে দেয়। আপনি যে TO-DO অ্যাপটি পরীক্ষা করবেন সেটি একটি নির্দিষ্ট আর্কিটেকচার অনুসরণ করে:
এই পাঠে, আপনি দেখতে পাবেন কীভাবে উপরের আর্কিটেকচারের অংশগুলি সঠিকভাবে বিচ্ছিন্নভাবে পরীক্ষা করা যায়:
- প্রথমে আপনি সংগ্রহস্থলটি ইউনিট পরীক্ষা করবেন।
- তারপর আপনি ভিউ মডেলে একটি টেস্ট ডাবল ব্যবহার করবেন, যা ভিউ মডেলের ইউনিট টেস্টিং এবং ইন্টিগ্রেশন পরীক্ষার জন্য প্রয়োজনীয়।
- এর পরে, আপনি টুকরো এবং তাদের ভিউ মডেলগুলির জন্য ইন্টিগ্রেশন পরীক্ষা লিখতে শিখবেন।
- অবশেষে, আপনি নেভিগেশন উপাদান অন্তর্ভুক্ত ইন্টিগ্রেশন পরীক্ষা লিখতে শিখবেন।
শেষ থেকে শেষ পরীক্ষা পরবর্তী পাঠে কভার করা হবে।
আপনি যখন একটি ক্লাসের একটি অংশের জন্য একটি ইউনিট পরীক্ষা লেখেন (একটি পদ্ধতি বা পদ্ধতির একটি ছোট সংগ্রহ), আপনার লক্ষ্য শুধুমাত্র সেই ক্লাসের কোডটি পরীক্ষা করা ।
একটি নির্দিষ্ট ক্লাস বা ক্লাসে শুধুমাত্র কোড পরীক্ষা করা কঠিন হতে পারে। এর একটি উদাহরণ তাকান. 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 টেস্ট ব্যবহার করা উচিত।
- কোডের কিছু অংশ, যেমন নেটওয়ার্কিং কোড, চালানোর জন্য দীর্ঘ সময় নিতে পারে, বা মাঝে মাঝে এমনকি ব্যর্থ হতে পারে, দীর্ঘক্ষণ চলমান, ফ্ল্যাকি পরীক্ষা তৈরি করে।
- আপনার পরীক্ষাগুলি পরীক্ষার ব্যর্থতার জন্য কোন কোডটি ত্রুটিযুক্ত তা নির্ণয় করার ক্ষমতা হারাতে পারে। আপনার পরীক্ষাগুলি নন-রিপোজিটরি কোড পরীক্ষা করা শুরু করতে পারে, তাই, উদাহরণস্বরূপ, আপনার অনুমিত "রিপোজিটরি" ইউনিট পরীক্ষাগুলি ডাটাবেস কোডের মতো কিছু নির্ভরশীল কোডে একটি সমস্যার কারণে ব্যর্থ হতে পারে।
টেস্ট ডাবলস
এর সমাধান হল আপনি যখন রিপোজিটরি পরীক্ষা করছেন, তখন প্রকৃত নেটওয়ার্কিং বা ডাটাবেস কোড ব্যবহার করবেন না , পরিবর্তে একটি টেস্ট ডাবল ব্যবহার করুন। একটি টেস্ট ডবল হল পরীক্ষার জন্য বিশেষভাবে তৈরি করা ক্লাসের একটি সংস্করণ। এটি পরীক্ষায় একটি ক্লাসের আসল সংস্করণ প্রতিস্থাপন করার জন্য বোঝানো হয়েছে। স্টান্ট ডবল একজন অভিনেতা যে স্টান্টে পারদর্শী এবং বিপজ্জনক ক্রিয়াকলাপের জন্য প্রকৃত অভিনেতাকে প্রতিস্থাপন করে তার সাথে এটি একই রকম।
এখানে কিছু ধরণের টেস্ট ডাবল রয়েছে:
নকল | একটি টেস্ট ডাবল যার ক্লাসের একটি "কাজ" বাস্তবায়ন রয়েছে, কিন্তু এটি এমনভাবে প্রয়োগ করা হয়েছে যা এটিকে পরীক্ষার জন্য ভাল করে তোলে কিন্তু উৎপাদনের জন্য অনুপযুক্ত। |
উপহাস | একটি টেস্ট ডবল যা ট্র্যাক করে তার কোন পদ্ধতিগুলিকে বলা হয়েছিল৷ তারপরে এটির পদ্ধতিগুলি সঠিকভাবে বলা হয়েছিল কিনা তার উপর নির্ভর করে এটি একটি পরীক্ষা পাস করে বা ব্যর্থ হয়। |
অসম্পূর্ণ | একটি পরীক্ষা দ্বিগুণ যাতে কোন যুক্তি নেই এবং শুধুমাত্র আপনি যা প্রোগ্রাম করেন তা ফেরত দেয়। একটি |
ডামি | একটি পরীক্ষা দ্বিগুণ যা চারপাশে পাস করা হয় কিন্তু ব্যবহার করা হয় না, যেমন আপনাকে শুধুমাত্র একটি প্যারামিটার হিসাবে এটি প্রদান করতে হবে। আপনার যদি একটি |
গুপ্তচর | একটি টেস্ট ডবল যা কিছু অতিরিক্ত তথ্যের ট্র্যাক রাখে; উদাহরণস্বরূপ, আপনি যদি একটি |
টেস্ট ডাবল সম্পর্কে আরও তথ্যের জন্য, টয়লেটে টেস্টিং দেখুন: আপনার টেস্ট ডাবলস জানুন ।
অ্যান্ড্রয়েডে ব্যবহৃত সবচেয়ে সাধারণ টেস্ট ডাবল হল ফেকস এবং মকস ।
এই টাস্কে, আপনি একটি FakeDataSource
টেস্ট তৈরি করতে যাচ্ছেন যা ইউনিট টেস্ট DefaultTasksRepository
প্রকৃত ডেটা উৎস থেকে ডিকপল করা হয়েছে।
ধাপ 1: FakeDataSource ক্লাস তৈরি করুন
এই ধাপে আপনি FakeDataSouce
নামক একটি ক্লাস তৈরি করতে যাচ্ছেন, যা একটি LocalDataSource
এবং RemoteDataSource
এর দ্বিগুণ পরীক্ষা হবে।
- পরীক্ষার উৎস সেটে, নতুন -> প্যাকেজ নির্বাচন করুন ডান ক্লিক করুন।
- ভিতরে একটি উৎস প্যাকেজ সহ একটি ডেটা প্যাকেজ তৈরি করুন।
- ডেটা/সোর্স প্যাকেজে
FakeDataSource
নামে একটি নতুন ক্লাস তৈরি করুন।
ধাপ 2: TasksDataSource ইন্টারফেস বাস্তবায়ন করুন
আপনার নতুন ক্লাস FakeDataSource
একটি টেস্ট ডাবল হিসাবে ব্যবহার করতে সক্ষম হতে, এটি অবশ্যই অন্যান্য ডেটা উত্সগুলি প্রতিস্থাপন করতে সক্ষম হবে। এই ডেটা উত্সগুলি হল TasksLocalDataSource
এবং TasksRemoteDataSource
।
- লক্ষ্য করুন কিভাবে এই দুটিই
TasksDataSource
ইন্টারফেস বাস্তবায়ন করে।
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
-
FakeDataSource
কার্যকর করুনTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android স্টুডিও অভিযোগ করবে যে আপনি TasksDataSource
জন্য প্রয়োজনীয় পদ্ধতি প্রয়োগ করেননি।
- দ্রুত-সমাধান মেনু ব্যবহার করুন এবং সদস্যদের বাস্তবায়ন নির্বাচন করুন।
- সমস্ত পদ্ধতি নির্বাচন করুন এবং ঠিক আছে টিপুন।
ধাপ 3: FakeDataSource এ getTasks পদ্ধতি প্রয়োগ করুন
FakeDataSource
হল একটি নির্দিষ্ট ধরনের টেস্ট ডবল যাকে জাল বলা হয়। একটি নকল হল একটি টেস্ট ডবল যার ক্লাসের "কাজ" বাস্তবায়ন রয়েছে, কিন্তু এটি এমনভাবে প্রয়োগ করা হয়েছে যা এটি পরীক্ষার জন্য ভাল কিন্তু উত্পাদনের জন্য অনুপযুক্ত। "ওয়ার্কিং" বাস্তবায়নের অর্থ হল ক্লাসটি ইনপুট দেওয়া বাস্তবসম্মত আউটপুট তৈরি করবে।
উদাহরণস্বরূপ, আপনার জাল ডেটা উত্স নেটওয়ার্কের সাথে সংযোগ করবে না বা একটি ডাটাবেসে কিছু সংরক্ষণ করবে না - পরিবর্তে এটি কেবল একটি ইন-মেমরি তালিকা ব্যবহার করবে৷ এটি "আপনার প্রত্যাশা অনুযায়ী কাজ করবে" যে পদ্ধতিতে কাজগুলি পেতে বা সংরক্ষণ করার জন্য প্রত্যাশিত ফলাফলগুলি ফিরে আসবে, তবে আপনি কখনই এই বাস্তবায়নটি উত্পাদনে ব্যবহার করতে পারবেন না, কারণ এটি সার্ভার বা ডাটাবেসে সংরক্ষিত নয়।
একটি FakeDataSource
- আপনি একটি বাস্তব ডাটাবেস বা নেটওয়ার্কের উপর নির্ভর করার প্রয়োজন ছাড়াই
DefaultTasksRepository
এ কোড পরীক্ষা করতে দেয়। - পরীক্ষার জন্য একটি "বাস্তব-পর্যাপ্ত" বাস্তবায়ন প্রদান করে।
-
FakeDataSource
কন্সট্রাক্টর পরিবর্তন করে একটিvar
তৈরি করতে হবে যার নামtasks
যা একটিMutableList<Task>?
একটি খালি পরিবর্তনযোগ্য তালিকার একটি ডিফল্ট মান সহ।
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
এটি একটি ডাটাবেস বা সার্ভার প্রতিক্রিয়া হিসাবে "জাল" কাজের তালিকা। আপাতত, লক্ষ্য হল সংগ্রহস্থলের getTasks
পদ্ধতি পরীক্ষা করা। এটি ডেটা উত্সের getTasks
, deleteAllTasks
এবং saveTask
পদ্ধতিগুলিকে কল করে৷
এই পদ্ধতিগুলির একটি জাল সংস্করণ লিখুন:
-
getTasks
লিখুন : যদিtasks
null
না হয়, একটিSuccess
ফলাফল ফেরত দিন। যদিtasks
null
হয়, একটিError
ফলাফল ফেরত দিন। -
deleteAllTasks
লিখুন: পরিবর্তনযোগ্য কাজের তালিকা সাফ করুন। -
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-এ কনস্ট্রাক্টর ডিপেন্ডেন্সি ইনজেকশন ব্যবহার করুন
-
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 }
- যেহেতু আপনি নির্ভরতা পাস করেছেন,
init
পদ্ধতিটি সরান। আপনাকে আর নির্ভরতা তৈরি করতে হবে না। - এছাড়াও পুরানো উদাহরণ ভেরিয়েবল মুছে দিন। আপনি কনস্ট্রাক্টরে তাদের সংজ্ঞায়িত করছেন:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- অবশেষে, নতুন কনস্ট্রাক্টর ব্যবহার করতে
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
পরীক্ষা করতে আপনার জাল ডেটা উৎস ব্যবহার করতে পারেন।
-
DefaultTasksRepository
ক্লাসের নামের উপর ডান-ক্লিক করুন এবং জেনারেট নির্বাচন করুন, তারপর পরীক্ষা করুন। - পরীক্ষার উৎস সেটে
DefaultTasksRepositoryTest
তৈরি করতে প্রম্পটগুলি অনুসরণ করুন। - আপনার নতুন
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 }
- তিনটি ভেরিয়েবল তৈরি করুন, দুটি
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
ব্যবহার করবে।
-
createRepository
নামে একটি পদ্ধতি তৈরি করুন এবং@Before
দিয়ে এটি টীকা করুন। -
remoteTasks
এবংlocalTasks
লিস্ট ব্যবহার করে আপনার জাল ডেটা সোর্স ইনস্ট্যান্টিয়েট করুন। - আপনার তৈরি করা দুটি জাল ডেটা উত্স এবং
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
পরীক্ষা লেখার সময়!
- সংগ্রহস্থলের
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 চালু করতে হবে৷ এর জন্য, আপনার একটি করুটিন সুযোগ প্রয়োজন। এই ত্রুটিটি সমাধান করার জন্য, আপনার পরীক্ষায় লঞ্চিং কোরোটিনগুলি পরিচালনা করার জন্য আপনাকে কিছু গ্রেডেল নির্ভরতা যুক্ত করতে হবে।
- টেস্ট ইমপ্লিমেন্টেশন ব্যবহার করে সেট করা পরীক্ষার উৎসে
testImplementation
পরীক্ষার জন্য প্রয়োজনীয় নির্ভরতা যোগ করুন।
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
সিঙ্ক করতে ভুলবেন না!
kotlinx-coroutines-test
হল coroutines টেস্ট লাইব্রেরি, বিশেষ করে coroutines পরীক্ষা করার জন্য। আপনার পরীক্ষা চালানোর জন্য, runBlockingTest
ফাংশনটি ব্যবহার করুন। এটি coroutines টেস্ট লাইব্রেরি দ্বারা প্রদত্ত একটি ফাংশন। এটি কোডের একটি ব্লক নেয় এবং তারপরে কোডের এই ব্লকটিকে একটি বিশেষ কোরাউটিন প্রসঙ্গে চালায় যা সিঙ্ক্রোনাসভাবে এবং অবিলম্বে চলে, যার অর্থ ক্রিয়াগুলি একটি নির্ধারক ক্রমে ঘটবে। এটি মূলত আপনার কোরোটিনগুলিকে নন-করোটিনের মতো চালায়, তাই এটি কোড পরীক্ষার জন্য বোঝানো হয়েছে।
আপনি যখন একটি suspend
ফাংশন কল করছেন তখন আপনার পরীক্ষার ক্লাসে runBlockingTest
ব্যবহার করুন। রানব্লকিংটেস্ট কীভাবে কাজ করে এবং এই সিরিজের পরবর্তী কোডল্যাবে কীভাবে runBlockingTest
পরীক্ষা করা যায় সে সম্পর্কে আপনি আরও শিখবেন।
- ক্লাসের উপরে
@ExperimentalCoroutinesApi
যোগ করুন। এটি প্রকাশ করে যে আপনি জানেন যে আপনি ক্লাসে একটি পরীক্ষামূলক coroutine api (runBlockingTest
) ব্যবহার করছেন। এটি ছাড়া, আপনি একটি সতর্কতা পাবেন। - আপনার
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))
}
}
- আপনার নতুন
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 পৃষ্ঠ) অন্তর্ভুক্ত করতে হবে।
-
DefaultTasksRepository
খুলুন এবং ক্লাসের নামের উপর ডান-ক্লিক করুন। তারপর রিফ্যাক্টর -> এক্সট্রাক্ট -> ইন্টারফেস নির্বাচন করুন।
- আলাদা ফাইল করার জন্য এক্সট্রাক্ট নির্বাচন করুন।
- এক্সট্র্যাক্ট ইন্টারফেস উইন্ডোতে, ইন্টারফেসের নামটি
TasksRepository
এ পরিবর্তন করুন। - মেম্বারস টু ফর্ম ইন্টারফেস বিভাগে, দুই সহচর সদস্য এবং ব্যক্তিগত পদ্ধতি ব্যতীত সমস্ত সদস্য পরীক্ষা করুন।
- রিফ্যাক্টরে ক্লিক করুন। নতুন
TasksRepository
ইন্টারফেস ডেটা/সোর্স প্যাকেজে উপস্থিত হওয়া উচিত।
এবং DefaultTasksRepository
এখন TasksRepository
প্রয়োগ করে।
- সবকিছু এখনও কার্যকরী ক্রমে আছে তা নিশ্চিত করতে আপনার অ্যাপটি চালান (পরীক্ষা নয়)।
ধাপ 2. FakeTestRepository তৈরি করুন
এখন আপনার ইন্টারফেস আছে, আপনি DefaultTaskRepository
টেস্ট ডবল তৈরি করতে পারেন।
- টেস্ট সোর্স সেটে, ডেটা/সোর্সে Kotlin ফাইল তৈরি করুন এবং
FakeTestRepository.kt
ক্লাস করুন এবংTasksRepository
ইন্টারফেস থেকে প্রসারিত করুন।
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
আপনাকে বলা হবে যে আপনাকে ইন্টারফেস পদ্ধতিগুলি বাস্তবায়ন করতে হবে।
- যতক্ষণ না আপনি পরামর্শ মেনুটি দেখতে পাচ্ছেন ততক্ষণ ত্রুটিটির উপরে হোভার করুন, তারপরে ক্লিক করুন এবং সদস্য প্রয়োগ করুন নির্বাচন করুন।
- সমস্ত পদ্ধতি নির্বাচন করুন এবং ঠিক আছে টিপুন।
ধাপ 3. FakeTestRepository পদ্ধতি প্রয়োগ করুন
আপনার কাছে এখন "বাস্তবায়িত নয়" পদ্ধতি সহ একটি FakeTestRepository
ক্লাস আছে। আপনি যেভাবে FakeDataSource
প্রয়োগ করেছেন তার অনুরূপ, FakeTestRepository
স্থানীয় এবং দূরবর্তী ডেটা উত্সগুলির মধ্যে একটি জটিল মধ্যস্থতার সাথে মোকাবিলা করার পরিবর্তে একটি ডেটা কাঠামো দ্বারা সমর্থন করা হবে৷
মনে রাখবেন যে আপনার FakeTestRepository
এর FakeDataSource
s বা এরকম কিছু ব্যবহার করার দরকার নেই; এটি শুধুমাত্র প্রদত্ত ইনপুট বাস্তবসম্মত জাল আউটপুট ফেরত প্রয়োজন. আপনি কাজের তালিকা সংরক্ষণ করতে একটি LinkedHashMap
এবং আপনার পর্যবেক্ষণযোগ্য কাজের জন্য একটি MutableLiveData
ব্যবহার করবেন।
-
FakeTestRepository
এ, একটিLinkedHashMap
ভেরিয়েবল যোগ করুন যা বর্তমান কাজের তালিকা এবং আপনার পর্যবেক্ষণযোগ্য কাজের জন্য একটিMutableLiveData
উপস্থাপন করে।
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
নিম্নলিখিত পদ্ধতিগুলি প্রয়োগ করুন:
-
getTasks
— এই পদ্ধতিটিtasksServiceData
নিতে হবে এবং এটিকেtasksServiceData.values.toList()
ব্যবহার করে একটি তালিকায় পরিণত করতে হবে এবং তারপর এটিকেSuccess
ফলাফল হিসাবে ফিরিয়ে দিতে হবে। -
refreshTasks
—getTasks()
দ্বারা যা ফেরত দেওয়া হয় তা হতেobservableTasks
টাস্কের মান আপডেট করে। -
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
বেশ কয়েকবার কল করতে পারেন, কিন্তু এটি সহজ করতে, বিশেষভাবে পরীক্ষার জন্য একটি সহায়ক পদ্ধতি যোগ করুন যা আপনাকে কাজগুলি যোগ করতে দেয়।
-
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
স্ক্রিনের সাথে সম্পর্কিত ক্লাস এবং পরীক্ষা আপডেট করে শুরু করুন।
-
TasksViewModel
খুলুন । -
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
এর মতো একই ফাইলে ফ্যাক্টরি ক্লাস রাখুন, তবে আপনি এটির নিজস্ব ফাইলেও রাখতে পারেন।
-
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
কীভাবে তৈরি করা হয় তা আপনি পরিবর্তন করার আদর্শ উপায়। এখন যেহেতু আপনার কারখানা আছে, আপনি যেখানেই আপনার ভিউ মডেল তৈরি করেন সেখানেই এটি ব্যবহার করুন।
- ফ্যাক্টরি ব্যবহার করতে
TasksFragment
আপডেট করুন।
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- আপনার অ্যাপ কোড চালান এবং নিশ্চিত করুন যে সবকিছু এখনও কাজ করছে!
ধাপ 2. TasksViewModelTest-এর ভিতরে FakeTestRepository ব্যবহার করুন
এখন আপনার ভিউ মডেল পরীক্ষায় আসল সংগ্রহস্থল ব্যবহার করার পরিবর্তে, আপনি জাল সংগ্রহস্থল ব্যবহার করতে পারেন।
-
TasksViewModelTest
খুলুন । -
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
}
- তিনটি কাজ সহ একটি
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)
}
- যেহেতু আপনি আর AndroidX টেস্ট
ApplicationProvider.getApplicationContext
কোড ব্যবহার করছেন না, আপনি@RunWith(AndroidJUnit4::class)
সরাতে পারেন। - 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.
- Open
TaskDetailViewModel
. - 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 }
- At the bottom of the
TaskDetailViewModel
file, outside the class, add aTaskDetailViewModelFactory
.
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)
}
- 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))
}
- 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
- 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.
- Open
taskdetail.TaskDetailFragment
. - Generate a test for
TaskDetailFragment
, as you've done before. Accept the default choices and put it in the androidTest source set (NOT thetest
source set).
- Add the following annotations to the
TaskDetailFragmentTest
class.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
The purpose of these annotation is:
-
@MediumTest
—Marks the test as a "medium run-time" integration test (versus@SmallTest
unit tests and@LargeTest
large end-to-end tests). This helps you group and choose which size of test to run. -
@RunWith(AndroidJUnit4::class)
—Used in any class using AndroidX Test.
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
).
- 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:
- Creates a task.
- Creates a
Bundle
, which represents the fragment arguments for the task that get passed into the fragment). - The
launchFragmentInContainer
function creates aFragmentScenario
, with this bundle and a theme.
This is not a finished test yet, because it's not asserting anything. For now, run the test and observe what happens.
- This is an instrumented test, so make sure the emulator or your device is visible.
- 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:
- Create a Service Locator class that is able to construct and store a repository. By default it constructs a "normal" repository.
- Refactor your code so that when you need a repository, use the Service Locator.
- 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.
- Create the file ServiceLocator.kt in the top level of the main source set.
- Define an
object
calledServiceLocator
. - Create
database
andrepository
instance variables and set both tonull
. - 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:
-
provideTasksRepository
—Either provides an already existing repository or creates a new one. This method should besynchronized
onthis
to avoid, in situations with multiple threads running, ever accidentally creating two repository instances. -
createTasksRepository
—Code for creating a new repository. Will callcreateTaskLocalDataSource
and create a newTasksRemoteDataSource
. -
createTaskLocalDataSource
—Code for creating a new local data source. Will callcreateDataBase
. -
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.
- At the top level of your package hierarchy, open
TodoApplication
and create aval
for your repository and assign it a repository that is obtained usingServiceLocator.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
.
- 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.
- Open
TaskDetailFragement
and find the call togetRepository
at the top of the class. - 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)
}
- 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)
}
- For
StatisticsViewModel
andAddEditTaskViewModel
, update the code that acquires the repository to use the repository from theTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 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
.
- Right-click the
androidTest
source set and make a data package. Right-click again and make a source package. - Make a new class in this source package called
FakeAndroidTestRepository.kt
. - 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.
- Open
ServiceLocator.kt
. - 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.
- Add an instance variable called
lock
with theAny
value.
ServiceLocator.kt
private val lock = Any()
- 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
.
- Open
TaskDetailFragmentTest
. - Declare a
lateinit TasksRepository
variable. - 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()
}
- Wrap the function body of
activeTaskDetails_DisplayedInUi()
inrunBlockingTest
. - 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)
}
- 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)
}
}
- 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!):
- On your testing device, go to Settings > Developer options .
- 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:
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.
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
.
- Open
TaskDetailFragmentTest.kt
. - 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
- Everything after the
// THEN
comment uses Espresso. Examine the test structure and the use ofwithId
and check to make assertions about how the detail page should look. - Run the test and confirm it passes.
Step 4. Optional, Write your own Espresso Test
Now write a test yourself.
- 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
}
- Looking at the previous test, complete this test.
- 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
- 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 asDatePicker
andRecyclerView
. It also contains Accessibility checks and class calledCountingIdlingResource
that is covered later.
Step 2. Create TasksFragmentTest
- Open
TasksFragment
. - Right-click on the
TasksFragment
class name and select Generate then Test . Create a test in the androidTest source set. - 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
.
- 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)
}
- 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.
- Make your new mock the fragment's
NavController
.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- 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 .
- 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")
)
}
- Run your test!
In summary, to test navigation you can:
- Use Mockito to create a
NavController
mock. - Attach that mocked
NavController
to the fragment. - 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.
- Write the test
clickAddTaskButton_navigateToAddEditFragment
which checks that if you click on the + FAB, you navigate to theAddEditTaskFragment
.
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:
অ্যান্ড্রয়েড বিকাশকারী ডকুমেন্টেশন:
- Guide to app architecture
-
runBlocking
andrunBlockingTest
-
FragmentScenario
- Espresso
- Mockito
- JUnit4
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- Source sets
- Test from the command line
ভিডিও:
Other:
এই কোর্সে অন্যান্য কোডল্যাবগুলির লিঙ্কগুলির জন্য, কোটলিন কোডল্যাবগুলির ল্যান্ডিং পৃষ্ঠাতে উন্নত Android দেখুন৷