Çerçeve Minimalizmi ve Yazılım Mimarisi ile Test Etme Daha Kolay
Yayınlanan: 2022-03-10Diğer birçok Android geliştiricisi gibi, platformda test etme konusundaki ilk adımım, hemen moral bozucu bir jargonla karşı karşıya kalmama neden oldu. Ayrıca, o sırada karşılaştığım birkaç örnek (yaklaşık 2015), bir TextView.setText ( …) düzgün çalışıyordu, makul bir yatırımdı.
Daha da kötüsü, teoride veya pratikte çalışan bir yazılım mimarisi anlayışına sahip değildim, bu da bu çerçeveleri öğrenmeye zahmet etsem bile, birkaç god
sınıfından oluşan monolitik uygulamalar için testler yazıyor olacağım anlamına geliyordu. spagetti kodunda . Can alıcı nokta şudur ki, bu tür uygulamaları oluşturmak, test etmek ve sürdürmek, çerçeve uzmanlığınız ne olursa olsun, kendi kendini sabote etme alıştırmasıdır; yine de bu gerçekleşme, ancak modüler , gevşek bağlı ve yüksek düzeyde uyumlu bir uygulama oluşturulduktan sonra netleşir.
Buradan, burada sade bir dille özetleyeceğim bu makaledeki ana tartışma noktalarından birine geliyoruz: Yazılım mimarisinin altın ilkelerini uygulamanın birincil faydaları arasında (endişelenmeyin, bunları basit örneklerle tartışacağım ve dil), kodunuzun test edilmesi daha kolay hale gelebilir. Bu ilkeleri uygulamanın başka faydaları da vardır, ancak yazılım mimarisi ve test arasındaki ilişki bu makalenin odak noktasıdır.
Ancak, kodumuzu neden ve nasıl test ettiğimizi anlamak isteyenler için, önce analoji ile test kavramını keşfedeceğiz; herhangi bir jargonu ezberlemenize gerek kalmadan. Ana konuya derinlemesine girmeden önce, neden bu kadar çok test çerçevesinin var olduğu sorusuna da bakacağız, çünkü bunu incelerken faydalarını, sınırlamalarını ve hatta belki de alternatif bir çözümü görmeye başlayabiliriz.
Test: Neden ve Nasıl
Bu bölüm, herhangi bir deneyimli testçi için yeni bir bilgi olmayacak, ancak yine de bu benzetmeyi beğenebilirsiniz. Tabii ki ben bir yazılım mühendisiyim, roket mühendisi değilim, ama bir an için hem fiziksel uzayda hem de bir bilgisayarın hafıza alanında nesneler tasarlamak ve inşa etmekle ilgili bir benzetme ödünç alacağım. Ortam değişirken, sürecin prensipte tamamen aynı olduğu ortaya çıkıyor.
Bir an için roket mühendisi olduğumuzu ve işimizin bir uzay mekiğinin ilk aşama* roket güçlendiricisini yapmak olduğunu varsayalım. Ayrıca, çeşitli koşullarda inşa etmeye ve test etmeye başlamak için ilk aşama için kullanışlı bir tasarım bulduğumuzu varsayalım.
"Birinci aşama", roket ilk fırlatıldığında ateşlenen güçlendiricileri ifade eder.
İşleme geçmeden önce neden bu benzetmeyi tercih ettiğimi belirtmek isterim: Tasarımımızı insan hayatının söz konusu olduğu durumlara sokmadan önce neden test etmeye uğraşıyoruz sorusuna yanıt vermekte zorlanmanız gerekir. Uygulamalarınızı başlatmadan önce test etmenin hayat kurtarabileceğine (uygulamanın doğasına bağlı olarak mümkün olsa da) sizi ikna etmeye çalışmasam da, derecelendirmeleri, incelemeleri ve işinizi kurtarabilir. En geniş anlamıyla, test etme, tek parçaların, birkaç bileşenin ve tüm sistemlerin, arızalanmamalarının kritik önem taşıdığı durumlarda bunları kullanmadan önce çalıştığından emin olmamızın yoludur.
Bu benzetmenin nasıl yönüne dönersek, mühendislerin belirli bir tasarımı test etme sürecini tanıtacağım: artıklık . Yedeklilik prensipte basittir: Test edilecek bileşenin kopyalarını, başlatma zamanında kullanmak istediğinizle aynı tasarım spesifikasyonuna göre oluşturun. Bu kopyaları, ön koşulları ve değişkenleri sıkı bir şekilde kontrol eden yalıtılmış bir ortamda test edin. Bu, roket güçlendiricinin tüm mekiğe entegre edildiğinde düzgün çalışacağını garanti etmese de, kontrollü bir ortamda çalışmazsa, hiç çalışma ihtimalinin çok düşük olacağından emin olunabilir.
Roket tasarımının kopyalarının test edildiği yüzlerce, belki de binlerce değişken arasından, roket güçlendiricinin test ateşlemesinin yapılacağı ortam sıcaklıklarına indiğini varsayalım. 35°C'de test ettiğimizde her şeyin hatasız çalıştığını görüyoruz. Yine, roket arızasız olarak kabaca oda sıcaklığında test edilir. Son test, fırlatma sahası için kaydedilen en düşük sıcaklıkta, -5°C'de olacaktır. Bu son test sırasında roket ateşlenir, ancak kısa bir süre sonra roket alevlenir ve kısa bir süre sonra şiddetli bir şekilde patlar; ama neyse ki kontrollü ve güvenli bir ortamda.
Bu noktada, sıcaklıktaki değişikliklerin en azından başarısız testte rol oynadığını biliyoruz, bu da roket güçlendiricinin hangi bölümlerinin soğuk sıcaklıklardan olumsuz etkilenebileceğini düşünmemize yol açıyor. Zamanla, bir bölmeden diğerine yakıt akışını durdurmaya yarayan bir kauçuk O-ring olan bir anahtar bileşenin, donma noktasına yaklaşan veya altındaki sıcaklıklara maruz kaldığında katılaştığı ve etkisiz hale geldiği keşfedildi.
Onun benzetmesinin, Challenger uzay mekiği felaketinin trajik olaylarına gevşek bir şekilde dayandığını fark etmişsinizdir. Aşina olmayanlar için, üzücü gerçek (araştırmaların sonucuna göre) çok sayıda başarısız testin ve mühendislerin uyarılarının olduğu ve yine de idari ve politik kaygıların fırlatmanın ne olursa olsun devam etmesine neden olduğudur. Her halükarda, artıklık terimini ezberlemiş olsanız da olmasanız da, herhangi bir tür sistemin parçalarını test etmek için temel süreci kavradığınızı umuyorum.
Yazılımla ilgili
Önceki benzetme, roketleri test etmek için temel süreci açıklarken (daha ince ayrıntılarla bolca özgürlük alırken), şimdi muhtemelen sizin ve benim için daha alakalı bir şekilde özetleyeceğim. Yazılımı yalnızca fırlatarak test etmek mümkün olsa da herhangi bir konuşlandırılabilir durumda olduğunda, cihazlara, bunun yerine artıklık ilkesini önce uygulamanın tek tek bölümlerine uygulayabileceğimizi düşünüyorum.
Bu, tüm uygulamanın daha küçük bölümlerinin (genellikle yazılım Birimleri olarak anılır) kopyalarını oluşturduğumuz, yalıtılmış bir test ortamı oluşturduğumuz ve oluşabilecek değişkenlere, argümanlara, olaylara ve yanıtlara dayalı olarak nasıl davrandıklarını gördüğümüz anlamına gelir. işlem esnasında. Test etmek, teoride olduğu kadar basittir, ancak bu sürece ulaşmanın anahtarı, uygulanabilir bir şekilde test edilebilir uygulamalar oluşturmaktır. Bu, sonraki iki bölümde inceleyeceğimiz iki endişeye indirgeniyor. İlk endişe test ortamıyla , ikinci endişe ise uygulamaları yapılandırma şeklimizle ilgili.
Neden Çerçevelere İhtiyacımız Var?
Bir yazılımı test etmek için (bundan böyle Birim olarak anılacaktır, ancak bu tanım kasıtlı olarak aşırı basitleştirmedir), çalışma zamanında yazılımınızla etkileşime girmenize izin veren bir tür test ortamına sahip olmak gerekir. Yalnızca bir JVM ( Java Sanal Makinesi ) ortamında yürütülecek uygulamalar oluşturmak için, testler yazmak için gereken tek şey bir JRE'dir ( Java Runtime Environment ). Örneğin bu çok basit Hesap Makinesi sınıfını alın:
class Calculator { private int add(int a, int b){ return a + b; } private int subtract(int a, int b){ return a - b; } }
Herhangi bir çerçevenin yokluğunda, kodumuzu gerçekten yürütmek için bir main
işlevi içeren bir test sınıfımız olduğu sürece, onu test edebiliriz. Hatırlayacağınız gibi, main
işlev, basit bir Java programı için yürütmenin başlangıç noktasını belirtir. Neyi test ettiğimize gelince, Hesap Makinesinin işlevlerine bazı test verilerini besliyoruz ve temel aritmetiği doğru şekilde gerçekleştirdiğini doğrulıyoruz:
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."); } }
Bir Android uygulamasını test etmek elbette tamamen farklı bir prosedürdür. JVM'de bir Android uygulaması başlatılmadan önce çağrılan ZygoteInit.java dosyasının (daha ince ayrıntıları burada önemli olmayan) kaynağının derinliklerinde gömülü bir main
işlev olmasına rağmen, küçük bir Android geliştiricisi bile bunu yapmalıdır . bu işlevi çağırmaktan sistemin kendisinin sorumlu olduğunu bilin; geliştirici değil . Bunun yerine, Android uygulamalarının giriş noktaları Application
sınıfı ve sistemin AndroidManifest.xml dosyası aracılığıyla gösterilebileceği herhangi bir Activity
sınıfı olabilir.
Tüm bunlar, yalnızca test ortamımızın artık Android platformunu hesaba katması gerektiğinden, bir Android uygulamasındaki Test Birimlerinin daha yüksek bir karmaşıklık düzeyi sunduğu gerçeğine giden bir ipucudur.
Sıkı Kaplin Sorununu Evcilleştirmek
Sıkı bağlantı , belirli platformlara, çerçevelere, dillere ve kitaplıklara bağlı olan bir işlevi, sınıfı veya uygulama modülünü tanımlayan bir terimdir. Göreceli bir terimdir, yani Calculator.java örneğimiz Java programlama dili ve standart kitaplıkla sıkı bir şekilde bağlantılıdır, ancak bu, bağlantının kapsamıdır. Aynı şekilde, Android platformuna sıkı sıkıya bağlı olan sınıfları test etme sorunu , platformla veya platform etrafında çalışmanın bir yolunu bulmanız gerektiğidir.
Android platformuna sıkı sıkıya bağlı sınıflar için iki seçeneğiniz var. Birincisi, sınıflarınızı bir Android cihaza (fiziksel veya sanal) dağıtmaktır. Uygulama kodunuzu üretime göndermeden önce test etmenizi önersem de, bu, geliştirme sürecinin erken ve orta aşamalarında zamana göre oldukça verimsiz bir yaklaşımdır.
Bir Unit , ne kadar teknik bir tanım tercih ederseniz edin, genellikle bir sınıftaki tek bir işlev olarak düşünülür (bazıları tanımı ilk tek işlev çağrısı tarafından dahili olarak çağrılan sonraki yardımcı işlevleri içerecek şekilde genişletse de). Her iki durumda da Birimler küçük olmalıdır; Tek bir Birimi test etmek için tüm bir uygulamayı oluşturmak, derlemek ve dağıtmak, tek başına test etme noktasını tamamen gözden kaçırmak demektir.
Sıkı bağlantı sorununa başka bir çözüm, platform bağımlılıklarıyla etkileşim kurmak veya bunlarla alay etmek (simüle etmek) için test çerçevelerini kullanmaktır. Espresso ve Robolectric gibi çerçeveler geliştiricilere Birimleri test etmek için önceki yaklaşımdan çok daha etkili araçlar sağlar; birincisi, bir cihazda yürütülen testler için kullanışlıdır ("araçlı testler" olarak bilinir, çünkü görünüşe göre bunları cihaz testleri olarak adlandırmak yeterince belirsiz değildir) ve ikincisi, bir JVM'de Android çerçevesini yerel olarak alay etme yeteneğine sahiptir.
Birazdan tartışacağım alternatif yerine bu tür çerçevelere karşı çıkmadan önce, bu seçenekleri asla kullanmamanız gerektiğini ima etmek istemediğimi açıkça belirtmek isterim. Bir geliştiricinin uygulamalarını oluşturmak ve test etmek için kullandığı süreç, kişisel tercih ve verimliliğe yönelik bir bakışın birleşiminden doğmalıdır.
Modüler ve gevşek bağlı uygulamalar yapmaktan hoşlanmayanlar için, yeterli düzeyde test kapsamına sahip olmak istiyorsanız, bu çerçevelere aşina olmaktan başka seçeneğiniz olmayacaktır. Bu şekilde birçok harika uygulama oluşturuldu ve uygulamalarımı fazla modüler ve soyut yapmakla sık sık suçlanmıyorum. İster benim yaklaşımımı benimseyin ister çerçevelere ağırlık vermeye karar verin, uygulamalarınızı test etmek için zaman ve çaba harcadığınız için sizi selamlıyorum.
Çerçevelerinizi Silah Mesafesinde Tutun
Bu makalenin temel dersinin son önsözü için, çerçeveleri kullanmak söz konusu olduğunda neden minimalizm tavrına sahip olmak isteyebileceğinizi tartışmaya değer (ve bu, çerçeveleri test etmekten daha fazlası için geçerlidir). Yukarıdaki altyazı, yazılımın en iyi uygulamalarının cömert öğretmeninin bir ifadesidir: Robert “Uncle Bob” C. Martin. Çalışmalarını ilk okuduğumdan beri bana verdiği birçok mücevherden bunu kavramak için birkaç yıllık doğrudan deneyim gerekti.
Bu ifadenin ne hakkında olduğunu anladığım kadarıyla, çerçeveleri kullanmanın maliyeti, onları öğrenmek ve sürdürmek için gereken zaman yatırımındadır. Bazıları oldukça sık değişir ve bazıları yeterince sık değişmez. İşlevler kullanımdan kaldırılır, çerçevelerin bakımı sona erer ve her 6-24 ayda bir, sonuncunun yerini alacak yeni bir çerçeve gelir. Bu nedenle, bir platform veya dil özelliği olarak uygulanabilecek (çok daha uzun süre dayanan) bir çözüm bulabilirseniz, yukarıda belirtilen çeşitli türlerdeki değişikliklere karşı daha dirençli olma eğiliminde olacaktır.
Daha teknik bir notta, Espresso gibi çerçeveler ve daha düşük bir derecede Robolectric , asla basit JUnit testleri kadar verimli çalışamaz, hatta daha önce yapılan çerçeve ücretsiz testi bile. JUnit gerçekten bir çerçeve olsa da, uygun Android platformundan çok daha yavaş bir oranda değişme eğiliminde olan JVM ile sıkı bir şekilde bağlantılıdır. Daha az çerçeve, neredeyse değişmez bir şekilde, bir veya daha fazla testi yürütmek ve yazmak için gereken süre açısından daha verimli olan kod anlamına gelir.
Bundan, muhtemelen Android platformunu silah mesafesinde tutmamıza izin veren bazı tekniklerden yararlanacak bir yaklaşımı tartışacağımızı anlayabilirsiniz; tüm bu süre boyunca bize bol miktarda kod kapsamı, test verimliliği ve ihtiyaç duyulduğunda burada veya orada bir çerçeve kullanma fırsatı veriyor.
Mimarlık Sanatı
Aptalca bir benzetme kullanmak gerekirse, çerçeveler ve platformlar, siz onlarla uygun sınırlar koymadığınız sürece geliştirme sürecinizi devralacak baskıcı meslektaşlar gibi düşünülebilir. Yazılım mimarisinin altın ilkeleri, size bu sınırları hem oluşturmak hem de uygulamak için gerekli genel kavramları ve özel teknikleri verebilir. Birazdan göreceğimiz gibi, yazılım mimarisi ilkelerini kodunuza uygulamanın faydalarının gerçekten ne olduğunu hiç merak ettiyseniz, bazıları doğrudan ve çoğu dolaylı olarak kodunuzu test etmeyi kolaylaştırır.
Endişelerin Ayrılması
Endişelerin Ayrımı , tahminimce, yazılım mimarisinde bir bütün olarak evrensel olarak en uygulanabilir ve kullanışlı kavramdır (diğerlerinin ihmal edilmesi gerektiğini söylemeden). Endişelerin ayrılması (SOC), bildiğim yazılım geliştirmenin her perspektifinde uygulanabilir veya tamamen göz ardı edilebilir. Konsepti kısaca özetlemek için, sınıflara uygulandığında SOC'ye bakacağız, ancak SOC'nin kapsamlı yardımcı işlevlerin kullanımı yoluyla işlevlere uygulanabileceğini ve bir uygulamanın tüm modüllerine ("modüller") tahmin edilebileceğini unutmayın. Android/Gradle bağlamı).
GUI uygulamaları için tüm yazılım mimari modellerini araştırmak için çok zaman harcadıysanız, muhtemelen en az biriyle karşılaşmış olacaksınız: Model-View-Controller (MVC), Model-View-Presenter (MVP) veya Model-View- Görünüm Modeli (MVVM). Her tarzda uygulamalar geliştirdikten sonra, hiçbirinin tüm projeler (hatta tek bir projedeki özellikler) için en iyi seçenek olduğunu düşünmediğimi peşinen söyleyeceğim. İronik olarak, Android ekibinin birkaç yıl önce önerilen yaklaşımı olarak sunduğu model olan MVVM, Android'e özgü test çerçevelerinin yokluğunda en az test edilebilir gibi görünüyor (Android platformunun, benim de hayranı olduğum ViewModel sınıflarını kullanmak istediğinizi varsayarsak). ile ilgili).
Her durumda, bu kalıpların özellikleri, genellerinden daha az önemlidir. Bu kalıpların tümü, SOC'nin yalnızca farklı tatlarıdır ve bunlar, Veri , Kullanıcı Arayüzü , Mantık olarak adlandırdığım üç tür kodun temel ayrılmasını vurgular.
Peki Data , User Interface ve Logic'i ayırmak uygulamalarınızı test etmenize tam olarak nasıl yardımcı olur? Cevap, platform/çerçeve bağımlılıklarıyla ilgilenmesi gereken sınıflardan mantığı, çok az platform/çerçeve bağımlılığı olan veya hiç olmayan sınıflara çekerek, test etmenin kolaylaşması ve çerçevenin minimal hale gelmesidir. Açık olmak gerekirse, genellikle kullanıcı arabirimini oluşturması, verileri bir SQL tablosunda depolaması veya uzak bir sunucuya bağlanması gereken sınıflardan bahsediyorum. Bunun nasıl çalıştığını göstermek için varsayımsal bir Android uygulamasının basitleştirilmiş üç katmanlı mimarisine bakalım.
Birinci sınıf, kullanıcı arabirimimizi yönetecek. İşleri basit tutmak için, bu amaç için bir Aktivite kullandım, ancak genellikle kullanıcı arayüzü sınıfları olarak Fragments'ı tercih ediyorum. Her iki durumda da, her iki sınıf da Android platformuna benzer sıkı bağlantı sunar:
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(); } }
Gördüğünüz gibi, Activity'nin iki görevi vardır: Birincisi, bir Android uygulamasının belirli bir özelliğinin giriş noktası olduğu için, özelliğin diğer bileşenleri için bir tür kapsayıcı görevi görür. Basit bir ifadeyle, bir kap , diğer bileşenlerin nihai olarak referanslar (veya bu durumda özel üye alanları) yoluyla bağlı olduğu bir tür kök sınıf olarak düşünülebilir. Ayrıca şişirir, referansları bağlar ve XML düzenine (kullanıcı arayüzü) dinleyiciler ekler.
Kontrol Mantığını Test Etme
Activity'nin arka uçta somut bir sınıfa referansa sahip olması yerine, onun CalculatorContract.IControlLogic.
Bunun neden bir arayüz olduğunu bir sonraki bölümde tartışacağız. Şimdilik, bu arayüzün diğer tarafında ne varsa, Presenter veya Controller gibi bir şey olması gerektiğini anlayın. Bu sınıf, ön uç Activity ve arka uç Calculator arasındaki etkileşimleri kontrol edeceğinden, onu CalculatorControlLogic
olarak adlandırmayı seçtim:
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(); } }
Bu sınıfın tasarlanma şekliyle ilgili, test etmeyi kolaylaştıran pek çok ince nokta vardır. İlk olarak, tüm referansları ya Java standart kitaplığından ya da uygulama içinde tanımlanan arayüzlerdendir. Bu, bu sınıfı herhangi bir çerçeve olmadan test etmenin mutlak bir esinti olduğu ve yerel olarak bir JVM üzerinde yapılabileceği anlamına gelir. Bir başka küçük ama kullanışlı ipucu, bu sınıfın tüm farklı etkileşimlerinin tek bir genel handleInput(...)
işlevi aracılığıyla çağrılabilmesidir. Bu, bu sınıfın her davranışını test etmek için tek bir giriş noktası sağlar.
Ayrıca, evaluateExpression()
işlevinde, arka uçtan Optional<String>
türünde bir sınıf döndürdüğümü unutmayın. Normalde, işlevsel programcıların ya Monad dediği ya da benim tercih ettiğim gibi, Result Wrapper dediği şeyi kullanırdım. Hangi aptal adı kullanırsanız kullanın, tek bir işlev çağrısı yoluyla birden çok farklı durumu temsil edebilen bir nesnedir. Optional
, bir null veya sağlanan genel türün bir değerini temsil edebilen daha basit bir yapıdır. Her durumda, arka uca geçersiz bir ifade verilebileceğinden, ControlLogic
sınıfına arka uç işleminin sonucunu belirlemenin bazı yollarını vermek istiyoruz; hem başarıyı hem de başarısızlığı hesaba katar. Bu durumda, null bir başarısızlığı temsil eder.
Aşağıda JUnit kullanılarak yazılmış örnek bir test sınıfı ve test jargonunda Fake olarak adlandırılan bir sınıf bulunmaktadır:
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"); } } }
Gördüğünüz gibi, bu test takımı sadece çok hızlı bir şekilde yürütülmekle kalmıyor, aynı zamanda yazması da çok fazla zaman almıyor. Her durumda, şimdi bu test sınıfını yazmayı çok kolaylaştıran daha incelikli şeylere bakacağız.

Soyutlamanın Gücü ve Bağımlılığı Tersine Çevirme
CalculatorControlLogic
uygulanmış ve test etmeyi son derece kolaylaştıran iki önemli kavram daha vardır. İlk olarak, Java'da Arayüzler ve Soyut Sınıflar (topluca soyutlamalar olarak anılır) kullanmanın faydalarının ne olduğunu merak ettiyseniz, yukarıdaki kod doğrudan bir gösteridir. Test edilecek sınıf, somut sınıflar yerine soyutlamalara atıfta bulunduğundan, kullanıcı arayüzü ve arka uç için test sınıfımızdan Sahte test çiftleri oluşturabildik. Bu test çiftleri uygun arabirimleri uyguladığı sürece, CalculatorControlLogic
bunların gerçek olmadıklarını umursayamazdı.
İkinci olarak, CalculatorControlLogic
kendi bağımlılıklarını oluşturmak yerine, bağımlılıkları yapıcı aracılığıyla verilmiştir (evet, bu bir Bağımlılık Enjeksiyonu biçimidir). Bu nedenle, verimlilik için bir bonus olan bir üretim veya test ortamında kullanıldığında yeniden yazılmasına gerek yoktur.
Dependency Injection , sade bir dilde tanımlanması zor bir kavram olan Inversion Of Control'ün bir biçimidir. Dependency Injection veya Service Locator Pattern kullansanız da, ikisi de Martin Fowler'ın (bu tür konularda en sevdiğim öğretmenim) "yapılandırmayı kullanımdan ayırma ilkesi" olarak tanımladığı şeye ulaşır. Bu, test edilmesi daha kolay ve birbirinden izole olarak oluşturulması daha kolay sınıflarla sonuçlanır.
Hesaplama Mantığını Test Etme
Son olarak, bir adaptör gibi bir IO cihazını uzak bir sunucuya veya yerel bir veritabanına yaklaştırması beklenen ComputationLogic
sınıfına geliyoruz. Basit bir hesap makinesi için bunların hiçbirine ihtiyacımız olmadığı için, yalnızca ona verdiğimiz ifadeleri doğrulamak ve değerlendirmek için gereken mantığın kapsüllenmesinden sorumlu olacaktır:
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); } } }
Bu sınıf hakkında söylenecek çok fazla bir şey yok çünkü tipik olarak belirli bir arka uç kitaplığına sıkı bir bağlantı olacak ve bu da Android'e sıkı sıkıya bağlı bir sınıf olarak benzer sorunları ortaya çıkaracaktır. Birazdan bu tür sınıflar hakkında ne yapacağımızı tartışacağız, ancak bunu test etmek o kadar kolay ki biz de deneyebiliriz:
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()); } }
Test edilmesi en kolay sınıflar, basitçe bir değer veya nesne verilen ve bazı dış bağımlılıkları çağırmaya gerek kalmadan bir sonuç döndürmesi beklenen sınıflardır. Her durumda, ne kadar yazılım mimarisi sihirbazlığı uygularsanız uygulayın, yine de platformlardan ve çerçevelerden ayrıştırılamayan sınıflar hakkında endişelenmeniz gerekeceği bir nokta gelir. Neyse ki, yazılım mimarisini şu amaçlarla kullanmanın hala bir yolu var: En kötü ihtimalle bu sınıfların test edilmesini kolaylaştırmak ve en iyi ihtimalle testin bir bakışta yapılabilmesi için çok basit hale getirmek.
Mütevazı Nesneler ve Pasif Görüşler
Yukarıdaki iki isim, düşük seviyeli bağımlılıklarla konuşması gereken bir nesnenin o kadar basitleştirildiği ve muhtemelen test edilmesi gerekmediği bir kalıba atıfta bulunur. Bu modelle ilk olarak Martin Fowler'ın Model-View-Presenter varyasyonları hakkındaki blogu aracılığıyla tanıştım. Daha sonra, Robert C. Martin'in çalışmaları aracılığıyla, belirli sınıfları Humble Objects olarak ele alma fikriyle tanıştım; bu, bu kalıbın kullanıcı arabirimi sınıflarıyla sınırlı olması gerekmediği anlamına gelir (ancak Fowler'ın asla böyle bir sınırlama anlamına gelir).
Bu kalıba ne demeyi seçerseniz seçin, bunu anlamak son derece basittir ve bir bakıma bunun sadece derslerinize SOC'yi titizlikle uygulamanın bir sonucu olduğuna inanıyorum. Bu model arka uç sınıfları için de geçerli olsa da, bu prensibi eylemde göstermek için kullanıcı arabirimi sınıfımızı kullanacağız. Ayırma çok basittir: Platform ve çerçeve bağımlılıklarıyla etkileşime giren sınıflar, kendileri için düşünmezler (bu nedenle Humble ve Passive takma adları). Bir olay meydana geldiğinde, yaptıkları tek şey bu olayın ayrıntılarını dinlemekte olan herhangi bir mantık sınıfına iletmektir:
//from CalculatorActivity's onCreate() function: evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } });
Test edilmesi son derece kolay olması gereken mantık sınıfı, kullanıcı arayüzünü çok ince taneli bir şekilde kontrol etmekten sorumludur. user interface
sınıfında tek bir genel updateUserInterface(...)
işlevini çağırmak ve onu toplu bir güncellemenin işini yapmaya bırakmak yerine, user interface
(veya bu tür başka bir sınıf), kullanımı kolay olması gereken küçük ve özel işlevlere sahip olacaktır. isim ve uygula:
//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. Ah.
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 .
SmashingMag'de Daha Fazla Okuma :
- Sliding In And Out Of Vue.js
- Designing And Building A Progressive Web Application Without A Framework
- CSS Çerçeveleri veya CSS Izgarası: Projem İçin Ne Kullanmalıyım?
- Gerçekten Platformlar Arası Mobil Geliştirme İçin Google'ın Flutter'ını Kullanma