SlideShare a Scribd company logo
初めてFlutterアプリを
作ってテストを書いた話
DevFest Kyoto 2021
自己紹介
加藤正憲
Mobile App Developer@クックビズ株式会社
 @nitakanworld 
やってること:モバイルアプリの開発・運用
(主にFlutter/Android/iOS)
趣味:キャンプ/ロードバイク
2
● はじめてのFlutterアプリ
● テストができない!
● 依存関係をシンプルにしよう
● テストを書こう
● 振り返って
● まとめ
アジェンダ
3
● 発表者が初めてのFlutterプロジェクトで経験した
ことです
● 内容に1年前の事柄があるので今はよりよい方法
があるやもしれません
● Integrationテストはしていません
注意事項
4
Flutterアプリつくったけどどうやってテスト書いて
いけばいいの?
にひとつの道を示せられればいいかなと思います
目的
5
初めてのFlutterアプリ
6
● toB向けサービスのモバイルアプリ
● WEBサービスの一部機能
● Flutterで初めて作った
○ アプリ2名API1名体制
○ 3ヶ月の開発期間
○ 勉強しつつ作り始めた
初めてのFlutterアプリ
7
テストができない!
8
● コードを触るときにデグレしないようにしたい
● 計算ロジックを網羅テストしたい
● Widgetの表示状態をテストしたい
○ 表示しているか
○ テキストの文字列編集が想定通りか
そろそろテストを書こう
9
● クラス内で他クラスをnewしている
● 依存関係が複雑になっている
● Widgetクラスでデータの変換処理をしている
アプリがテスト可能な状態ではなかった
10
● クラス内で他クラスをnewしている
● 依存関係が複雑になっている
● Widgetクラスでデータの変換処理をしている
アプリがテスト可能な状態ではなかった
😇 11
テストできるようにしよう
12
● クリーンアーキテクチャ
● MVVM+α
まずは依存関係をきれいにしよう
13
クリーンアーキテクチャ+MVVMで再構成
https://nrslib.com/clean-architecture-with-java
14
クリーンアーキテクチャ+MVVMで再構成
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
🤔
15
クリーンアーキテクチャで再構成
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● 依存方向は一方向のみ
● ドメイン層はどこにも依存しない
16
クリーンアーキテクチャで再構成
DataSource
Repository
Domain
Widget/Controller
UseCase
17
● DI(Dependency Injection)
依存を注入しよう
18
● DI
○ コンストラクタで依存を注入
○ テストダブルなクラスに切り替えられるように
依存を注入しよう
19
 
class SendMessageInteractor implements SendMessageUseCase {
SendMessageInteractor(this._messageRepository);
static const String _replyPrefix = SendMessage.replyMessageTitlePrefix;
final MessageRepository _messageRepository;
...
}
● コンストラクタで依存を注入
依存を注入しよう
20
 
class SendMessageInteractor implements SendMessageUseCase {
SendMessageInteractor(this._messageRepository);
static const String _replyPrefix = SendMessage.replyMessageTitlePrefix;
final MessageRepository _messageRepository;
...
}
● コンストラクタで依存を注入
依存を注入しよう
21
● DI
○ コンストラクタで依存を注入
○ テストダブルなクラスに切り替えられるように
● ServiceLocator(GetIt)を導入
依存を注入しよう
22
● DI
○ コンストラクタで依存を注入
○ テストダブルなクラスに切り替えられるように
● ServiceLocator(GetIt)を導入
○ 各層でインターフェースと実装を紐付け
依存を注入しよう
23
 
void installUseCases(GetIt getIt) {
getIt.registerFactory<SendMessageUseCase>(
() => SendMessageInteractor(getIt.get<MessageRepository>(),
));
}
● ServiceLocatorへ登録
依存を注入しよう
24
 
final _messageUseCase = GetIt.I.get<SendMessageUseCase>();
await _messageUseCase.handle(messageId, message);
● ServiceLocatorから取得して使用
依存を注入しよう
25
● DI
○ コンストラクタで依存を注入
○ テストダブルなクラスに切り替えられるように
● ServiceLocator(GetIt)を導入
○ 各層でインターフェースと実装を紐付け
○ インターフェースは暗黙的に作られる
依存を注入しよう
26
 
// A person. The implicit interface contains greet().
class Person {
// Not in the interface, since this is a constructor.
Person(this._name);
// In the interface, but visible only in this library.
final String _name;
// In the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}
// An implementation of the Person interface.
class Impostor implements Person {
@override
String get _name => '';
@override
String greet(String who) => 'Hi $who. Do you know who I am?';
}
https://dart.dev/guides/language/language-tour#implicit-interfaces 27
 
// A person. The implicit interface contains greet().
class Person {
// Not in the interface, since this is a constructor.
Person(this._name);
// In the interface, but visible only in this library.
final String _name;
// In the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}
// An implementation of the Person interface.
class Impostor implements Person {
@override
String get _name => '';
@override
String greet(String who) => 'Hi $who. Do you know who I am?';
}
https://dart.dev/guides/language/language-tour#implicit-interfaces 28
テストを書こう
29
各層をテストしよう
Domain
View
(Widget)
Controller DataSource
UseCase Repository
● 各層でどんなテストをしたのか
30
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● 計算ロジック / 表示条件ロジックのテスト
● テキスト編集のテスト
31
 
void main() {
test('Test JobPostingId displayId max line', () {
const maxJobPostingId = JobPostingId(123456);
expect(maxJobPostingId.displayId, 'DpJob123-456');
});
test('Test JobPostingId displayId min line', () {
const minJobPostingId = JobPostingId(1);
expect(minJobPostingId.displayId, 'DpJob000-001');
});
}
32
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● APIのレスポンスデータ変換ロジック(含エラー)
● APIのリクエストを検証
33
 
// レスポンスの変換
test('現在のスカウト数が取得できていること ', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.get, 200, contractsJson);
final ds = MockedDependencies.get<CompanyDataSource>();
final res = await ds.getContracts();
expect(res,
const ContractsResponse(OfferStockResponse(9999, 10, 9990), null));
expect(MockedDependencies.apiConnection.lastUri.toString(),
'${MockedDependencies.apiConnection.getHost()}/api/contracts');
});
34
 
// レスポンスの変換
test('現在のスカウト数が取得できていること ', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.get, 200, contractsJson);
final ds = MockedDependencies.get<CompanyDataSource>();
final res = await ds.getContracts();
expect(res,
const ContractsResponse(OfferStockResponse(9999, 10, 9990), null));
expect(MockedDependencies.apiConnection.lastUri.toString(),
'${MockedDependencies.apiConnection.getHost()}/api/contracts');
});
35
 
// レスポンスの変換
test('現在のスカウト数が取得できていること ', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.get, 200, contractsJson);
final ds = MockedDependencies.get<CompanyDataSource>();
final res = await ds.getContracts();
expect(res,
const ContractsResponse(OfferStockResponse(9999, 10, 9990), null));
expect(MockedDependencies.apiConnection.lastUri.toString(),
'${MockedDependencies.apiConnection.getHost()}/api/contracts');
});
36
 
// レスポンスの変換
test('現在のスカウト数が取得できていること ', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.get, 200, contractsJson);
final ds = MockedDependencies.get<CompanyDataSource>();
final res = await ds.getContracts();
expect(res,
const ContractsResponse(OfferStockResponse(9999, 10, 9990), null));
expect(MockedDependencies.apiConnection.lastUri.toString(),
'${MockedDependencies.apiConnection.getHost()}/api/contracts');
});
37
 
// レスポンスの変換(エラー時)
test('Test post message error', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.post, 403, accessDenied);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re:Test', body: 'TEST');
await expectThrows(() => ds.sendMessage(request),
const TypeMatcher<DataSourceAccessDeniedException>());
});
38
 
// レスポンスの変換(エラー時)
test('Test post message error', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.post, 403, accessDenied);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re:Test', body: 'TEST');
await expectThrows(() => ds.sendMessage(request),
const TypeMatcher<DataSourceAccessDeniedException>());
});
39
 
// レスポンスの変換(エラー時)
test('Test post message error', () async {
MockedDependencies.apiConnection
.enqueueResponse(MethodType.post, 403, accessDenied);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re:Test', body: 'TEST');
await expectThrows(() => ds.sendMessage(request),
const TypeMatcher<DataSourceAccessDeniedException>());
});
40
 
// Exceptionの検証(Non Future)
expect(() => hogehoge(), throwsA(TypeMatcher<HogeException>()));
// Exceptionの検証(Future)
Future<void> expectThrows(FutureCreator creator, dynamic matcher) async {
try {
await creator.call();
fail('Expect an error.');
} on Exception catch (e) {
expect(e, matcher);
// ignore: avoid_catching_errors
} on Error catch (e) {
expect(e, matcher);
}
}
41
 
// Exceptionの検証(Non Future)
expect(() => hogehoge(), throwsA(TypeMatcher<HogeException>()));
// Exceptionの検証(Future)
Future<void> expectThrows(FutureCreator creator, dynamic matcher) async {
try {
await creator.call();
fail('Expect an error.');
} on Exception catch (e) {
expect(e, matcher);
// ignore: avoid_catching_errors
} on Error catch (e) {
expect(e, matcher);
}
}
42
 
// リクエストの検証
test('Test post message', () async {
final api = MockedDependencies.apiConnection
..enqueueResponse(MethodType.post, 201, created);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re: Test', body: 'BodyTest');
await ds.sendMessage(request);
expect(api.lastUri.toString(), '${api.getHost()}/api/messages');
expect(api.lastBody['title'], 'Re: Test');
expect(api.lastBody['body'], 'BodyTest');
expect(api.lastBody['matching_connection_id'], '1');
});
43
 
// リクエストの検証
test('Test post message', () async {
final api = MockedDependencies.apiConnection
..enqueueResponse(MethodType.post, 201, created);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re: Test', body: 'BodyTest');
await ds.sendMessage(request);
expect(api.lastUri.toString(), '${api.getHost()}/api/messages');
expect(api.lastBody['title'], 'Re: Test');
expect(api.lastBody['body'], 'BodyTest');
expect(api.lastBody['matching_connection_id'], '1');
});
44
 
// リクエストの検証
test('Test post message', () async {
final api = MockedDependencies.apiConnection
..enqueueResponse(MethodType.post, 201, created);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re: Test', body: 'BodyTest');
await ds.sendMessage(request);
expect(api.lastUri.toString(), '${api.getHost()}/api/messages');
expect(api.lastBody['title'], 'Re: Test');
expect(api.lastBody['body'], 'BodyTest');
expect(api.lastBody['matching_connection_id'], '1');
});
45
 
// リクエストの検証
test('Test post message', () async {
final api = MockedDependencies.apiConnection
..enqueueResponse(MethodType.post, 201, created);
final ds = MockedDependencies.get<MessagesApiDataSource>();
const request = SendMessageRequestParameter(
matchingConnectionId: 1, title: 'Re: Test', body: 'BodyTest');
await ds.sendMessage(request);
expect(api.lastUri.toString(), '${api.getHost()}/api/messages');
expect(api.lastBody['title'], 'Re: Test');
expect(api.lastBody['body'], 'BodyTest');
expect(api.lastBody['matching_connection_id'], '1');
});
46
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● DataSourceが呼び出せているかのテスト
● ドメインオブジェクトへの変換のテスト
● キャッシュのテスト 47
 
test('Test send', () async {
final msgDs = MockedDependencies.get<MessagesApiDataSource>()
as MockMessagesApiDataSource;
when(msgDs.sendMessage(any)).thenAnswer((realInvocation) async =>
CommonResponse.test(true, const SendMessageResponse()));
final repo = MockedDependencies.get<MessageRepository>();
final message = SendMessage.validated('12345678', title: 'Re:');
await repo.sendMessage(const MatchingConnectionId(1234), message);
verify(msgDs.sendMessage(argThat(allOf(
predicate<SendMessageRequestParameter>(
(req) => req.matchingConnectionId == 1234),
predicate<SendMessageRequestParameter>((req) => req.title == 'Re:'),
predicate<SendMessageRequestParameter>((req) => req.body == '12345678'),
)))).called(1);
});
48
 
test('Test send', () async {
final msgDs = MockedDependencies.get<MessagesApiDataSource>()
as MockMessagesApiDataSource;
when(msgDs.sendMessage(any)).thenAnswer((realInvocation) async =>
CommonResponse.test(true, const SendMessageResponse()));
final repo = MockedDependencies.get<MessageRepository>();
final message = SendMessage.validated('12345678', title: 'Re:');
await repo.sendMessage(const MatchingConnectionId(1234), message);
verify(msgDs.sendMessage(argThat(allOf(
predicate<SendMessageRequestParameter>(
(req) => req.matchingConnectionId == 1234),
predicate<SendMessageRequestParameter>((req) => req.title == 'Re:'),
predicate<SendMessageRequestParameter>((req) => req.body == '12345678'),
)))).called(1);
});
49
 
test('Test send', () async {
final msgDs = MockedDependencies.get<MessagesApiDataSource>()
as MockMessagesApiDataSource;
when(msgDs.sendMessage(any)).thenAnswer((realInvocation) async =>
CommonResponse.test(true, const SendMessageResponse()));
final repo = MockedDependencies.get<MessageRepository>();
final message = SendMessage.validated('12345678', title: 'Re:');
await repo.sendMessage(const MatchingConnectionId(1234), message);
verify(msgDs.sendMessage(argThat(allOf(
predicate<SendMessageRequestParameter>(
(req) => req.matchingConnectionId == 1234),
predicate<SendMessageRequestParameter>((req) => req.title == 'Re:'),
predicate<SendMessageRequestParameter>((req) => req.body == '12345678'),
)))).called(1);
});
50
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● アプリケーションロジックのテスト
● 各リポジトリを操作しているかのテスト
51
 
test('Test send reply message(without Re:)', () async {
final repo =
MockedDependencies.get<MessageRepository>() as MockMessageRepository;
final send = MockedDependencies.get<SendMessageUseCase>();
when(repo.getLastConversationMessage(any)).thenAnswer(
(realInvocation) => mockedMessage.copyWith(title: 'title'));
await send.handle(mId(1234), SendMessage.validated('body'));
verify(repo.sendMessage(
argThat(equals(mId(1234))),
argThat(
equals(SendMessage.validated('body', title: 'Re:title')))))
.called(1);
verify(repo.fetch(any)).called(1);
});
52
 
test('Test send reply message(without Re:)', () async {
final repo =
MockedDependencies.get<MessageRepository>() as MockMessageRepository;
final send = MockedDependencies.get<SendMessageUseCase>();
when(repo.getLastConversationMessage(any)).thenAnswer(
(realInvocation) => mockedMessage.copyWith(title: 'title'));
await send.handle(mId(1234), SendMessage.validated('body'));
verify(repo.sendMessage(
argThat(equals(mId(1234))),
argThat(
equals(SendMessage.validated('body', title: 'Re:title')))))
.called(1);
verify(repo.fetch(any)).called(1);
});
53
 
test('Test send reply message(without Re:)', () async {
final repo =
MockedDependencies.get<MessageRepository>() as MockMessageRepository;
final send = MockedDependencies.get<SendMessageUseCase>();
when(repo.getLastConversationMessage(any)).thenAnswer(
(realInvocation) => mockedMessage.copyWith(title: 'title'));
await send.handle(mId(1234), SendMessage.validated('body'));
verify(repo.sendMessage(
argThat(equals(mId(1234))),
argThat(
equals(SendMessage.validated('body', title: 'Re:title')))))
.called(1);
verify(repo.fetch(any)).called(1);
});
54
 
test('Test send reply message(without Re:)', () async {
final repo =
MockedDependencies.get<MessageRepository>() as MockMessageRepository;
final send = MockedDependencies.get<SendMessageUseCase>();
when(repo.getLastConversationMessage(any)).thenAnswer(
(realInvocation) => mockedMessage.copyWith(title: 'title'));
await send.handle(mId(1234), SendMessage.validated('body'));
verify(repo.sendMessage(
argThat(equals(mId(1234))),
argThat(
equals(SendMessage.validated('body', title: 'Re:title')))))
.called(1);
verify(repo.fetch(any)).called(1);
});
55
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● ボタンが押されたときに適切なUseCaseがよばれ
ているかをテスト
● Widgetの表示状態をテスト 56
 
testWidgets('Test clear form after sent message', (WidgetTester tester) async {
final send = MockedDependencies.getAs<SendMessageUseCase, MockSendMessageUseCase>();
when(send.handle(any, any)).thenAnswer((realInvocation) async {});
await _pumpMessageFormComponent(tester);
await tester.enterText(find.byType(TextFormField), 'test message');
await tester.pump();
expect(find.text(_testMessage), findsOneWidget);
await tester.tap(find.byIcon(Icons.send));
await tester.pump();
verify(send.handle(
argThat(equals(mId(1234))), SendMessage.validated('test message')))
.called(1);
expect(find.text(_testMessage), findsNothing);
});
57
 
testWidgets('Test clear form after sent message', (WidgetTester tester) async {
final send = MockedDependencies.getAs<SendMessageUseCase, MockSendMessageUseCase>();
when(send.handle(any, any)).thenAnswer((realInvocation) async {});
await _pumpMessageFormComponent(tester);
await tester.enterText(find.byType(TextFormField), 'test message');
await tester.pump();
expect(find.text(_testMessage), findsOneWidget);
await tester.tap(find.byIcon(Icons.send));
await tester.pump();
verify(send.handle(
argThat(equals(mId(1234))), SendMessage.validated('test message')))
.called(1);
expect(find.text(_testMessage), findsNothing);
});
58
 
testWidgets('Test clear form after sent message', (WidgetTester tester) async {
final send = MockedDependencies.getAs<SendMessageUseCase, MockSendMessageUseCase>();
when(send.handle(any, any)).thenAnswer((realInvocation) async {});
await _pumpMessageFormComponent(tester);
await tester.enterText(find.byType(TextFormField), 'test message');
await tester.pump();
expect(find.text(_testMessage), findsOneWidget);
await tester.tap(find.byIcon(Icons.send));
await tester.pump();
verify(send.handle(
argThat(equals(mId(1234))), SendMessage.validated('test message')))
.called(1);
expect(find.text(_testMessage), findsNothing);
});
59
 
testWidgets('Test clear form after sent message', (WidgetTester tester) async {
final send = MockedDependencies.getAs<SendMessageUseCase, MockSendMessageUseCase>();
when(send.handle(any, any)).thenAnswer((realInvocation) async {});
await _pumpMessageFormComponent(tester);
await tester.enterText(find.byType(TextFormField), 'test message');
await tester.pump();
expect(find.text(_testMessage), findsOneWidget);
await tester.tap(find.byIcon(Icons.send));
await tester.pump();
verify(send.handle(
argThat(equals(mId(1234))), SendMessage.validated('test message')))
.called(1);
expect(find.text(_testMessage), findsNothing);
});
60
各層をテストしよう
Domain
View
(Widget)
ViewModel
(Controller)
DataSource
UseCase Repository
● 各層でテストすることによって、テスト範囲を狭
め、シンプルに保つことができる
61
62
振り返って
63
振り返って
● DIやServiceLocatorを導入することで各クラス単
体のテストができる
● クリーンアーキテクチャをちゃんと実装しようと
するとクラス数が多くなり大変=テストも大変
○ ただメソッドを呼び出すだけなどはテスト書か
ない判断も必要かも
64
よかったこと3選
● 機能追加でのデグレを早期発見できた
● 各層に分けたことでアプリ内のフロントエンドと
バックエンドを違う担当者が触ることができた
● テストを書ける粒度でコードを書く習慣がついた
65
まとめ
66
まとめ
● テストできるような仕組みにしよう!
○ クラスの粒度は小さく
○ クラス同士の依存は少なく
● 無理にすべてを書こうとしない
● テスト書いて良いコードにしましょう!
67
ご清聴
ありがとうございました
68

More Related Content

PDF
Introduction_on_designing_test_in_flutter
ODP
Mastering Mock Objects - Advanced Unit Testing for Java
PPT
JMockit
PPTX
Mockito vs JMockit, battle of the mocking frameworks
PPT
JMockit Framework Overview
PPTX
Unit Testing in Java
PDF
Documenting Bugs in Doxygen
PPTX
Refactoring
Introduction_on_designing_test_in_flutter
Mastering Mock Objects - Advanced Unit Testing for Java
JMockit
Mockito vs JMockit, battle of the mocking frameworks
JMockit Framework Overview
Unit Testing in Java
Documenting Bugs in Doxygen
Refactoring

What's hot (19)

PPTX
Test driven development
PPTX
Android code convention
PDF
C++ Unit Test with Google Testing Framework
PDF
Android coding standard
ODT
Android Open source coading guidel ine
PPT
20111018 boost and gtest
ODP
Automated testing in Python and beyond
 
PDF
Software Engineering - RS3
PPT
Xp Day 080506 Unit Tests And Mocks
PPT
Google mock for dummies
PDF
Testing untestable code - STPCon11
PPTX
Rc2010 tdd
PPTX
In search of JavaScript code quality: unit testing
PDF
JAVASCRIPT TDD(Test driven Development) & Qunit Tutorial
PDF
Metaprogramming
PDF
Test and refactoring
PDF
Declarative Input Validation with JSR 303 and ExtVal
PPTX
Junit mockito and PowerMock in Java
PDF
Test driven development
Android code convention
C++ Unit Test with Google Testing Framework
Android coding standard
Android Open source coading guidel ine
20111018 boost and gtest
Automated testing in Python and beyond
 
Software Engineering - RS3
Xp Day 080506 Unit Tests And Mocks
Google mock for dummies
Testing untestable code - STPCon11
Rc2010 tdd
In search of JavaScript code quality: unit testing
JAVASCRIPT TDD(Test driven Development) & Qunit Tutorial
Metaprogramming
Test and refactoring
Declarative Input Validation with JSR 303 and ExtVal
Junit mockito and PowerMock in Java
Ad

Similar to Dev fest kyoto_2021-flutter_test (20)

ODP
Grails unit testing
PDF
Testable JavaScript: Application Architecture
PPT
Testing And Drupal
PDF
Unit & Automation Testing in Android - Stanislav Gatsev, Melon
PDF
Android best practices
PDF
JS Lab`16. Сергей Селецкий: "Ретроспектива тестирования JavaScript"
PPT
Pragmatic Parallels: Java and JavaScript
PPT
Java performance
PDF
Droidcon ES '16 - How to fail going offline
PDF
Design for Testability
PDF
Тестирование на Android с Dagger 2
ODP
Polyglot persistence with Spring Data
PDF
33rd Degree 2013, Bad Tests, Good Tests
PDF
Android testing
PPTX
Testing with VS2010 - A Bugs Life
PDF
Developer Test - Things to Know
PPTX
Unit testing on mobile apps
PPTX
Junit_.pptx
PDF
DOCX
Rhino Mocks
Grails unit testing
Testable JavaScript: Application Architecture
Testing And Drupal
Unit & Automation Testing in Android - Stanislav Gatsev, Melon
Android best practices
JS Lab`16. Сергей Селецкий: "Ретроспектива тестирования JavaScript"
Pragmatic Parallels: Java and JavaScript
Java performance
Droidcon ES '16 - How to fail going offline
Design for Testability
Тестирование на Android с Dagger 2
Polyglot persistence with Spring Data
33rd Degree 2013, Bad Tests, Good Tests
Android testing
Testing with VS2010 - A Bugs Life
Developer Test - Things to Know
Unit testing on mobile apps
Junit_.pptx
Rhino Mocks
Ad

Recently uploaded (20)

PPTX
AUTOMOTIVE ENGINE MANAGEMENT (MECHATRONICS).pptx
PDF
Exploratory_Data_Analysis_Fundamentals.pdf
PPTX
Fundamentals of Mechanical Engineering.pptx
PDF
Categorization of Factors Affecting Classification Algorithms Selection
PPT
INTRODUCTION -Data Warehousing and Mining-M.Tech- VTU.ppt
PDF
August 2025 - Top 10 Read Articles in Network Security & Its Applications
PDF
SMART SIGNAL TIMING FOR URBAN INTERSECTIONS USING REAL-TIME VEHICLE DETECTI...
PPTX
Fundamentals of safety and accident prevention -final (1).pptx
PDF
BIO-INSPIRED ARCHITECTURE FOR PARSIMONIOUS CONVERSATIONAL INTELLIGENCE : THE ...
PPTX
Feature types and data preprocessing steps
PPTX
Artificial Intelligence
PDF
R24 SURVEYING LAB MANUAL for civil enggi
PPTX
Nature of X-rays, X- Ray Equipment, Fluoroscopy
PDF
COURSE DESCRIPTOR OF SURVEYING R24 SYLLABUS
PDF
Human-AI Collaboration: Balancing Agentic AI and Autonomy in Hybrid Systems
PDF
EXPLORING LEARNING ENGAGEMENT FACTORS INFLUENCING BEHAVIORAL, COGNITIVE, AND ...
PDF
Unit I ESSENTIAL OF DIGITAL MARKETING.pdf
PPTX
communication and presentation skills 01
PDF
distributed database system" (DDBS) is often used to refer to both the distri...
PPTX
Graph Data Structures with Types, Traversals, Connectivity, and Real-Life App...
AUTOMOTIVE ENGINE MANAGEMENT (MECHATRONICS).pptx
Exploratory_Data_Analysis_Fundamentals.pdf
Fundamentals of Mechanical Engineering.pptx
Categorization of Factors Affecting Classification Algorithms Selection
INTRODUCTION -Data Warehousing and Mining-M.Tech- VTU.ppt
August 2025 - Top 10 Read Articles in Network Security & Its Applications
SMART SIGNAL TIMING FOR URBAN INTERSECTIONS USING REAL-TIME VEHICLE DETECTI...
Fundamentals of safety and accident prevention -final (1).pptx
BIO-INSPIRED ARCHITECTURE FOR PARSIMONIOUS CONVERSATIONAL INTELLIGENCE : THE ...
Feature types and data preprocessing steps
Artificial Intelligence
R24 SURVEYING LAB MANUAL for civil enggi
Nature of X-rays, X- Ray Equipment, Fluoroscopy
COURSE DESCRIPTOR OF SURVEYING R24 SYLLABUS
Human-AI Collaboration: Balancing Agentic AI and Autonomy in Hybrid Systems
EXPLORING LEARNING ENGAGEMENT FACTORS INFLUENCING BEHAVIORAL, COGNITIVE, AND ...
Unit I ESSENTIAL OF DIGITAL MARKETING.pdf
communication and presentation skills 01
distributed database system" (DDBS) is often used to refer to both the distri...
Graph Data Structures with Types, Traversals, Connectivity, and Real-Life App...

Dev fest kyoto_2021-flutter_test