Pengujian Menjadi Lebih Mudah Melalui Kerangka Minimalis Dan Arsitektur Perangkat Lunak

Diterbitkan: 2022-03-10
Ringkasan cepat Seperti banyak topik lain dalam pengembangan perangkat lunak, pengujian dan pengembangan yang didorong oleh pengujian sering kali dibuat rumit secara teori dan implementasi dengan terlalu menekankan pada mempelajari beragam kerangka kerja pengujian. Dalam artikel ini, kita akan meninjau kembali apa yang dimaksud pengujian dengan analogi sederhana, mengeksplorasi konsep dalam arsitektur perangkat lunak yang secara langsung akan menghasilkan pengurangan kebutuhan untuk kerangka pengujian, dan beberapa argumen mengapa Anda mungkin mendapat manfaat dari sikap minimalisme untuk proses pengujian Anda. .

Seperti banyak pengembang Android lainnya, percobaan awal saya dalam pengujian pada platform membuat saya segera dihadapkan dengan tingkat jargon yang menurunkan moral. Lebih lanjut, beberapa contoh yang saya temukan pada saat itu (sekitar 2015) tidak menyajikan kasus penggunaan praktis yang mungkin membuat saya berpikir bahwa rasio biaya terhadap manfaat mempelajari alat seperti Espresso untuk memverifikasi bahwa TextView.setText( ...) bekerja dengan baik, adalah investasi yang masuk akal.

Lebih buruk lagi, saya tidak memiliki pemahaman yang baik tentang arsitektur perangkat lunak dalam teori atau praktik, yang berarti bahwa bahkan jika saya repot-repot mempelajari kerangka kerja ini, saya akan menulis tes untuk aplikasi monolitik yang terdiri dari beberapa kelas god , ditulis dalam kode spageti . Intinya adalah bahwa membangun, menguji, dan memelihara aplikasi semacam itu adalah latihan dalam sabotase diri terlepas dari keahlian kerangka kerja Anda; namun realisasi ini baru menjadi jelas setelah seseorang membangun aplikasi modular , loosely-coupled , dan sangat kohesif .

Dari sini kita sampai pada salah satu poin utama diskusi dalam artikel ini, yang akan saya rangkum dalam bahasa sederhana di sini: Di ​​antara manfaat utama penerapan prinsip-prinsip emas arsitektur perangkat lunak (jangan khawatir, saya akan membahasnya dengan contoh sederhana dan bahasa), adalah bahwa kode Anda dapat menjadi lebih mudah untuk diuji. Ada manfaat lain untuk menerapkan prinsip-prinsip tersebut, tetapi hubungan antara arsitektur perangkat lunak dan pengujian adalah fokus dari artikel ini.

Namun, demi mereka yang ingin memahami mengapa dan bagaimana kami menguji kode kami, pertama-tama kami akan mengeksplorasi konsep pengujian dengan analogi; tanpa mengharuskan Anda untuk menghafal jargon apapun. Sebelum masuk lebih dalam ke topik utama, kita juga akan melihat pertanyaan mengapa begitu banyak kerangka pengujian ada, karena dalam memeriksa ini kita mungkin mulai melihat manfaat, keterbatasan, dan bahkan mungkin solusi alternatifnya.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Pengujian: Mengapa Dan Bagaimana

Bagian ini bukanlah informasi baru bagi penguji berpengalaman, tetapi mungkin Anda tetap menikmati analogi ini. Tentu saja saya seorang insinyur perangkat lunak, bukan seorang insinyur roket, tetapi sejenak saya akan meminjam analogi yang berkaitan dengan merancang dan membangun objek baik di ruang fisik, maupun di ruang memori komputer. Ternyata sementara media berubah, prosesnya pada prinsipnya sama.

Misalkan sejenak kita adalah insinyur roket, dan tugas kita adalah membangun pendorong roket tahap pertama* dari pesawat ulang-alik. Misalkan juga, bahwa kita telah membuat desain yang dapat diservis untuk tahap pertama untuk mulai membangun dan menguji dalam berbagai kondisi.

"Tahap pertama" mengacu pada booster yang ditembakkan saat roket pertama kali diluncurkan

Sebelum kita masuk ke prosesnya, saya ingin menunjukkan mengapa saya lebih memilih analogi ini: Anda seharusnya tidak mengalami kesulitan menjawab pertanyaan mengapa kita repot-repot menguji desain kita sebelum menempatkannya dalam situasi di mana nyawa manusia dipertaruhkan. Meskipun saya tidak akan mencoba meyakinkan Anda bahwa menguji aplikasi Anda sebelum peluncuran dapat menyelamatkan nyawa (walaupun mungkin tergantung pada sifat aplikasi), itu dapat menghemat peringkat, ulasan, dan pekerjaan Anda. Dalam arti luas, pengujian adalah cara di mana kami memastikan bahwa satu bagian, beberapa komponen, dan keseluruhan sistem bekerja sebelum kami menggunakannya dalam situasi di mana sangat penting bagi mereka untuk tidak gagal.

Kembali ke aspek bagaimana analogi ini, saya akan memperkenalkan proses yang digunakan para insinyur untuk menguji desain tertentu: redundansi . Redundansi pada prinsipnya sederhana: Buat salinan komponen yang akan diuji dengan spesifikasi desain yang sama seperti yang ingin Anda gunakan pada waktu peluncuran. Uji salinan ini di lingkungan yang terisolasi yang secara ketat mengontrol prasyarat dan variabel. Meskipun ini tidak menjamin bahwa pendorong roket akan bekerja dengan baik ketika terintegrasi di seluruh pesawat ulang-alik, dapat dipastikan bahwa jika tidak bekerja di lingkungan yang terkendali, itu akan sangat tidak mungkin untuk bekerja sama sekali.

Misalkan dari ratusan, atau mungkin ribuan variabel yang salinan desain roketnya telah diuji, itu turun ke suhu sekitar di mana pendorong roket akan diuji tembak. Setelah pengujian pada 35 ° Celcius, kami melihat bahwa semuanya berfungsi tanpa kesalahan. Sekali lagi, roket diuji pada suhu kamar secara kasar tanpa kegagalan. Tes terakhir akan dilakukan pada suhu terendah yang tercatat untuk lokasi peluncuran, pada -5 ° Celcius. Selama tes terakhir ini, roket menembak, tetapi setelah beberapa saat, roket menyala dan tak lama kemudian meledak dengan hebat; tapi untungnya di lingkungan yang terkendali dan aman.

Pada titik ini, kita tahu bahwa perubahan suhu tampaknya paling tidak terlibat dalam pengujian yang gagal, yang mengarahkan kita untuk mempertimbangkan bagian pendorong roket mana yang mungkin terpengaruh oleh suhu dingin. Seiring waktu, ditemukan bahwa salah satu komponen kunci, cincin-O karet yang berfungsi untuk menahan aliran bahan bakar dari satu kompartemen ke kompartemen lain, menjadi kaku dan tidak efektif saat terkena suhu mendekati atau di bawah titik beku.

Mungkin Anda telah memperhatikan bahwa analoginya secara longgar didasarkan pada peristiwa tragis bencana pesawat ulang-alik Challenger . Bagi mereka yang tidak terbiasa, kebenaran yang menyedihkan (sejauh penyelidikan menyimpulkan) adalah bahwa ada banyak tes dan peringatan yang gagal dari para insinyur, namun masalah administrasi dan politik mendorong peluncuran untuk melanjutkan. Bagaimanapun, apakah Anda telah menghafal istilah redundansi atau tidak, harapan saya adalah Anda telah memahami proses dasar untuk menguji bagian-bagian dari segala jenis sistem.

Tentang Perangkat Lunak

Sedangkan analogi sebelumnya menjelaskan proses dasar untuk pengujian roket (sambil mengambil banyak kebebasan dengan rincian yang lebih halus), sekarang saya akan meringkas dengan cara yang mungkin lebih relevan untuk Anda dan saya. Meskipun dimungkinkan untuk menguji perangkat lunak hanya dengan meluncurkan ke perangkat setelah berada dalam keadaan yang dapat digunakan apa pun, saya kira sebagai gantinya kita dapat menerapkan prinsip redundansi ke masing-masing bagian aplikasi terlebih dahulu.

Ini berarti bahwa kita membuat salinan dari bagian yang lebih kecil dari keseluruhan aplikasi (biasanya disebut sebagai Unit perangkat lunak), menyiapkan lingkungan pengujian yang terisolasi, dan melihat bagaimana mereka berperilaku berdasarkan variabel, argumen, peristiwa, dan respons apa pun yang mungkin terjadi. saat berjalan. Pengujian benar-benar sesederhana itu dalam teori, tetapi kunci untuk mencapai proses ini terletak pada membangun aplikasi yang layak untuk diuji. Ini bermuara pada dua masalah yang akan kita lihat dalam dua bagian berikutnya. Perhatian pertama berkaitan dengan lingkungan pengujian , dan perhatian kedua berkaitan dengan cara kita menyusun aplikasi.

Mengapa Kita Membutuhkan Kerangka?

Untuk menguji bagian dari perangkat lunak (selanjutnya disebut sebagai Unit , meskipun definisi ini sengaja dibuat terlalu menyederhanakan), perlu untuk memiliki beberapa jenis lingkungan pengujian yang memungkinkan Anda untuk berinteraksi dengan perangkat lunak Anda saat runtime. Untuk membangun aplikasi yang akan dieksekusi murni pada lingkungan JVM ( Java Virtual Machine ), semua yang diperlukan untuk menulis tes adalah JRE ( Java Runtime Environment ). Ambil contoh kelas Kalkulator yang sangat sederhana ini:

 class Calculator { private int add(int a, int b){ return a + b; } private int subtract(int a, int b){ return a - b; } }

Dengan tidak adanya kerangka kerja apa pun, selama kita memiliki kelas pengujian yang berisi fungsi main untuk benar-benar mengeksekusi kode kita, kita dapat mengujinya. Seperti yang Anda ingat, fungsi main menunjukkan titik awal eksekusi untuk program Java sederhana. Adapun apa yang kami uji, kami cukup memasukkan beberapa data uji ke dalam fungsi Kalkulator dan memverifikasi bahwa itu melakukan aritmatika dasar dengan benar:

 public class Main { public static void main(String[] args){ //create a copy of the Unit to be tested Calculator calc = new Calculator(); //create test conditions to verify behaviour int addTest = calc.add(2, 2); int subtractTest = calc.subtract(2, 2); //verify behaviour by assertion if (addTest == 4) System.out.println("addTest has passed."); else System.out.println("addTest has failed."); if (subtractTest == 0) System.out.println("subtractTest has passed."); else System.out.println("subtractTest has failed."); } }

Menguji aplikasi Android tentu saja merupakan prosedur yang sama sekali berbeda. Meskipun ada fungsi main yang terkubur jauh di dalam sumber file ZygoteInit.java (detail yang lebih baik tidak penting di sini), yang dipanggil sebelum aplikasi Android diluncurkan di JVM , bahkan pengembang Android junior harus tahu bahwa sistem itu sendiri bertanggung jawab untuk memanggil fungsi ini; bukan pengembang . Sebagai gantinya, titik masuk untuk aplikasi Android adalah kelas Application , dan kelas Activity apa pun yang dapat diarahkan oleh sistem melalui file AndroidManifest.xml .

Semua ini hanya mengarah pada fakta bahwa pengujian Unit dalam aplikasi Android menghadirkan tingkat kerumitan yang lebih besar, karena lingkungan pengujian kami sekarang harus memperhitungkan platform Android.

Menjinakkan Masalah Kopling Ketat

Kopling ketat adalah istilah yang menggambarkan fungsi, kelas, atau modul aplikasi yang bergantung pada platform, kerangka kerja, bahasa, dan pustaka tertentu. Ini adalah istilah relatif, yang berarti bahwa contoh Calculator.java kami digabungkan erat dengan bahasa pemrograman Java dan pustaka standar, tetapi itu adalah tingkat penggabungannya. Sejalan dengan itu, masalah pengujian kelas yang digabungkan erat ke platform Android, adalah Anda harus menemukan cara untuk bekerja dengan atau di sekitar platform.

Untuk kelas yang digabungkan erat ke platform Android, Anda memiliki dua opsi. Yang pertama, cukup menyebarkan kelas Anda ke perangkat Android (fisik atau virtual). Meskipun saya menyarankan agar Anda menguji penerapan kode aplikasi Anda sebelum mengirimkannya ke produksi, ini adalah pendekatan yang sangat tidak efisien selama tahap awal dan tengah proses pengembangan sehubungan dengan waktu.

Unit , bagaimanapun teknis definisi yang Anda sukai, umumnya dianggap sebagai fungsi tunggal di kelas (walaupun beberapa memperluas definisi untuk menyertakan fungsi pembantu berikutnya yang dipanggil secara internal oleh panggilan fungsi tunggal awal). Either way, Unit dimaksudkan untuk menjadi kecil; membangun, mengompilasi, dan menerapkan seluruh aplikasi untuk menguji satu Unit sama sekali tidak berarti menguji secara terpisah .

Solusi lain untuk masalah kopling ketat, adalah dengan menggunakan kerangka kerja pengujian untuk berinteraksi dengan, atau tiruan (mensimulasikan) dependensi platform. Kerangka kerja seperti Espresso dan Robolectric memberi pengembang cara yang jauh lebih efektif untuk menguji Unit daripada pendekatan sebelumnya; yang pertama berguna untuk pengujian yang dijalankan pada perangkat (dikenal sebagai "tes berinstrumen" karena tampaknya menyebutnya sebagai pengujian perangkat tidak cukup ambigu) dan yang terakhir mampu mengejek kerangka kerja Android secara lokal pada JVM.

Sebelum saya melanjutkan ke pagar terhadap kerangka kerja tersebut alih-alih alternatif yang akan saya bahas segera, saya ingin menjelaskan bahwa saya tidak bermaksud menyiratkan bahwa Anda tidak boleh menggunakan opsi ini. Proses yang digunakan pengembang untuk membangun dan menguji aplikasi mereka harus lahir dari kombinasi preferensi pribadi dan efisiensi.

Bagi mereka yang tidak suka membangun aplikasi modular dan digabungkan secara longgar, Anda tidak akan punya pilihan selain menjadi akrab dengan kerangka kerja ini jika Anda ingin memiliki tingkat cakupan pengujian yang memadai. Banyak aplikasi luar biasa telah dibangun dengan cara ini, dan saya tidak jarang dituduh membuat aplikasi saya terlalu modular dan abstrak. Apakah Anda mengambil pendekatan saya atau memutuskan untuk sangat bergantung pada kerangka kerja, saya salut Anda telah meluangkan waktu dan upaya untuk menguji aplikasi Anda.

Pertahankan Kerangka Anda Di Lengan Panjang

Untuk pembukaan terakhir dari pelajaran inti artikel ini, ada baiknya mendiskusikan mengapa Anda mungkin ingin memiliki sikap minimalisme dalam hal menggunakan kerangka kerja (dan ini berlaku untuk lebih dari sekadar kerangka pengujian). Subjudul di atas adalah parafrase dari guru praktik terbaik perangkat lunak yang murah hati: Robert “Paman Bob” C. Martin. Dari sekian banyak permata yang telah dia berikan kepada saya sejak saya pertama kali mempelajari karya-karyanya, yang satu ini membutuhkan beberapa tahun pengalaman langsung untuk dipahami.

Sejauh yang saya pahami tentang pernyataan ini, biaya penggunaan kerangka kerja adalah investasi waktu yang diperlukan untuk mempelajari dan memeliharanya. Beberapa dari mereka cukup sering berubah dan beberapa dari mereka tidak cukup sering berubah. Fungsi menjadi tidak digunakan lagi, kerangka kerja berhenti dipertahankan, dan setiap 6-24 bulan kerangka kerja baru tiba untuk menggantikan yang terakhir. Oleh karena itu, jika Anda dapat menemukan solusi yang dapat diimplementasikan sebagai platform atau fitur bahasa (yang cenderung bertahan lebih lama), itu akan cenderung lebih tahan terhadap perubahan dari berbagai jenis yang disebutkan di atas.

Pada catatan yang lebih teknis, kerangka kerja seperti Espresso dan pada tingkat yang lebih rendah Robolectric , tidak pernah dapat berjalan seefisien pengujian JUnit sederhana, atau bahkan pengujian kerangka kerja gratis sejak awal. Meskipun JUnit memang sebuah kerangka kerja, JUnit sangat terkait dengan JVM , yang cenderung berubah pada tingkat yang jauh lebih lambat daripada platform Android yang sebenarnya. Kerangka kerja yang lebih sedikit hampir selalu berarti kode yang lebih efisien dalam hal waktu yang dibutuhkan untuk mengeksekusi dan menulis satu atau lebih tes.

Dari sini, Anda mungkin dapat menyimpulkan bahwa kita sekarang akan membahas pendekatan yang akan memanfaatkan beberapa teknik yang memungkinkan kita untuk menjaga platform Android tetap panjang; sementara itu memungkinkan kami banyak cakupan kode, efisiensi pengujian, dan kesempatan untuk tetap menggunakan kerangka kerja di sana-sini saat diperlukan.

Seni Arsitektur

Untuk menggunakan analogi yang konyol, orang mungkin menganggap kerangka kerja dan platform seperti rekan kerja yang sombong yang akan mengambil alih proses pengembangan Anda kecuali Anda menetapkan batasan yang sesuai dengan mereka. Prinsip-prinsip emas arsitektur perangkat lunak dapat memberi Anda konsep umum dan teknik khusus yang diperlukan untuk membuat dan menegakkan batas-batas ini. Seperti yang akan kita lihat sebentar lagi, jika Anda pernah bertanya-tanya apa manfaat menerapkan prinsip-prinsip arsitektur perangkat lunak dalam kode Anda sebenarnya, beberapa secara langsung, dan banyak secara tidak langsung membuat kode Anda lebih mudah untuk diuji.

Pemisahan Kekhawatiran

Separation Of Concerns menurut perkiraan saya adalah konsep yang paling dapat diterapkan secara universal dan berguna dalam arsitektur perangkat lunak secara keseluruhan (tanpa bermaksud mengatakan bahwa yang lain harus diabaikan). Pemisahan kekhawatiran (SOC) dapat diterapkan, atau sepenuhnya diabaikan, di setiap perspektif pengembangan perangkat lunak yang saya ketahui. Untuk meringkas konsep secara singkat, kita akan melihat SOC ketika diterapkan ke kelas, tetapi perlu diketahui bahwa SOC dapat diterapkan ke fungsi melalui penggunaan ekstensif dari fungsi pembantu , dan dapat diekstrapolasi ke seluruh modul aplikasi (“modul” yang digunakan dalam konteks Android/Gradle).

Jika Anda telah menghabiskan banyak waktu untuk meneliti pola arsitektur perangkat lunak untuk aplikasi GUI, Anda mungkin akan menemukan setidaknya satu dari: Model-View-Controller (MVC), Model-View-Presenter (MVP), atau Model-View- ViewModel (MVVM). Setelah membangun aplikasi dalam setiap gaya, saya akan mengatakan di muka bahwa saya tidak menganggap salah satu dari mereka sebagai pilihan terbaik untuk semua proyek (atau bahkan fitur dalam satu proyek). Ironisnya, pola yang disajikan oleh tim Android beberapa tahun lalu sebagai pendekatan yang direkomendasikan, MVVM, tampaknya paling tidak dapat diuji tanpa adanya kerangka kerja pengujian khusus Android (dengan asumsi Anda ingin menggunakan kelas ViewModel platform Android, yang saya akui adalah penggemarnya. dari).

Bagaimanapun, kekhususan pola-pola ini kurang penting daripada umumnya. Semua pola ini hanyalah rasa berbeda dari SOC yang menekankan pemisahan mendasar dari tiga jenis kode yang saya sebut sebagai: Data , User Interface , Logic .

Jadi, bagaimana tepatnya memisahkan Data , Antarmuka Pengguna , dan Logika membantu Anda menguji aplikasi? Jawabannya adalah dengan menarik logika keluar dari kelas yang harus berurusan dengan dependensi platform/kerangka ke dalam kelas yang memiliki sedikit atau tidak ada dependensi platform/kerangka, pengujian menjadi mudah dan kerangka kerja minimal . Untuk lebih jelasnya, saya biasanya berbicara tentang kelas yang harus membuat antarmuka pengguna, menyimpan data dalam tabel SQL, atau terhubung ke server jauh. Untuk mendemonstrasikan cara kerjanya, mari kita lihat arsitektur tiga lapis yang disederhanakan dari aplikasi Android hipotetis.

Kelas pertama akan mengelola antarmuka pengguna kami. Untuk menjaga semuanya tetap sederhana, saya telah menggunakan Aktivitas untuk tujuan ini, tetapi saya biasanya memilih Fragmen sebagai gantinya sebagai kelas antarmuka pengguna. Dalam kedua kasus tersebut, kedua kelas menghadirkan kopling ketat yang serupa dengan platform Android :

 public class CalculatorUserInterface extends Activity implements CalculatorContract.IUserInterface { private TextView display; private CalculatorContract.IControlLogic controlLogic; private final String INVALID_MESSAGE = "Invalid Expression."; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); controlLogic = new DependencyProvider().provideControlLogic(this); display = findViewById(R.id.textViewDisplay); Button evaluate = findViewById(R.id.buttonEvaluate); evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } }); //..bindings for the rest of the calculator buttons } @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } }

Seperti yang Anda lihat, Aktivitas memiliki dua pekerjaan: Pertama, karena merupakan titik masuk dari fitur tertentu dari aplikasi Android , ia bertindak sebagai semacam wadah untuk komponen lain dari fitur tersebut. Dalam istilah sederhana, wadah dapat dianggap sebagai semacam kelas akar yang komponen lain akhirnya ditambatkan melalui referensi (atau bidang anggota pribadi dalam kasus ini). Itu juga mengembang, mengikat referensi, dan menambahkan pendengar ke tata letak XML (antarmuka pengguna).

Menguji Logika Kontrol

Daripada memiliki Activity yang memiliki referensi ke kelas konkret di bagian belakang, kami membuatnya berbicara dengan antarmuka tipe CalculatorContract.IControlLogic. Kami akan membahas mengapa ini adalah antarmuka di bagian selanjutnya. Untuk saat ini, pahami saja bahwa apa pun yang ada di sisi lain antarmuka itu seharusnya menjadi sesuatu seperti Presenter atau Controller . Karena kelas ini akan mengontrol interaksi antara Aktivitas front-end dan Calculator back-end , saya memilih untuk menyebutnya CalculatorControlLogic :

 public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } } public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } }

Ada banyak hal halus tentang cara kelas ini dirancang yang membuatnya lebih mudah untuk diuji. Pertama, semua referensinya berasal dari perpustakaan standar Java, atau antarmuka yang ditentukan dalam aplikasi. Ini berarti bahwa menguji kelas ini tanpa kerangka kerja apa pun sangat mudah, dan dapat dilakukan secara lokal pada JVM . Tip kecil tapi berguna lainnya adalah bahwa semua interaksi yang berbeda dari kelas ini dapat dipanggil melalui satu fungsi generik handleInput(...) . Ini menyediakan satu titik masuk untuk menguji setiap perilaku kelas ini.

Perhatikan juga bahwa dalam fungsi evaluateExpression() , saya mengembalikan kelas tipe Optional<String> dari bagian belakang. Biasanya saya akan menggunakan apa yang disebut programmer fungsional sebagai Either Monad , atau seperti yang saya lebih suka menyebutnya, Result Wrapper . Apa pun nama bodoh yang Anda gunakan, itu adalah objek yang mampu mewakili beberapa status berbeda melalui satu panggilan fungsi. Optional adalah konstruksi sederhana yang dapat mewakili null , atau beberapa nilai dari tipe generik yang disediakan. Bagaimanapun, karena back end mungkin diberikan ekspresi yang tidak valid, kami ingin memberikan kelas ControlLogic beberapa cara untuk menentukan hasil operasi backend; memperhitungkan keberhasilan dan kegagalan. Dalam hal ini, null akan mewakili kegagalan.

Di bawah ini adalah contoh kelas pengujian yang telah ditulis menggunakan JUnit , dan kelas yang dalam jargon pengujian disebut Fake :

 public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } } public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } }

Seperti yang Anda lihat, test suite ini tidak hanya dapat dieksekusi dengan sangat cepat, tetapi juga tidak membutuhkan banyak waktu untuk menulis. Bagaimanapun, sekarang kita akan melihat beberapa hal yang lebih halus yang membuat penulisan kelas tes ini menjadi sangat mudah.

Kekuatan Abstraksi Dan Inversi Ketergantungan

Ada dua konsep penting lainnya yang telah diterapkan ke CalculatorControlLogic yang membuatnya mudah untuk diuji. Pertama, jika Anda pernah bertanya-tanya apa manfaat menggunakan Antarmuka dan Kelas Abstrak (secara kolektif disebut sebagai abstractions ) di Java, kode di atas adalah demonstrasi langsung. Karena kelas yang akan diuji merujuk pada abstraksi alih-alih kelas konkret , kami dapat membuat uji palsu ganda untuk antarmuka pengguna dan back end dari dalam kelas pengujian kami. Selama pengujian ganda ini mengimplementasikan antarmuka yang sesuai, CalculatorControlLogic tidak peduli bahwa itu bukan yang asli.

Kedua, CalculatorControlLogic telah diberikan dependensinya melalui konstruktor (ya, itu adalah bentuk Dependency Injection ), alih-alih membuat dependensinya sendiri. Oleh karena itu, tidak perlu ditulis ulang saat digunakan di lingkungan produksi atau pengujian, yang merupakan bonus efisiensi.

Dependency Injection adalah bentuk Inversion Of Control , yang merupakan konsep rumit untuk didefinisikan dalam bahasa sederhana. Baik Anda menggunakan Injeksi Ketergantungan atau Pola Pencari Layanan , keduanya mencapai apa yang dijelaskan Martin Fowler (guru favorit saya tentang topik semacam itu) sebagai "prinsip pemisahan konfigurasi dari penggunaan." Ini menghasilkan kelas yang lebih mudah untuk diuji, dan lebih mudah untuk dibangun secara terpisah satu sama lain.

Menguji Logika Komputasi

Akhirnya, kita sampai pada kelas ComputationLogic , yang seharusnya memperkirakan perangkat IO seperti adaptor ke server jauh, atau database lokal. Karena kita tidak memerlukan keduanya untuk kalkulator sederhana, itu hanya akan bertanggung jawab untuk merangkum logika yang diperlukan untuk memvalidasi dan mengevaluasi ekspresi yang kita berikan:

 public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } } public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }

Tidak banyak yang bisa dikatakan tentang kelas ini karena biasanya akan ada beberapa sambungan erat ke perpustakaan back-end tertentu yang akan menghadirkan masalah serupa sebagai kelas yang dipasangkan dengan erat ke Android. Sebentar lagi kita akan membahas apa yang harus dilakukan tentang kelas seperti itu, tetapi yang ini sangat mudah untuk diuji sehingga kita mungkin juga mencobanya:

 public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } } public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }

Kelas yang paling mudah untuk diuji, adalah kelas yang hanya diberi beberapa nilai atau objek, dan diharapkan mengembalikan hasil tanpa perlu memanggil beberapa dependensi eksternal. Bagaimanapun, ada titik di mana tidak peduli berapa banyak sihir arsitektur perangkat lunak yang Anda terapkan, Anda masih perlu khawatir tentang kelas yang tidak dapat dipisahkan dari platform dan kerangka kerja. Untungnya, masih ada cara kita dapat menggunakan arsitektur perangkat lunak untuk: Paling buruk membuat kelas-kelas ini lebih mudah untuk diuji, dan yang terbaik, sangat sederhana sehingga pengujian dapat dilakukan dalam sekejap .

Objek Sederhana Dan Tampilan Pasif

Dua nama di atas mengacu pada pola di mana objek yang harus berbicara dengan dependensi tingkat rendah, disederhanakan sedemikian rupa sehingga bisa dibilang tidak perlu diuji. Saya pertama kali diperkenalkan dengan pola ini melalui blog Martin Fowler tentang variasi Model-View-Presenter. Kemudian, melalui karya Robert C. Martin, saya diperkenalkan dengan gagasan untuk memperlakukan kelas tertentu sebagai Humble Objects , yang menyiratkan bahwa pola ini tidak perlu terbatas pada kelas antarmuka pengguna (walaupun saya tidak bermaksud mengatakan bahwa Fowler pernah menyiratkan batasan seperti itu).

Apa pun yang Anda pilih untuk menyebut pola ini, itu sangat sederhana untuk dipahami, dan dalam beberapa hal saya percaya itu sebenarnya hanya hasil dari penerapan SOC yang ketat ke kelas Anda. Sementara pola ini berlaku juga untuk kelas back end, kami akan menggunakan kelas antarmuka pengguna kami untuk mendemonstrasikan prinsip ini dalam tindakan. Pemisahannya sangat sederhana: Kelas-kelas yang berinteraksi dengan dependensi platform dan kerangka kerja, tidak berpikir sendiri (karenanya disebut Humble dan Pasif ). Ketika suatu peristiwa terjadi, satu-satunya hal yang mereka lakukan adalah meneruskan detail peristiwa ini ke kelas logika apa pun yang sedang mendengarkan:

 //from CalculatorActivity's onCreate() function: evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } });

Kelas logika, yang seharusnya mudah untuk diuji, kemudian bertanggung jawab untuk mengontrol antarmuka pengguna dengan cara yang sangat halus. Daripada memanggil fungsi updateUserInterface(...) generik tunggal pada kelas user interface dan membiarkannya melakukan pekerjaan pembaruan massal, user interface (atau kelas semacam itu) akan memiliki fungsi kecil dan spesifik yang seharusnya mudah untuk nama dan implementasi:

 //Interface functions of CalculatorActivity: @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } //…

In principal, these two examples ought to give you enough to understand how to go about implementing this pattern. The object which possesses the logic is loosely coupled, and the object which is tightly coupled to pesky dependencies becomes almost devoid of logic.

Now, at the start of this subsection, I made the statement that these classes become arguably unnecessary to test, and it is important we look at both sides of this argument. In an absolute sense, it is impossible to achieve 100% test coverage by employing this pattern, unless you still write tests for such humble / passive classes. It is also worth noting that my decision to use a Calculator as an example App, means that I cannot escape having a gigantic mass of findViewById(...) calls present in the Activity. Giant masses of repetitive code are a common cause of typing errors, and in the absence of some Android UI testing frameworks, my only recourse for testing would be via deploying the feature to a device and manually testing each interaction. Aduh.

It is at this point that I will humbly say that I do not know if 100% code coverage is absolutely necessary. I do not know many developers who strive for absolute test coverage in production code, and I have never done so myself. One day I might, but I will reserve my opinions on this matter until I have the reference experiences to back them up. In any case, I would argue that applying this pattern will still ultimately make it simpler and easier to test tightly coupled classes; if for no reason other than they become simpler to write.

Another objection to this approach, was raised by a fellow programmer when I described this approach in another context. The objection was that the logic class (whether it be a Controller , Presenter , or even a ViewModel depending on how you use it), becomes a God class.

While I do not agree with that sentiment, I do agree that the end result of applying this pattern is that your Logic classes become larger than if you left more decisions up to your user interface class.

This has never been an issue for me as I treat each feature of my applications as self-contained components, as opposed to having one giant controller for managing multiple user interface screens. In any case, I think this argument holds reasonably true if you fail to apply SOC to your front end or back end components. Therefore, my advice is to apply SOC to your front end and back end components quite rigorously.

Further Considerations

After all of this discussion on applying the principles of software architecture to reduce the necessity of using a wide-array of testing frameworks, improve the testability of classes in general, and a pattern which allows classes to be tested indirectly (at least to some degree), I am not actually here to tell you to stop using your preferred frameworks.

For those curious, I often use a library to generate mock classes for my Unit tests (for Java I prefer Mockito , but these days I mostly write Kotlin and prefer Mockk in that language), and JUnit is a framework which I use quite invariably. Since all of these options are coupled to languages as opposed to the Android platform, I can use them quite interchangeably across mobile and web application development. From time to time (if project requirements demand it), I will even use tools like Robolectric , MockWebServer , and in my five years of studying Android, I did begrudgingly use Espresso once.

My hope is that in reading this article, anyone who has experienced a similar degree of aversion to testing due to paralysis by jargon analysis , will come to see that getting started with testing really can be simple and framework minimal .

Bacaan Lebih Lanjut tentang SmashingMag:

  • Sliding In And Out Of Vue.js
  • Designing And Building A Progressive Web Application Without A Framework
  • Kerangka Kerja CSS Atau Kotak CSS: Apa yang Harus Saya Gunakan Untuk Proyek Saya?
  • Menggunakan Flutter Google Untuk Pengembangan Seluler yang Benar-Benar Lintas-Platform