Birim Testleri Yazarken Mocks Kullanmayın

Yayınlanan: 2018-05-02

Not: Bu, baş mühendis Seth Ammons tarafından yazılan en son teknik mühendislik yazımızdır. Sam Nguyen, Kane Kim, Elmer Thomas ve Kevin Gillette'e bu gönderiyi değerlendirdikleri için özel teşekkürler . Ve bunun gibi daha fazla gönderi için teknik blog rulomuza göz atın.

Kodum için testler yazmaktan gerçekten zevk alıyorum, özellikle birim testleri. Bana verdiği güven duygusu harika. Uzun zamandır üzerinde çalışmadığım bir şeyi almak ve birim ve entegrasyon testlerini yürütebilmek, gerekirse ve testlerimin iyi ve anlamlı bir kapsamı olduğu ve geçmeye devam ettiği sürece acımasızca yeniden düzenleme yapabileceğim bilgisini veriyor. , Daha sonra hala işlevsel yazılımım olacak.

Birim testleri, kod tasarımına rehberlik eder ve arıza modlarının ve mantık akışlarının istendiği gibi çalıştığını hızlı bir şekilde doğrulamamızı sağlar. Bununla, belki biraz daha tartışmalı bir şey hakkında yazmak istiyorum: birim testleri yazarken alay kullanmayın.

Tabloda bazı tanımlar alalım

Birim ve entegrasyon testleri arasındaki fark nedir? Alaylar ile ne demek istiyorum ve bunun yerine ne kullanmalısınız? Bu gönderi Go'da çalışmaya odaklanmıştır ve bu nedenle bu kelimelere olan eğilimim Go bağlamındadır.

Birim testleri dediğimde, küçük kod birimlerini test ederek doğru hata işlemeyi sağlayan ve sistemin tasarımını yönlendiren testlerden bahsediyorum. Birim olarak, bütün bir pakete, bir arayüze veya tek bir yönteme atıfta bulunabiliriz.

Entegrasyon testi, bağımlı sistemler ve/veya kitaplıklarla gerçekten etkileşimde bulunduğunuz yerdir. "Alay" dediğimde, özellikle "Sahte Nesne" terimine atıfta bulunuyorum, burada "etki alanı kodunu hem gerçek işlevselliği taklit eden hem de kodumuzun davranışı hakkında iddiaları zorunlu kılan sahte uygulamalarla değiştiriyoruz [1]" (vurgu). benim).

Biraz daha kısa ifade edildi: alaycı davranış davranışı, örneğin:

MyMock.Method("foo").Called(1).WithArgs("bar").Returns("raz")

“Alay yerine Sahte”yi savunuyorum.

Sahte, iş davranışını içerebilen bir tür test dublörüdür [2]. Sahteler yalnızca bir arayüze uyan yapılardır ve davranışı kontrol ettiğimiz bir bağımlılık enjeksiyonu şeklidir. Sahtelerin en büyük yararı, sahtelerin eşleşmeyi artırdığı ve eşleşmenin yeniden düzenlemeyi zorlaştırdığı kodda eşleşmeyi azaltmalarıdır [3].

Bu yazıda, sahtelerin esneklik sağladığını ve kolay test ve yeniden düzenlemeye izin verdiğini göstermeyi amaçlıyorum. Taklitlere kıyasla bağımlılıkları azaltırlar ve bakımı kolaydır.

Bu türden tipik bir gönderide görebileceğiniz gibi “toplam fonksiyonunu test etmekten” biraz daha gelişmiş bir örnekle başlayalım. Ancak, bu gönderide yer alan kodu daha kolay anlayabilmeniz için size biraz bağlam vermem gerekiyor.

SendGrid'de, sistemlerimizden biri geleneksel olarak yerel dosya sisteminde dosyalara sahiptir, ancak daha yüksek kullanılabilirlik ve daha iyi aktarım hızı ihtiyacı nedeniyle bu dosyaları S3'e taşıyoruz.

Bu dosyaları okuyabilmesi gereken bir uygulamamız var ve konfigürasyona bağlı olarak “yerel” veya “uzak” olarak iki modda çalışabilen bir uygulama seçtik. Kod örneklerinin çoğunda atlanan bir uyarı, uzak bir arıza durumunda dosyayı yerel olarak okumaya geri dönmemizdir.

Bunun dışında, bu uygulamanın bir paket alıcısı var. Paket alıcının uzak dosya sisteminden veya yerel dosya sisteminden dosya alabilmesini sağlamalıyız.

Saf Yaklaşım: sadece kitaplığı ve sistem düzeyinde aramaları arayın

Saf yaklaşım, uygulama paketimizin getter.New(...) öğesini çağıracağı ve ona uzak veya yerel dosya alımını ayarlamak için gereken bilgileri ileteceği ve bir Getter döndüreceğidir. Döndürülen değer daha sonra uzak veya yerel dosyayı bulmak için gereken parametrelerle MyGetter.GetFile(...) öğesini çağırabilecektir.

Bu bize temel yapımızı verecektir. Yeni Getter oluşturduğumuzda, olası herhangi bir uzak dosya getirme (bir erişim anahtarı ve sır) için gerekli olan parametreleri başlatırız ve ayrıca, kodun denenmesini söyleyen useRemoteFS gibi uygulama yapılandırmamızdan kaynaklanan bazı değerleri de iletiriz. uzak dosya sistemi.

Bazı temel işlevler sağlamamız gerekiyor. Buradaki saf kodu kontrol edin [4]; aşağıda azaltılmış bir sürümdür. Not, bu bitmemiş bir örnek ve bazı şeyleri yeniden düzenleyeceğiz.

Buradaki temel fikir, uzak dosya sisteminden okumak üzere yapılandırıldıysak ve uzak dosya sistemi ayrıntılarını (ana bilgisayar, paket ve anahtar) alırsak, uzak dosya sisteminden okumaya çalışmamız gerektiğidir. Sistemin uzaktan okumasına güvendikten sonra, tüm dosya okumalarını uzak dosya sistemine kaydıracağız ve yerel dosya sisteminden okumaya yönelik referansları kaldıracağız.

Bu kod çok birim testi dostu değildir; Nasıl çalıştığını doğrulamak için yalnızca yerel dosya sistemine değil, uzak dosya sistemine de vurmamız gerektiğini unutmayın. Şimdi, sadece bir entegrasyon testi yapabilir ve koddaki mutlu yolu doğrulamamıza izin veren bir s3 örneğine sahip olmak için biraz Docker büyüsü kurabiliriz.

Birim testleri, alternatif kod ve hata yollarını kolayca test ederek daha sağlam yazılımlar tasarlamamıza yardımcı olduğundan, yalnızca entegrasyon testine sahip olmak ideal değildir. Entegrasyon testlerini daha büyük “gerçekten çalışıyor mu” testleri için kaydetmeliyiz. Şimdilik, birim testlerine odaklanalım.

Bu kodu nasıl daha birim test edilebilir hale getirebiliriz? İki düşünce okulu vardır. Bunlardan biri, alayları test ederken kullanım için ortak kod oluşturan bir sahte oluşturucu (https://github.com/vektra/mockery veya https://github.com/golang/mock gibi) kullanmaktır.

Bu rotaya gidebilir ve dosya sistemi çağrılarını ve Minio istemci çağrılarını oluşturabilirsiniz. Ya da belki bir bağımlılıktan kaçınmak istersiniz, bu yüzden alaylarınızı elle oluşturursunuz. Somut olarak yazılmış bir nesne döndüren somut olarak yazılmış bir istemciniz olduğundan, Minio istemcisiyle alay etmenin kolay olmadığı ortaya çıktı.

Alay etmekten daha iyi bir yol olduğunu söylüyorum. Kodumuzu daha test edilebilir olacak şekilde yeniden yapılandırırsak, alaylar ve ilgili cruft için ek içe aktarmalara ihtiyacımız olmaz ve arayüzleri güvenle test etmek için ek test DSL'lerini bilmeye gerek kalmaz. Kodumuzu aşırı bağlı olmayacak şekilde ayarlayabiliriz ve test kodu Go'nun arayüzlerini kullanarak normal Go kodu olacaktır. Haydi Yapalım şunu!

Arayüz Yaklaşımı: Daha fazla soyutlama, daha kolay test etme

Test etmemiz gereken şey nedir? Bazı yeni Gophers'ların işleri yanlış anladığı yer burasıdır. İnsanların arayüzlerden yararlanmanın değerini anladığını gördüm, ancak kullandıkları paketin somut uygulamasına uyan arayüzlere ihtiyaçları olduğunu hissediyorum.

Bir Minio istemcimiz olduğunu görebilirler, bu nedenle Minio istemcisinin (veya başka bir s3 istemcisinin) TÜM yöntemleri ve kullanımlarıyla eşleşen arabirimler oluşturarak başlayabilirler. “Arayüz ne kadar büyükse, soyutlama o kadar zayıftır” şeklindeki Go Atasözünü [5] [6] unuturlar.

Minio istemcisine karşı test etmemize gerek yok. Dosyaları uzaktan veya yerel olarak alabileceğimizi test etmemiz (ve uzak hatalar gibi bazı hata yollarını doğrulamamız) gerekiyor. Bu ilk yaklaşımı yeniden değerlendirelim ve Minio istemcisini uzak bir alıcıya çekelim. Bunu yaparken yerel dosya okuma kodumuza da aynısını yapalım ve yerel bir alıcı yapalım. İşte temel arayüzler ve her birini uygulayacak türümüz olacak:

Bu soyutlamalar yerindeyken, ilk uygulamamızı yeniden gözden geçirebiliriz. LocalFetcher ve remoteFetcher'ı Getter yapısına koyacağız ve bunları kullanmak için GetFile'ı yeniden düzenleyeceğiz. Yeniden düzenlenmiş kodun tam sürümünü buradan kontrol edin [7]. Aşağıda, yeni arayüz sürümünü kullanan biraz basitleştirilmiş bir pasaj bulunmaktadır:

Bu yeni, yeniden düzenlenmiş kod çok daha fazla birim test edilebilir çünkü Getter yapısında parametre olarak arabirimleri alıyoruz ve sahte türler için somut türleri değiştirebiliyoruz. İşletim sistemi çağrılarıyla alay etmek veya Minio istemcisi veya büyük arayüzlerle tam bir alaya ihtiyaç duymak yerine, sadece iki basit sahteye ihtiyacımız var: fakeLocalFetcher ve fakeRemoteFetcher .

Bu sahtelerin üzerinde, ne döndürdüklerini belirtmemize izin veren bazı özellikler var. Dosya verilerini veya istediğimiz herhangi bir hatayı döndürebileceğiz ve çağıran GetFile yönteminin verileri ve hataları istediğimiz gibi işlediğini doğrulayabiliriz.

Bunu akılda tutarak, testlerin kalbi şu hale gelir:

Bu temel yapı ile, hepsini tabloya dayalı testlerde toplayabiliriz [8]. Test tablosundaki her durum, yerel veya uzak dosya erişimi için test edilecektir. Uzak veya yerel dosya erişiminde bir hata enjekte edebileceğiz. Yayılan hataları, dosya içeriğinin aktarıldığını ve beklenen günlük girişlerinin mevcut olduğunu doğrulayabiliriz.

Devam ettim ve tüm olası test senaryolarını ve permütasyonları burada mevcut olan tek bir tabloya dayalı teste dahil ettim [9] (bazı yöntem imzalarının biraz farklı olduğunu not edebilirsiniz - bu bize bir kaydedici enjekte etme ve günlük ifadelerine karşı doğrulama gibi şeyler yapmamıza izin verir) ).

Şık, ha? GetFile'ın nasıl davranmasını istediğimiz konusunda tam kontrole sahibiz ve sonuçlara karşı iddiada bulunabiliriz. Kodumuzu birim testi dostu olacak şekilde tasarladık ve artık GetFile yönteminde uygulanan başarı ve hata yollarını doğrulayabiliyoruz.

Kod gevşek bir şekilde birleştirilmiştir ve gelecekte yeniden düzenleme yapmak çok kolay olacaktır. Bunu, Go'ya aşina olan herhangi bir geliştiricinin anlayabileceği ve gerektiğinde genişletebileceği düz ol 'Go kodu yazarak yaptık.

Alaylar: peki ya küçük, cesur uygulama detayları?

Alaylar bize önerilen çözümde bulamadığımız ne satın alacak? Geleneksel bir denemenin faydasını gösteren harika bir soru, "s3 istemcisini doğru parametrelerle çağırdığınızı nereden biliyorsunuz? Sahtelerle, anahtar değerini kova parametresine değil, anahtar parametresine ilettiğimden emin olabilirim."

Bu geçerli bir endişedir ve bir yerde bir test kapsamında ele alınmalıdır. Burada savunduğum test yaklaşımı, Minio istemcisini kepçe ve anahtar parametrelerle doğru sırada aradığınızı doğrulamaz.

Yakın zamanda okuduğum harika bir alıntı, “Alay etmek, riski getiren varsayımları ortaya çıkarır [10]” dedi. İstemci kitaplığının doğru uygulandığını varsayıyorsunuz, tüm sınırların katı olduğunu varsayıyorsunuz, kitaplığın gerçekte nasıl davrandığını bildiğinizi varsayıyorsunuz.

Kütüphaneyle alay etmek yalnızca varsayımlarla alay eder ve kodu güncellediğinizde testlerinizi daha kırılgan ve değişebilir hale getirir (Martin Fowler'ın Mocks Aren't Stubs [3]'te vardığı sonuç budur). Lastik yolla buluştuğunda, Minio istemcisini gerçekten doğru kullandığımızı doğrulamamız gerekecek ve bu, entegrasyon testleri anlamına gelir (bunlar bir Docker kurulumunda veya bir test ortamında bulunabilir). Hem birim hem de entegrasyon testlerine sahip olacağımız için, entegrasyon testi bunu kapsadığı için tam uygulamayı kapsayacak bir birim testine gerek yoktur.

Örneğimizde, birim testleri kod tasarımımıza rehberlik eder ve hataların ve mantık akışlarının tasarlandığı gibi çalıştığını ve tam olarak yapmaları gereken şeyi yaparak hızlı bir şekilde test etmemizi sağlar.
Bazıları için bunun yeterli birim test kapsamı olmadığını düşünüyorlar. Yukarıdaki noktalar için endişeleniyorlar. Bazıları, bir arayüzün başka bir arayüz döndürdüğü başka bir arayüz döndürdüğü, belki de aşağıdaki gibi, Rus bebek tarzı arayüzlerde ısrar edecek:

Ardından Minio istemcisinin her bir parçasını her bir paketleyiciye çekebilir ve ardından bir sahte oluşturucu kullanabilir (yapılara ve testlere bağımlılıklar ekleyerek, varsayımları artırarak ve işleri daha kırılgan hale getirerek). Sonunda, alaycı şöyle bir şey söyleyebilecek:

myClientMock.ExpectsCall("GetObject").Returns(mockObject).NumberOfCalls(1).WithArgs(anahtar, kova) – ve eğer bu özel DSL için doğru büyüyü hatırlayabiliyorsanız.

Bu, Minio istemcisini kullanmanın uygulama seçimine doğrudan bağlı çok fazla ekstra soyutlama olacaktır. Bu, müşteri hakkındaki varsayımlarımızı değiştirmemiz gerektiğini veya tamamen farklı bir müşteriye ihtiyacımız olduğunu öğrendiğimizde kırılgan testlere neden olacaktır.

Bu, şimdi ve gelecekte uçtan uca kod geliştirme süresini artırır, kod karmaşıklığını artırır ve okunabilirliği azaltır, potansiyel olarak sahte oluşturuculara olan bağımlılıkları artırır ve bize, kova ile anahtar parametreleri karıştırıp karıştırmadığımızı bilmenin şüpheli ek değerini verir. Zaten entegrasyon testinde keşfedecektik.

Gittikçe daha fazla nesne ortaya çıktıkça, bağlantı daha da sıkılaşıyor. Bir günlükçü alayı yapmış olabiliriz ve daha sonra bir metrik alayı yapmaya başlarız. Farkına bile varmadan, bir günlük girişi veya yeni bir ölçüm ekliyorsunuz ve ek bir ölçümün gelmesini beklemeyen onca testi geçtiniz.

Go'da en son buna maruz kaldığımda, alaycı çerçeve paniklediği ve korkunç bir ölümle öldüğü için hangi testin veya dosyanın başarısız olduğunu bile söylemedi çünkü yeni bir metrikle karşılaştı (bu, testleri yorumlayarak ikili aramayı gerektiriyordu). sahte davranışı değiştirmemiz gereken yeri bulabilmek için). Sahteler değer katabilir mi? Emin. Fiyatına değer mi? Çoğu durumda, ikna olmadım.

Arayüzler: kazanç için basitlik ve birim testi

Go'da arayüzlerin basit kullanımıyla tasarımı yönlendirebileceğimizi ve uygun kod ve hata yollarının izlenmesini sağlayabileceğimizi gösterdik. Arayüzlere bağlı basit sahteler yazarak, test için tasarlanmış kod oluşturmak için alaylara, alaycı çerçevelere veya sahte oluşturuculara ihtiyacımız olmadığını görebiliriz. Ayrıca birim testinin her şey olmadığını ve sistemlerin birbiriyle düzgün şekilde entegre edildiğinden emin olmak için entegrasyon testleri yazmanız gerektiğini not ettik.

Gelecekte entegrasyon testlerini çalıştırmanın bazı düzgün yolları hakkında bir yazı almayı umuyorum; bizi izlemeye devam edin!

Referanslar

1: Endo-Test: Sahte Nesnelerle Birim Testi (2000): Sahte nesnenin tanımı için girişe bakın
2: Küçük Alaycı: Sahtelerle ilgili bölüme bakın, özellikle, "Sahte bir iş davranışına sahiptir. Farklı veriler vererek bir sahtekarı farklı şekillerde davranmaya yönlendirebilirsiniz.”
3: Sahtekarlıklar Saplama Değildir: "Öyleyse klasikçi mi yoksa alaycı mı olmalıyım?" bölümüne bakın. Martin Fowler, "Sahte TDD için herhangi bir zorlayıcı fayda görmüyorum ve birleştirme testlerinin uygulamaya geçirilmesinin sonuçları konusunda endişeliyim" diyor.
4: Saf Yaklaşım: kodun basitleştirilmiş bir versiyonu. Bkz. [7].
5: https://go-proverbs.github.io/: Görüşmelere bağlantılar içeren Go Atasözleri listesi.
6: https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s: Arayüz boyutu ve soyutlama ile ilgili olarak Rob Pike'ın konuşması için doğrudan bağlantı.
7: Demo kodunun tam sürümü: depoyu klonlayabilir ve "go testi" çalıştırabilirsiniz.
8: Tabloya dayalı testler: Tekrarlamayı azaltmak için test kodunu düzenlemek için bir test stratejisi.
9: Demo kodunun tam sürümü için testler. Bunları 'go testi' ile çalıştırabilirsiniz.
10: Test Yazarken Kendinize Sormanız Gereken Sorular Michal Charemza: Alaycılık varsayımları, varsayımlar ise riski getirir.