Flutter'da Birim Testi: İş Akışı Temellerinden Karmaşık Senaryolara
Yayınlanan: 2022-09-21Flutter'a olan ilgi tüm zamanların en yüksek seviyesinde ve çok geç kalınmış durumda. Google'ın açık kaynaklı SDK'sı Android, iOS, macOS, web, Windows ve Linux ile uyumludur. Tek bir Flutter kod tabanı hepsini destekler. Ve birim testi, tutarlı ve güvenilir bir Flutter uygulaması sunmada, birleştirilmeden önce kodun kalitesini önceden iyileştirerek hatalara, kusurlara ve kusurlara karşı koruma sağlamada etkilidir.
Bu eğitimde, Flutter birim testi için iş akışı optimizasyonlarını paylaşıyor, temel bir Flutter birim testi gösteriyor ve ardından daha karmaşık Flutter test senaryoları ve kitaplıklarına geçiyoruz.
Flutter'da Birim Testinin Akışı
Flutter'da birim testini diğer teknoloji yığınlarında yaptığımızla aynı şekilde uygularız:
- Kodu değerlendirin.
- Veri alayını ayarlayın.
- Test gruplarını tanımlayın.
- Her test grubu için test işlevi imzasını/imzalarını tanımlayın.
- Testleri yazın.
Birim testini göstermek için örnek bir Flutter projesi hazırladım ve sizi boş zamanlarınızda kodu kullanıp test etmeye teşvik ediyorum. Proje, ülkeye göre filtreleyebileceğimiz üniversitelerin listesini getirmek ve görüntülemek için harici bir API kullanıyor.
Flutter'ın nasıl çalıştığı hakkında birkaç not: Çerçeve, bir proje oluşturulduğunda flutter_test
kitaplığını otomatik olarak yükleyerek testi kolaylaştırır. Kitaplık, Flutter'ın birim testlerini okumasını, çalıştırmasını ve analiz etmesini sağlar. Flutter ayrıca testlerin depolanacağı test
klasörünü otomatik olarak oluşturur. test
klasörünü yeniden adlandırmaktan ve/veya taşımaktan kaçınmak önemlidir, çünkü bu, işlevselliğini ve dolayısıyla testleri çalıştırma yeteneğimizi bozar. Flutter'ın test dosyalarını tanıma şekli bu sonek olduğundan, test dosya adlarımıza _test.dart
dahil etmek de önemlidir.
Dizin Yapısını Test Et
Projemizde birim testini teşvik etmek için, kaynak kodu alt klasörleri için seçilen adlarda gösterildiği gibi, temiz mimari ve bağımlılık ekleme (DI) ile MVVM'yi uyguladık. MVVM ve DI ilkelerinin birleşimi, endişelerin ayrılmasını sağlar:
- Her proje sınıfı tek bir hedefi destekler.
- Bir sınıf içindeki her işlev yalnızca kendi kapsamını yerine getirir.
Yazacağımız test dosyaları için organize bir depolama alanı oluşturacağız, test gruplarının kolayca tanımlanabilecek “evlere” sahip olacağı bir sistem. Flutter'ın testleri test
klasörü içinde bulma gereksinimi ışığında, kaynak kodumuzun test
altındaki klasör yapısını yansıtalım. Daha sonra bir test yazdığımızda uygun alt klasöre kaydedeceğiz: Nasıl temiz çoraplar şifonyerinizin çorap çekmecesine, katlanmış gömlekler gömlek çekmecesine gidiyorsa, Model
sınıflarının birim testleri model
adlı bir klasöre gider. , örneğin.
Bu dosya sistemini benimsemek, projede şeffaflık sağlar ve ekibe, kodumuzun hangi bölümlerinin ilişkili testlere sahip olduğunu görmenin kolay bir yolunu sunar.
Artık birim testini eyleme geçirmeye hazırız.
Basit Bir Çarpıntı Birimi Testi
model
sınıflarıyla (kaynak kodun data
katmanında) başlayacağız ve örneğimizi yalnızca bir model
sınıfı, ApiUniversityModel
içerecek şekilde sınırlayacağız. Bu sınıf iki işleve sahiptir:
- JSON nesnesini
Map
ile taklit ederek modelimizi başlatın. -
University
veri modelini oluşturun.
Modelin işlevlerinin her birini test etmek için daha önce açıklanan evrensel adımları özelleştireceğiz:
- Kodu değerlendirin.
- Veri alayını ayarlayın: API çağrımıza sunucu yanıtını tanımlayacağız.
- Test gruplarını tanımlayın: Her fonksiyon için bir tane olmak üzere iki test grubumuz olacak.
- Her test grubu için test işlevi imzalarını tanımlayın.
- Testleri yazın.
Kodumuzu değerlendirdikten sonra, ikinci hedefimizi gerçekleştirmeye hazırız: ApiUniversityModel
sınıfındaki iki işleve özel veri alayı kurmak.
İlk işlevi alay etmek için ( fromJson
bir Map
ile alay ederek modelimizi başlatmak), Json öğesinden, işlev için giriş verilerini simüle etmek üzere iki Map
nesnesi oluşturacağız. Sağlanan girdiyle işlevin beklenen sonucunu temsil etmek için iki eşdeğer ApiUniversityModel
nesnesi de oluşturacağız.
İkinci işlevi ( University
veri modelini oluşturma) taklit etmek için, toDomain
, bu işlevi daha önce başlatılmış ApiUniversityModel
nesnelerinde çalıştırdıktan sonra beklenen sonuç olan iki University
nesnesi oluşturacağız:
void main() { Map<String, dynamic> apiUniversityOneAsJson = { "alpha_two_code": "US", "domains": ["marywood.edu"], "country": "United States", "state-province": null, "web_pages": ["http://www.marywood.edu"], "name": "Marywood University" }; ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel( alphaCode: "US", country: "United States", state: null, name: "Marywood University", websites: ["http://www.marywood.edu"], domains: ["marywood.edu"], ); University expectedUniversityOne = University( alphaCode: "US", country: "United States", state: "", name: "Marywood University", websites: ["http://www.marywood.edu"], domains: ["marywood.edu"], ); Map<String, dynamic> apiUniversityTwoAsJson = { "alpha_two_code": "US", "domains": ["lindenwood.edu"], "country": "United States", "state-province":"MJ", "web_pages": null, "name": "Lindenwood University" }; ApiUniversityModel expectedApiUniversityTwo = ApiUniversityModel( alphaCode: "US", country: "United States", state:"MJ", name: "Lindenwood University", websites: null, domains: ["lindenwood.edu"], ); University expectedUniversityTwo = University( alphaCode: "US", country: "United States", state: "MJ", name: "Lindenwood University", websites: [], domains: ["lindenwood.edu"], ); }
Ardından, üçüncü ve dördüncü hedeflerimiz için test gruplarımızı ve test fonksiyon imzalarımızı tanımlamak için tanımlayıcı bir dil ekleyeceğiz:
void main() { // Previous declarations group("Test ApiUniversityModel initialization from JSON", () { test('Test using json one', () {}); test('Test using json two', () {}); }); group("Test ApiUniversityModel toDomain", () { test('Test toDomain using json one', () {}); test('Test toDomain using json two', () {}); }); }
fromJson işlevini kontrol etmek için iki testin ve fromJson
işlevini kontrol etmek için iki testin imzasını toDomain
.
Beşinci hedefimizi gerçekleştirmek ve testleri yazmak için, fonksiyonların sonuçlarını beklentilerimizle karşılaştırmak için flutter_test kitaplığının expect
yöntemini kullanalım:
void main() { // Previous declarations group("Test ApiUniversityModel initialization from json", () { test('Test using json one', () { expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson), expectedApiUniversityOne); }); test('Test using json two', () { expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson), expectedApiUniversityTwo); }); }); group("Test ApiUniversityModel toDomain", () { test('Test toDomain using json one', () { expect(ApiUniversityModel.fromJson(apiUniversityOneAsJson).toDomain(), expectedUniversityOne); }); test('Test toDomain using json two', () { expect(ApiUniversityModel.fromJson(apiUniversityTwoAsJson).toDomain(), expectedUniversityTwo); }); }); }
Beş hedefimizi gerçekleştirdikten sonra, testleri artık IDE'den veya komut satırından çalıştırabiliriz.
Bir terminalde, flutter test
komutunu girerek test
klasöründeki tüm testleri çalıştırabilir ve testlerimizin geçtiğini görebiliriz.
Alternatif olarak, flutter test --plain-name "ReplaceWithName"
komutunu girerek, test veya test grubumuzun adını ReplaceWithName
ile değiştirerek tek bir test veya test grubu çalıştırabiliriz.
Flutter'da Bir Uç Noktayı Test Eden Birim
Bağımlılık içermeyen basit bir testi tamamladıktan sonra, daha ilginç bir örneği keşfedelim: Kapsamı aşağıdakileri içeren endpoint
sınıfını test edeceğiz:
- Sunucuya bir API çağrısı yürütme.
- API JSON yanıtını farklı bir biçime dönüştürme.
Kodumuzu değerlendirdikten sonra, test grubumuzdaki sınıfları başlatmak için flutter_test kitaplığının setUp
yöntemini kullanacağız:
group("Test University Endpoint API calls", () { setUp(() { baseUrl = "https://test.url"; dioClient = Dio(BaseOptions()); endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl); }); }
API'lere ağ istekleri yapmak için gerekli kodun çoğunu üreten güçlendirme kitaplığını kullanmayı tercih ederim. UniversityEndpoint
sınıfını düzgün bir şekilde test etmek için, Retrofit
API çağrılarını yürütmek için kullandığı dio kitaplığını, özel bir yanıt adaptörü aracılığıyla Dio
sınıfının davranışını taklit ederek istenen sonucu döndürmeye zorlayacağız.
Özel Ağ Engelleyici Mock
UniversityEndpoint
sınıfını DI aracılığıyla oluşturmamız nedeniyle alay etmek mümkündür. ( UniversityEndpoint
sınıfı kendi başına bir Dio
sınıfını başlatacak olsaydı, sınıfın davranışıyla alay etmemizin hiçbir yolu olmazdı.)
Dio
sınıfının davranışıyla alay etmek için, Retrofit
kitaplığında kullanılan Dio
yöntemlerini bilmemiz gerekir, ancak Dio
doğrudan erişimimiz yoktur. Bu nedenle, özel bir ağ yanıtı önleyici kullanarak Dio
ile alay edeceğiz:
class DioMockResponsesAdapter extends HttpClientAdapter { final MockAdapterInterceptor interceptor; DioMockResponsesAdapter(this.interceptor); @override void close({bool force = false}) {} @override Future<ResponseBody> fetch(RequestOptions options, Stream<Uint8List>? requestStream, Future? cancelFuture) { if (options.method == interceptor.type.name.toUpperCase() && options.baseUrl == interceptor.uri && options.queryParameters.hasSameElementsAs(interceptor.query) && options.path == interceptor.path) { return Future.value(ResponseBody.fromString( jsonEncode(interceptor.serializableResponse), interceptor.responseCode, headers: { "content-type": ["application/json"] }, )); } return Future.value(ResponseBody.fromString( jsonEncode( {"error": "Request doesn't match the mock interceptor details!"}), -1, statusMessage: "Request doesn't match the mock interceptor details!")); } } enum RequestType { GET, POST, PUT, PATCH, DELETE } class MockAdapterInterceptor { final RequestType type; final String uri; final String path; final Map<String, dynamic> query; final Object serializableResponse; final int responseCode; MockAdapterInterceptor(this.type, this.uri, this.path, this.query, this.serializableResponse, this.responseCode); }
Artık ağ yanıtlarımızla alay etmek için önleyiciyi oluşturduğumuza göre, test gruplarımızı ve test işlevi imzalarımızı tanımlayabiliriz.
Bizim durumumuzda test edilecek tek bir fonksiyonumuz var ( getUniversitiesByCountry
), bu yüzden sadece bir test grubu oluşturacağız. Fonksiyonumuzun üç duruma tepkisini test edeceğiz:
-
Dio
sınıfının işlevi gerçektengetUniversitiesByCountry
tarafından mı çağrılıyor? - API isteğimiz bir hata döndürürse ne olur?
- API isteğimiz beklenen sonucu verirse ne olur?
İşte test grubumuz ve test fonksiyonu imzalarımız:
group("Test University Endpoint API calls", () { test('Test endpoint calls dio', () async {}); test('Test endpoint returns error', () async {}); test('Test endpoint calls and returns 2 valid universities', () async {}); });
Testlerimizi yazmaya hazırız. Her bir test durumu için, ilgili konfigürasyona sahip bir DioMockResponsesAdapter
örneği oluşturacağız:
group("Test University Endpoint API calls", () { setUp(() { baseUrl = "https://test.url"; dioClient = Dio(BaseOptions()); endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl); }); test('Test endpoint calls dio', () async { dioClient.httpClientAdapter = _createMockAdapterForSearchRequest( 200, [], ); var result = await endpoint.getUniversitiesByCountry("us"); expect(result, <ApiUniversityModel>[]); }); test('Test endpoint returns error', () async { dioClient.httpClientAdapter = _createMockAdapterForSearchRequest( 404, {"error": "Not found!"}, ); List<ApiUniversityModel>? response; DioError? error; try { response = await endpoint.getUniversitiesByCountry("us"); } on DioError catch (dioError, _) { error = dioError; } expect(response, null); expect(error?.error, "Http status error [404]"); }); test('Test endpoint calls and returns 2 valid universities', () async { dioClient.httpClientAdapter = _createMockAdapterForSearchRequest( 200, generateTwoValidUniversities(), ); var result = await endpoint.getUniversitiesByCountry("us"); expect(result, expectedTwoValidUniversities()); }); });
Artık uç nokta testimiz tamamlandığına göre, veri kaynağı sınıfımızı UniversityRemoteDataSource
test edelim. Daha önce, UniversityEndpoint
sınıfının UniversityRemoteDataSource({UniversityEndpoint? universityEndpoint})
yapıcısının bir parçası olduğunu gözlemledik; bu, UniversityEndpoint
UniversityRemoteDataSource
kullandığını gösterir, bu nedenle alay edeceğimiz sınıf budur.
Mockito ile alay etmek
Önceki örneğimizde, özel bir NetworkInterceptor
kullanarak Dio
istemcimizin istek adaptörüyle manuel olarak alay ettik. Burada bütün bir sınıfla alay ediyoruz. Bunu manuel olarak yapmak (bir sınıfla ve işlevleriyle alay etmek) zaman alıcı olacaktır. Neyse ki, sahte kitaplıklar bu tür durumlarla başa çıkmak için tasarlanmıştır ve minimum çabayla sahte sınıflar oluşturabilir. Flutter'da alay etmek için endüstri standardı kitaplık olan mockito kitaplığını kullanalım.
Mockito
ile alay etmek için, önce test kodunun önüne “ @GenerateMocks([class_1,class_2,…])
” ek açıklamasını ekliyoruz— void main() {}
işlevinin hemen üstüne. Açıklamada, parametre olarak sınıf adlarının bir listesini ekleyeceğiz ( class_1,class_2…
yerine).
Ardından, testle aynı dizinde sahte sınıflarımız için kod üreten Flutter'ın flutter pub run build_runner build
komutunu çalıştırıyoruz. Ortaya çıkan sahte dosyanın adı, testin .dart
son ekinin yerini alacak şekilde test dosyası adı ile .mocks.dart
birleşimi olacaktır. Dosyanın içeriği, adları Mock
önekiyle başlayan sahte sınıfları içerecektir. Örneğin, UniversityEndpoint
MockUniversityEndpoint
olur.
Şimdi, university_remote_data_source_test.dart.mocks.dart
(sahte dosyamız) university_remote_data_source_test.dart
(test dosyası) aktarıyoruz.
Ardından, setUp
işlevinde MockUniversityEndpoint
kullanarak ve UniversityRemoteDataSource
sınıfını başlatarak UniversityEndpoint
ile alay edeceğiz:
import 'university_remote_data_source_test.mocks.dart'; @GenerateMocks([UniversityEndpoint]) void main() { late UniversityEndpoint endpoint; late UniversityRemoteDataSource dataSource; group("Test function calls", () { setUp(() { endpoint = MockUniversityEndpoint(); dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint); }); }
UniversityEndpoint
ile başarıyla alay ettik ve ardından UniversityRemoteDataSource
sınıfımızı başlattık. Artık test gruplarımızı tanımlamaya ve fonksiyon imzalarını test etmeye hazırız:
group("Test function calls", () { test('Test dataSource calls getUniversitiesByCountry from endpoint', () {}); test('Test dataSource maps getUniversitiesByCountry response to Stream', () {}); test('Test dataSource maps getUniversitiesByCountry response to Stream with error', () {}); });
Bununla alay, test gruplarımız ve test fonksiyon imzalarımız kurulur. Gerçek testleri yazmaya hazırız.
İlk testimiz, veri kaynağı ülke bilgilerini getirmeyi başlattığında UniversityEndpoint
işlevinin çağrılıp çağrılmadığını kontrol eder. Her sınıfın işlevleri çağrıldığında nasıl tepki vereceğini tanımlayarak başlıyoruz. UniversityEndpoint
sınıfıyla alay ettiğimiz için, when( function_that_will_be_called ).then( what_will_be_returned )
kod yapısını kullanarak bu sınıfla çalışacağız.
Test ettiğimiz işlevler eşzamansızdır (bir Future
nesnesi döndüren işlevler), bu nedenle sonuçlarımızı değiştirmek için when(function name).thenanswer( (_) {modified function result} )
kod yapısını kullanacağız.
getUniversitiesByCountry
işlevinin UniversityEndpoint
sınıfındaki getUniversitiesByCountry
işlevini çağırıp çağırmadığını kontrol etmek için, when(...).thenAnswer( (_) {...} )
kullanarak UniversityEndpoint
sınıfındaki getUniversitiesByCountry
işleviyle alay edeceğiz:
when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));
Yanıtımızla alay ettiğimize göre, veri kaynağı işlevini çağırır ve verify
işlevini kullanarak UniversityEndpoint
işlevinin çağrılıp çağrılmadığını kontrol ederiz:
test('Test dataSource calls getUniversitiesByCountry from endpoint', () { when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[])); dataSource.getUniversitiesByCountry("test"); verify(endpoint.getUniversitiesByCountry("test")); });
Aynı ilkeleri, fonksiyonumuzun uç nokta sonuçlarımızı ilgili veri akışlarına doğru bir şekilde dönüştürüp dönüştürmediğini kontrol eden ek testler yazmak için kullanabiliriz:
import 'university_remote_data_source_test.mocks.dart'; @GenerateMocks([UniversityEndpoint]) void main() { late UniversityEndpoint endpoint; late UniversityRemoteDataSource dataSource; group("Test function calls", () { setUp(() { endpoint = MockUniversityEndpoint(); dataSource = UniversityRemoteDataSource(universityEndpoint: endpoint); }); test('Test dataSource calls getUniversitiesByCountry from endpoint', () { when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[])); dataSource.getUniversitiesByCountry("test"); verify(endpoint.getUniversitiesByCountry("test")); }); test('Test dataSource maps getUniversitiesByCountry response to Stream', () { when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[])); expect( dataSource.getUniversitiesByCountry("test"), emitsInOrder([ const AppResult<List<University>>.loading(), const AppResult<List<University>>.data([]) ]), ); }); test( 'Test dataSource maps getUniversitiesByCountry response to Stream with error', () { ApiError mockApiError = ApiError( statusCode: 400, message: "error", errors: null, ); when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.error(mockApiError)); expect( dataSource.getUniversitiesByCountry("test"), emitsInOrder([ const AppResult<List<University>>.loading(), AppResult<List<University>>.apiError(mockApiError) ]), ); }); }); }
Bir dizi Flutter birim testi gerçekleştirdik ve alay etmeye farklı yaklaşımlar gösterdik. Sizi ek testler yapmak için örnek Flutter projemi kullanmaya devam etmeye davet ediyorum.
Çarpıntı Birimi Testleri: Üstün UX İçin Anahtarınız
Flutter projelerinize zaten birim testini dahil ettiyseniz, bu makale iş akışınıza ekleyebileceğiniz bazı yeni seçenekler sunmuş olabilir. Bu eğitimde, birim testini bir sonraki Flutter projenize dahil etmenin ne kadar basit olacağını ve daha incelikli test senaryolarının zorluklarının nasıl üstesinden gelineceğini gösterdik. Flutter'da bir daha asla birim testlerini atlamak istemeyebilirsiniz.
Toptal Engineering Blog'un editör ekibi, bu makalede sunulan kod örneklerini ve diğer teknik içeriği gözden geçirdikleri için Matija Becirevic ve Paul Hoskins'e şükranlarını sunar.