علاقه به فلاتر در حد یک است اوج تمام دوران-و خیلی وقته مونده. SDK منبع باز Google با اندروید، iOS، macOS، وب، ویندوز و لینوکس سازگار است. یک پایگاه کد فلاتر از همه آنها پشتیبانی می کند. و تست واحد در ارائه یک برنامه Flutter سازگار و قابل اعتماد، با بهبود پیشگیرانه کیفیت کد قبل از مونتاژ، از خطاها، نقص ها و نقص ها اطمینان حاصل می کند.
در این آموزش، ما بهینهسازیهای گردش کار را برای تست واحد فلاتر به اشتراک میگذاریم، یک تست واحد فلاتر را نشان میدهیم، سپس به موارد و کتابخانههای پیچیدهتر تست فلاتر میرویم.
جریان تست واحد در فلوتر
ما تست واحد را در Flutter به همان روشی که در سایر پشته های فناوری انجام می دهیم پیاده سازی می کنیم:
- کد را ارزیابی کنید
- تمسخر داده را تنظیم کنید.
- گروه(های) آزمون را تعریف کنید.
- امضا(های) تابع آزمون را برای هر گروه آزمایشی تعریف کنید.
- تست ها را بنویسید
برای نشان دادن تست واحد، من یک را آماده کرده ام نمونه پروژه فلاتر و شما را تشویق به استفاده و تست کد در اوقات فراغت خود می کند. پروژه از یک API خارجی برای واکشی و نمایش لیستی از دانشگاه هایی که می توانیم بر اساس کشور فیلتر کنیم.
چند نکته در مورد نحوه عملکرد Flutter: این چارچوب تست را با بارگذاری خودکار تسهیل می کند flutter_test
کتابخانه زمانی که یک پروژه ایجاد می شود. این کتابخانه فلاتر را قادر می سازد تا تست های واحد را بخواند، اجرا کند و تجزیه و تحلیل کند. فلاتر نیز به صورت خودکار ایجاد می کند test
پوشه ای که در آن تست ها ذخیره می شوند. اجتناب از تغییر نام و/یا جابجایی آن بسیار مهم است test
پوشه، زیرا عملکرد آن و در نتیجه توانایی ما برای اجرای آزمایشها را از بین میبرد. گنجاندن آن نیز ضروری است _test.dart
در نام فایل های آزمایشی ما، زیرا این پسوند نحوه تشخیص فایل های آزمایشی توسط Flutter است.
تست ساختار دایرکتوری
برای ترویج تست واحد در پروژه خود، MVVM را با معماری تمیز و تزریق وابستگی (DI)، همانطور که در نام های انتخاب شده برای زیرپوشه های کد منبع مشهود است. ترکیبی از اصول MVVM و DI جداسازی نگرانی ها را تضمین می کند:
- هر کلاس پروژه از یک هدف واحد پشتیبانی می کند.
- هر تابع در یک کلاس فقط محدوده خود را برآورده می کند.
ما یک فضای ذخیره سازی سازمان یافته برای فایل های آزمایشی که می نویسیم ایجاد خواهیم کرد، سیستمی که در آن گروه هایی از تست ها “خانه هایی” به راحتی قابل شناسایی خواهند داشت. با توجه به نیاز فلاتر به مکان یابی تست ها در داخل test
پوشه، بیایید ساختار پوشه کد منبع خود را در زیر بازتاب دهیم test
. سپس، وقتی تستی می نویسیم، آن را در زیر پوشه مناسب ذخیره می کنیم: همانطور که جوراب های تمیز در کشوی جوراب کمد شما و پیراهن های تا شده در کشوی پیراهن می روند، تست های واحد Model
کلاس ها در پوشه ای به نام model
، مثلا.
اتخاذ این فایل سیستم شفافیت را در پروژه ایجاد می کند و به تیم یک راه آسان برای مشاهده اینکه کدام بخش از کد ما دارای تست های مرتبط است را می دهد.
ما اکنون آماده هستیم تا آزمایش واحد را وارد عمل کنیم.
تست واحد فلوتر ساده
ما با شروع خواهیم کرد model
کلاس ها (در data
لایه کد منبع) و مثال ما را محدود می کند که فقط یک مورد را شامل شود model
کلاس، ApiUniversityModel
. این کلاس دارای دو عملکرد است:
- مدل خود را با تمسخر شی JSON با a مقداردهی کنید
Map
. - را بسازید
University
مدل داده.
برای آزمایش هر یک از عملکردهای مدل، مراحل جهانی که قبلاً توضیح داده شد را سفارشی می کنیم:
- کد را ارزیابی کنید
- راه اندازی تمسخر داده: ما پاسخ سرور به تماس API خود را تعریف می کنیم.
- گروه های آزمایشی را تعریف کنید: ما دو گروه آزمایشی خواهیم داشت، یکی برای هر تابع.
- امضای تابع آزمون را برای هر گروه آزمایشی تعریف کنید.
- تست ها را بنویسید
پس از ارزیابی کد خود، ما آماده هستیم تا هدف دوم خود را به انجام برسانیم: راهاندازی تمسخر دادهها برای دو عملکرد در داخل ApiUniversityModel
کلاس
برای مسخره کردن تابع اول (مدل ما را با مسخره کردن JSON با a راه اندازی می کنیم 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": ["
"name": "Marywood University"
};
ApiUniversityModel expectedApiUniversityOne = ApiUniversityModel(
alphaCode: "US",
country: "United States",
state: null,
name: "Marywood University",
websites: ["
domains: ["marywood.edu"],
);
University expectedUniversityOne = University(
alphaCode: "US",
country: "United States",
state: "",
name: "Marywood University",
websites: ["
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
عملکرد.
برای تحقق هدف پنجم و نوشتن تستها، از عبارت استفاده میکنیم کتابخانه flutter_test‘s expect
روش مقایسه نتایج توابع در برابر انتظارات ما:
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);
});
});
}
پس از انجام پنج هدف خود، اکنون میتوانیم تستها را از IDE یا از خط فرمان اجرا کنیم.
در یک ترمینال، ما می توانیم تمام تست های موجود در داخل را اجرا کنیم test
پوشه با وارد کردن flutter test
دستور دهید و ببینید که تست های ما با موفقیت انجام می شود.
از طرف دیگر، میتوانیم یک آزمون یا گروه آزمایشی را با وارد کردن آن اجرا کنیم flutter test --plain-name "ReplaceWithName"
دستور، جایگزین نام گروه آزمایشی یا آزمایشی ما ReplaceWithName
.
واحد تست نقطه پایانی در فلوتر
پس از تکمیل یک تست ساده بدون وابستگی، بیایید یک مثال جالب تر را بررسی کنیم: ما آن را آزمایش می کنیم endpoint
کلاس، که دامنه آن شامل:
- اجرای یک تماس API با سرور.
- تبدیل پاسخ API JSON به فرمت دیگری.
پس از ارزیابی کد خود، از آن استفاده خواهیم کرد کتابخانه flutter_test‘s setUp
روش برای مقداردهی اولیه کلاس ها در گروه آزمایشی ما:
group("Test University Endpoint API calls", () {
setUp(() {
baseUrl = "
dioClient = Dio(BaseOptions());
endpoint = UniversityEndpoint(dioClient, baseUrl: baseUrl);
});
}
برای درخواست شبکه به API ها، ترجیح می دهم از آن استفاده کنم کتابخانه مقاوم سازی، که بیشتر کدهای لازم را تولید می کند. برای تست صحیح UniversityEndpoint
کلاس، ما به زور کتابخانه دیو– کدام Retrofit
برای اجرای فراخوانی های API استفاده می کند — برای بازگرداندن نتیجه مورد نظر با تمسخر Dio
رفتار کلاس از طریق یک آداپتور پاسخ سفارشی.
شبیه سازی رهگیر شبکه سفارشی
تمسخر به دلیل ساختن ما امکان پذیر است UniversityEndpoint
کلاس از طریق DI. (اگر UniversityEndpoint
کلاس باید a را مقداردهی اولیه کند 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);
}
اکنون که رهگیر را برای تمسخر پاسخهای شبکه خود ایجاد کردهایم، میتوانیم گروههای آزمایشی و امضاهای تابع آزمایشی خود را تعریف کنیم.
در مورد ما، ما فقط یک تابع برای آزمایش داریم (getUniversitiesByCountry
، بنابراین ما فقط یک گروه آزمایشی ایجاد می کنیم. ما پاسخ تابع خود را در سه موقعیت آزمایش می کنیم:
- هست
Dio
تابع کلاس در واقع توسط فراخوانی شده استgetUniversitiesByCountry
? - اگر درخواست API ما خطایی را برگرداند، چه اتفاقی میافتد؟
- اگر درخواست 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 = "
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
، ابتدا حاشیه نویسی را اضافه می کنیم@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 )
ساختار کد
توابعی که ما آزمایش می کنیم ناهمزمان هستند (توابعی که a را برمی گرداند 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)
]),
);
});
});
}
ما تعدادی تست واحد فلاتر را اجرا کرده ایم و رویکردهای مختلفی را برای تمسخر نشان داده ایم. من از شما دعوت می کنم به استفاده از من ادامه دهید نمونه پروژه فلاتر برای اجرای تست های اضافی
تست های واحد فلوتر: کلید شما برای UX برتر
اگر قبلاً تست واحد را در پروژههای فلاتر خود وارد کردهاید، این مقاله ممکن است گزینههای جدیدی را معرفی کرده باشد که میتوانید به گردش کار خود تزریق کنید. در این آموزش، ما نشان دادیم که چقدر ساده است که آزمایش واحد را در پروژه بعدی فلاتر خود بگنجانید و چگونه با چالشهای سناریوهای آزمایشی دقیقتر مقابله کنید. شاید دیگر هرگز نخواهید از تست های واحد در فلاتر رد شوید.
تیم تحریریه وبلاگ مهندسی Toptal از Matija Bečirević و Paul Hoskins برای بررسی نمونه کد و سایر محتوای فنی ارائه شده در این مقاله تشکر می کند.