การทดสอบหน่วยใน Flutter: จากสิ่งจำเป็นสำหรับเวิร์กโฟลว์ไปจนถึงสถานการณ์ที่ซับซ้อน

เผยแพร่แล้ว: 2022-09-21

ความสนใจใน Flutter นั้นสูงเป็นประวัติการณ์—และเกินกำหนดมานาน SDK โอเพ่นซอร์สของ Google เข้ากันได้กับ Android, iOS, macOS, เว็บ, Windows และ Linux Codebase Flutter เดียวรองรับทั้งหมด และการทดสอบหน่วยเป็นเครื่องมือในการนำเสนอแอป Flutter ที่สม่ำเสมอและเชื่อถือได้ โดยจะช่วยป้องกันข้อผิดพลาด ข้อบกพร่อง และข้อบกพร่องโดยการปรับปรุงคุณภาพของโค้ดล่วงหน้าก่อนที่จะประกอบ

ในบทช่วยสอนนี้ เราแชร์การเพิ่มประสิทธิภาพเวิร์กโฟลว์สำหรับการทดสอบหน่วย Flutter สาธิตการทดสอบหน่วย Flutter พื้นฐาน จากนั้นไปยังกรณีการทดสอบและไลบรารี Flutter ที่ซับซ้อนยิ่งขึ้น

การไหลของการทดสอบหน่วยใน Flutter

เราใช้การทดสอบหน่วยใน Flutter ในลักษณะเดียวกับที่เราทำในเทคโนโลยีอื่นๆ:

  1. ประเมินรหัส
  2. ตั้งค่าการจำลองข้อมูล
  3. กำหนดกลุ่มทดสอบ
  4. กำหนดลายเซ็นของฟังก์ชันการทดสอบสำหรับแต่ละกลุ่มการทดสอบ
  5. เขียนแบบทดสอบ

เพื่อสาธิตการทดสอบหน่วย ฉันได้เตรียมตัวอย่างโปรเจ็กต์ Flutter และสนับสนุนให้คุณใช้และทดสอบโค้ดในยามว่าง โปรเจ็กต์นี้ใช้ API ภายนอกเพื่อดึงและแสดงรายชื่อมหาวิทยาลัยที่เราสามารถกรองตามประเทศได้

หมายเหตุเล็กน้อยเกี่ยวกับวิธีการทำงานของ Flutter: เฟรมเวิร์กช่วยอำนวยความสะดวกในการทดสอบโดยโหลดไลบรารี flutter_test โดยอัตโนมัติเมื่อสร้างโปรเจ็กต์ ไลบรารีช่วยให้ Flutter สามารถอ่าน เรียกใช้ และวิเคราะห์การทดสอบหน่วยได้ Flutter ยังสร้างโฟลเดอร์ test โดยอัตโนมัติเพื่อจัดเก็บการทดสอบ สิ่งสำคัญคือต้องหลีกเลี่ยงการเปลี่ยนชื่อและ/หรือย้ายโฟลเดอร์ test เนื่องจากจะทำให้ฟังก์ชันการทำงานเสียหาย และด้วยเหตุนี้ ความสามารถในการเรียกใช้การทดสอบของเราจึงเป็นสิ่งสำคัญ สิ่งสำคัญคือต้องรวม _test.dart ไว้ในชื่อไฟล์ทดสอบของเรา เนื่องจากส่วนต่อท้ายนี้คือวิธีที่ Flutter รู้จักไฟล์ทดสอบ

โครงสร้างไดเร็กทอรีทดสอบ

เพื่อส่งเสริมการทดสอบหน่วยในโครงการของเรา เราได้นำ MVVM ไปใช้ด้วยสถาปัตยกรรมที่สะอาดและการฉีดพึ่งพา (DI) ตามหลักฐานในชื่อที่เลือกสำหรับโฟลเดอร์ย่อยซอร์สโค้ด การผสมผสานหลักการ MVVM และ DI ช่วยให้มั่นใจถึงการแยกข้อกังวล:

  1. แต่ละชั้นโครงการสนับสนุนวัตถุประสงค์เดียว
  2. แต่ละฟังก์ชันภายในคลาสจะเติมเต็มขอบเขตของตัวเองเท่านั้น

เราจะสร้างพื้นที่จัดเก็บที่เป็นระเบียบสำหรับไฟล์ทดสอบที่เราจะเขียน ซึ่งเป็นระบบที่กลุ่มการทดสอบจะมี "บ้าน" ที่ระบุได้ง่าย ตามข้อกำหนดของ Flutter ในการค้นหาตำแหน่งการทดสอบภายในโฟลเดอร์ test ให้จำลองโครงสร้างโฟลเดอร์ของซอร์สโค้ดภายใต้ test จากนั้นเมื่อเราเขียนการทดสอบ เราจะจัดเก็บไว้ในโฟลเดอร์ย่อยที่เหมาะสม เช่นเดียวกับถุงเท้าที่สะอาดอยู่ในลิ้นชักถุงเท้าของโต๊ะเครื่องแป้งและเสื้อที่พับอยู่ในลิ้นชักเสื้อ การทดสอบหน่วยของคลาส Model จะไปในโฟลเดอร์ชื่อ model , ตัวอย่างเช่น.

โครงสร้างโฟลเดอร์ไฟล์ที่มีสองโฟลเดอร์ระดับแรก: lib และ test ซ้อนอยู่ใต้ lib เรามีโฟลเดอร์คุณสมบัติ ซ้อนเพิ่มเติมคือ university_feed และซ้อนเพิ่มเติมคือข้อมูล โฟลเดอร์ข้อมูลประกอบด้วยที่เก็บและโฟลเดอร์ต้นทาง ซ้อนอยู่ใต้โฟลเดอร์ต้นทางคือโฟลเดอร์เครือข่าย ที่ซ้อนอยู่ใต้เครือข่ายคือโฟลเดอร์ปลายทางและรุ่น รวมทั้งไฟล์ university_remote_data_source.dart ในโฟลเดอร์ model คือไฟล์ api_university_model.dart ในระดับเดียวกับโฟลเดอร์ university_feed ที่กล่าวถึงก่อนหน้านี้คือโฟลเดอร์โดเมนและการนำเสนอ ซ้อนอยู่ใต้โดเมนคือโฟลเดอร์ usecase ซ้อนอยู่ใต้การนำเสนอคือรุ่นและโฟลเดอร์หน้าจอ โครงสร้างของโฟลเดอร์ทดสอบที่กล่าวถึงก่อนหน้านี้เลียนแบบของ lib ซ้อนอยู่ใต้โฟลเดอร์ทดสอบคือโฟลเดอร์ unit_test ซึ่งมีโฟลเดอร์ university_feed โครงสร้างโฟลเดอร์เหมือนกับโฟลเดอร์ university_feed ด้านบน โดยมีไฟล์ dart ที่มี "_test" ต่อท้ายชื่อ
โครงสร้างโฟลเดอร์ทดสอบของโปรเจ็กต์ที่จำลองโครงสร้างซอร์สโค้ด

การนำระบบไฟล์นี้มาใช้จะสร้างความโปร่งใสให้กับโปรเจ็กต์และช่วยให้ทีมดูได้ง่ายๆ ว่าโค้ดส่วนใดบ้างที่มีการทดสอบที่เกี่ยวข้อง

ตอนนี้เราพร้อมที่จะนำการทดสอบหน่วยไปปฏิบัติแล้ว

การทดสอบหน่วย Flutter อย่างง่าย

เราจะเริ่มด้วยคลาส model (ในชั้น data ของซอร์สโค้ด) และจะจำกัดตัวอย่างของเราให้รวมคลาส model เท่านั้น ApiUniversityModel คลาสนี้มีฟังก์ชั่นสองอย่าง:

  • เริ่มต้นโมเดลของเราด้วยการเยาะเย้ยวัตถุ JSON ด้วย Map
  • สร้างแบบจำลองข้อมูล University

ในการทดสอบแต่ละฟังก์ชันของโมเดล เราจะปรับแต่งขั้นตอนสากลที่อธิบายไว้ก่อนหน้านี้:

  1. ประเมินรหัส
  2. ตั้งค่าการเยาะเย้ยข้อมูล: เราจะกำหนดการตอบสนองของเซิร์ฟเวอร์ต่อการเรียก API ของเรา
  3. กำหนดกลุ่มทดสอบ: เราจะมีกลุ่มทดสอบสองกลุ่ม กลุ่มหนึ่งสำหรับแต่ละฟังก์ชัน
  4. กำหนดลายเซ็นฟังก์ชันการทดสอบสำหรับแต่ละกลุ่มการทดสอบ
  5. เขียนแบบทดสอบ

หลังจากประเมินโค้ดของเราแล้ว เราก็พร้อมที่จะบรรลุวัตถุประสงค์ที่สองของเรา: เพื่อตั้งค่าการเยาะเย้ยข้อมูลเฉพาะสำหรับสองฟังก์ชันภายในคลาส ApiUniversityModel

ในการเยาะเย้ยฟังก์ชันแรก (เริ่มต้นโมเดลของเราโดยจำลอง JSON ด้วย Map ) fromJson เราจะสร้างออบเจ็กต์ Map สองรายการเพื่อจำลองข้อมูลอินพุตสำหรับฟังก์ชัน เราจะสร้างวัตถุ ApiUniversityModel ที่เทียบเท่ากันสองรายการเพื่อแสดงผลลัพธ์ที่คาดหวังของฟังก์ชันด้วยอินพุตที่ให้มา

ในการเยาะเย้ยฟังก์ชันที่สอง (การสร้างโมเดลข้อมูลของ University ) toDomain เราจะสร้างสองอ็อบเจ็กต์ University ซึ่งเป็นผลลัพธ์ที่คาดหวังหลังจากเรียกใช้ฟังก์ชันนี้ในอ็อบเจ็กต์ ApiUniversityModel ที่สร้างอินสแตนซ์ก่อนหน้านี้:

 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"], ); }

ต่อไป สำหรับวัตถุประสงค์ที่สามและสี่ เราจะเพิ่มภาษาอธิบายเพื่อกำหนดกลุ่มการทดสอบและลายเซ็นของฟังก์ชันการทดสอบ:

 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 และอีกสองรายการเพื่อตรวจสอบฟังก์ชัน toDomain

เพื่อบรรลุวัตถุประสงค์ที่ห้าของเราและเขียนการทดสอบ ลองใช้วิธีการ expect ของไลบรารี flutter_test เพื่อเปรียบเทียบผลลัพธ์ของฟังก์ชันกับความคาดหวังของเรา:

 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); }); }); }

เมื่อบรรลุวัตถุประสงค์ทั้ง 5 ประการแล้ว ตอนนี้เราสามารถเรียกใช้การทดสอบได้จาก IDE หรือจากบรรทัดคำสั่ง

ภาพหน้าจอระบุว่าห้าในห้าการทดสอบผ่าน อ่านส่วนหัว: เรียกใช้: การทดสอบใน api_university_model_test.dart แผงด้านซ้ายของหน้าจออ่านว่า: ผลการทดสอบ---กำลังโหลด api_university_model_test.dart---api_university_model_test.dart---ทดสอบ ApiUniversityModel เริ่มต้นจาก json---ทดสอบโดยใช้ json one---ทดสอบโดยใช้ json two---ทดสอบ ApiUniversityModel toDomain ---ทดสอบ toDomain โดยใช้ json one---ทดสอบ toDomain โดยใช้ json two แผงด้านขวาของหน้าจออ่านว่า: การทดสอบที่ผ่าน: การทดสอบห้าจากห้าครั้ง---การทดสอบการกระพือปีก/unit_test/universities_feed/data/source/network/model/api_university_model_test.dart

ที่เทอร์มินัล เราสามารถเรียกใช้การทดสอบทั้งหมดที่อยู่ในโฟลเดอร์ test ได้โดยการป้อนคำสั่ง flutter test และเห็นว่าการทดสอบของเราผ่าน

อีกทางหนึ่ง เราสามารถเรียกใช้การทดสอบกลุ่มเดียวหรือกลุ่มทดสอบโดยป้อนคำสั่ง flutter test --plain-name "ReplaceWithName" แทนที่ชื่อกลุ่มทดสอบหรือกลุ่มทดสอบสำหรับ ReplaceWithName

หน่วยทดสอบจุดปลายใน Flutter

หลังจากเสร็จสิ้นการทดสอบอย่างง่ายโดยไม่มีการขึ้นต่อกัน มาสำรวจตัวอย่างที่น่าสนใจกันดีกว่า: เราจะทดสอบคลาส endpoint ซึ่งมีขอบเขตครอบคลุม:

  • ดำเนินการเรียก API ไปยังเซิร์ฟเวอร์
  • การแปลงการตอบสนอง API JSON เป็นรูปแบบอื่น

หลังจากประเมินโค้ดของเราแล้ว เราจะใช้วิธี setUp ของไลบรารี setUp เพื่อเริ่มต้นคลาสภายในกลุ่มทดสอบของเรา:

 group("Test University Endpoint API calls", () { setUp(() { baseUrl = "https://test.url"; dioClient = Dio(BaseOptions()); endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl); }); }

หากต้องการส่งคำขอเครือข่ายไปยัง API ฉันชอบใช้ไลบรารีสำหรับติดตั้งเพิ่มเติม ซึ่งสร้างโค้ดที่จำเป็นส่วนใหญ่ เพื่อทดสอบคลาส UniversityEndpoint อย่างถูกต้อง เราจะบังคับให้ไลบรารี dio ซึ่ง Retrofit ใช้ในการเรียกใช้ API เพื่อส่งคืนผลลัพธ์ที่ต้องการโดยจำลองพฤติกรรมของคลาส Dio ผ่านอะแด็ปเตอร์การตอบสนองที่กำหนดเอง

Custom Network Interceptor Mock

การเยาะเย้ยเป็นไปได้เนื่องจากเราได้สร้างคลาส UniversityEndpoint ผ่าน DI (หากคลาส UniversityEndpoint เริ่มต้นคลาส Dio ด้วยตัวเอง เราจะไม่มีทางล้อเลียนพฤติกรรมของชั้นเรียนได้)

เพื่อล้อเลียนพฤติกรรมของคลาส Dio เราจำเป็นต้องรู้วิธี Dio ที่ใช้ในไลบรารี Retrofit แต่เราไม่สามารถเข้าถึง Dio ได้โดยตรง ดังนั้น เราจะล้อเลียน Dio โดยใช้ตัวสกัดกั้นการตอบสนองเครือข่ายแบบกำหนดเอง:

 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); }

ตอนนี้เราได้สร้าง interceptor เพื่อเยาะเย้ยการตอบสนองของเครือข่ายแล้ว เราสามารถกำหนดกลุ่มการทดสอบและลายเซ็นของฟังก์ชันการทดสอบได้

ในกรณีของเรา เรามีฟังก์ชันเดียวที่จะทดสอบ ( getUniversitiesByCountry ) ดังนั้นเราจะสร้างกลุ่มทดสอบเพียงกลุ่มเดียว เราจะทดสอบการตอบสนองของฟังก์ชันของเราต่อสามสถานการณ์:

  1. ฟังก์ชันของคลาส Dio ถูกเรียกโดย getUniversitiesByCountry จริงหรือ
  2. หากคำขอ API ของเราส่งคืนข้อผิดพลาด จะเกิดอะไรขึ้น
  3. หากคำขอ API ของเราส่งคืนผลลัพธ์ที่คาดไว้ จะเกิดอะไรขึ้น

นี่คือกลุ่มทดสอบและลายเซ็นฟังก์ชันการทดสอบของเรา:

 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 {}); });

เราพร้อมที่จะเขียนแบบทดสอบของเรา สำหรับแต่ละกรณีทดสอบ เราจะสร้างอินสแตนซ์ของ DioMockResponsesAdapter ด้วยการกำหนดค่าที่เกี่ยวข้อง:

 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()); }); });

เมื่อการทดสอบปลายทางของเราเสร็จสมบูรณ์แล้ว มาทดสอบคลาสแหล่งข้อมูลของเรากันดีกว่า UniversityRemoteDataSource ก่อนหน้านี้ เราสังเกตว่าคลาส UniversityEndpoint เป็นส่วนหนึ่งของคอนสตรัคเตอร์ UniversityRemoteDataSource({UniversityEndpoint? universityEndpoint}) ซึ่งระบุว่า UniversityRemoteDataSource ใช้คลาส UniversityEndpoint เพื่อบรรลุขอบเขต ดังนั้นนี่คือคลาสที่เราจะล้อเลียน

เยาะเย้ยกับม็อกคิโต

ในตัวอย่างก่อนหน้านี้ เราจำลองคำขอของไคลเอ็นต์ Dio ด้วยตนเองโดยใช้ NetworkInterceptor ที่กำหนดเอง ที่นี่เรากำลังเยาะเย้ยทั้งชั้นเรียน การทำเช่นนี้ด้วยตนเอง—เยาะเย้ยชั้นเรียนและหน้าที่ของคลาส—จะใช้เวลานาน โชคดีที่ไลบรารีจำลองได้รับการออกแบบมาเพื่อจัดการกับสถานการณ์ดังกล่าว และสามารถสร้างคลาสจำลองได้โดยใช้ความพยายามเพียงเล็กน้อย มาใช้ห้องสมุด mockito ห้องสมุดมาตรฐานอุตสาหกรรมสำหรับการเยาะเย้ยใน Flutter

ในการเยาะเย้ย Mockito อื่นเราเพิ่มคำอธิบายประกอบ “ @GenerateMocks([class_1,class_2,…]) ” ก่อนโค้ดของการทดสอบ ซึ่งอยู่เหนือฟังก์ชัน void main() {} ในคำอธิบายประกอบ เราจะรวมรายชื่อคลาสเป็นพารามิเตอร์ (แทนที่ class_1,class_2… )

ต่อไป เรารันคำสั่ง flutter pub run build_runner build ที่สร้างโค้ดสำหรับคลาสจำลองของเราในไดเร็กทอรีเดียวกันกับการทดสอบ ชื่อไฟล์จำลองที่เป็นผลลัพธ์จะเป็นการรวมกันของชื่อไฟล์ทดสอบและ .mocks.dart โดยแทนที่ส่วนต่อท้าย . .dart ของการทดสอบ เนื้อหาของไฟล์จะรวมถึงคลาสจำลองที่มีชื่อขึ้นต้นด้วยคำนำหน้า Mock ตัวอย่างเช่น UniversityEndpoint กลายเป็น MockUniversityEndpoint

ตอนนี้ เรานำเข้า university_remote_data_source_test.dart.mocks.dart (ไฟล์จำลองของเรา) ลงใน university_remote_data_source_test.dart (ไฟล์ทดสอบ)

จากนั้น ในฟังก์ชัน setUp เราจะจำลอง UniversityEndpoint โดยใช้ MockUniversityEndpoint และเริ่มต้นคลาส UniversityRemoteDataSource :

 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 สำเร็จแล้วจึงเริ่มต้นคลาส UniversityRemoteDataSource ของเรา ตอนนี้เราพร้อมที่จะกำหนดกลุ่มทดสอบและลายเซ็นฟังก์ชันการทดสอบแล้ว:

 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', () {}); });

ด้วยเหตุนี้ เราจึงตั้งค่าการเยาะเย้ย กลุ่มทดสอบ และฟังก์ชันการทดสอบของเรา เราพร้อมที่จะเขียนแบบทดสอบจริง

การทดสอบครั้งแรกของเราตรวจสอบว่ามีการเรียกใช้ฟังก์ชัน UniversityEndpoint หรือไม่เมื่อแหล่งข้อมูลเริ่มต้นการดึงข้อมูลประเทศ เราเริ่มต้นด้วยการกำหนดว่าแต่ละคลาสจะตอบสนองอย่างไรเมื่อมีการเรียกฟังก์ชัน เนื่องจากเราล้อเลียนคลาส UniversityEndpoint นั่นคือคลาสที่เราจะทำงานด้วย โดยใช้โครงสร้างโค้ด when( function_that_will_be_called ).then( what_will_be_returned )

ฟังก์ชันที่เรากำลังทดสอบเป็นแบบอะซิงโครนัส (ฟังก์ชันที่ส่งคืนอ็อบเจ็กต์ Future ) ดังนั้นเราจะใช้โครงสร้างโค้ด when(function name).thenanswer( (_) {modified function result} ) เพื่อแก้ไขผลลัพธ์ของเรา

เพื่อตรวจสอบว่าฟังก์ชัน getUniversitiesByCountry เรียกใช้ฟังก์ชัน getUniversitiesByCountry ภายในคลาส UniversityEndpoint หรือไม่ เราจะใช้ when(...).thenAnswer( (_) {...} ) เพื่อจำลองฟังก์ชัน getUniversitiesByCountry ภายในคลาส UniversityEndpoint :

 when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[]));

ตอนนี้เราได้ล้อเลียนคำตอบของเราแล้ว เราเรียกฟังก์ชันแหล่งข้อมูลและตรวจสอบ—โดยใช้ฟังก์ชัน verify —ไม่ว่าจะเรียกฟังก์ชัน UniversityEndpoint หรือไม่:

 test('Test dataSource calls getUniversitiesByCountry from endpoint', () { when(endpoint.getUniversitiesByCountry("test")) .thenAnswer((realInvocation) => Future.value(<ApiUniversityModel>[])); dataSource.getUniversitiesByCountry("test"); verify(endpoint.getUniversitiesByCountry("test")); });

เราสามารถใช้หลักการเดียวกันนี้ในการเขียนการทดสอบเพิ่มเติมที่ตรวจสอบว่าฟังก์ชันของเราแปลงผลลัพธ์ปลายทางของเราเป็นสตรีมข้อมูลที่เกี่ยวข้องอย่างถูกต้องหรือไม่:

 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) ]), ); }); }); }

เราได้ทำการทดสอบหน่วย Flutter หลายครั้งและสาธิตวิธีการเยาะเย้ยที่แตกต่างกัน ฉันขอเชิญให้คุณใช้โปรเจ็กต์ Flutter ตัวอย่างของฉันต่อไปเพื่อทำการทดสอบเพิ่มเติม

การทดสอบหน่วย Flutter: กุญแจสู่ UX ที่เหนือกว่า

หากคุณรวมการทดสอบหน่วยเข้ากับโปรเจ็กต์ Flutter แล้ว บทความนี้อาจแนะนำตัวเลือกใหม่ๆ ที่คุณสามารถใส่ลงในเวิร์กโฟลว์ของคุณได้ ในบทช่วยสอนนี้ เราแสดงให้เห็นว่าการรวมการทดสอบหน่วยเข้ากับโปรเจ็กต์ Flutter ถัดไปของคุณนั้นตรงไปตรงมาเพียงใด และวิธีจัดการกับความท้าทายของสถานการณ์การทดสอบที่เหมาะสมยิ่งขึ้น คุณอาจไม่ต้องการข้ามการทดสอบหน่วยใน Flutter อีกต่อไป

ทีมบรรณาธิการของ Toptal Engineering Blog ขอขอบคุณ Matija Becirevic และ Paul Hoskins สำหรับการตรวจสอบตัวอย่างโค้ดและเนื้อหาทางเทคนิคอื่นๆ ที่นำเสนอในบทความนี้