Skip to content

Commit 2a50236

Browse files
Add native unit tests to iOS and macOS templates (#117147)
* Improve Swift plugin implementation * Add iOS tests * Review feedback on structure * Remove duplicate scheme file * Add macOS * Add iOS * swift test tweaks * unit tests * Whitespace * Add e2e tests
1 parent 9a347fb commit 2a50236

File tree

18 files changed

+633
-93
lines changed

18 files changed

+633
-93
lines changed

dev/devicelab/lib/framework/ios.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ Future<bool> runXcodeTests({
139139
required String platformDirectory,
140140
required String destination,
141141
required String testName,
142+
String configuration = 'Release',
142143
bool skipCodesign = false,
143144
}) async {
144145
final Map<String, String> environment = Platform.environment;
@@ -161,7 +162,7 @@ Future<bool> runXcodeTests({
161162
'-scheme',
162163
'Runner',
163164
'-configuration',
164-
'Release',
165+
configuration,
165166
'-destination',
166167
destination,
167168
'-resultBundlePath',

dev/devicelab/lib/tasks/plugin_tests.dart

+41-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:io';
77
import 'package:path/path.dart' as path;
88

99
import '../framework/framework.dart';
10+
import '../framework/ios.dart';
1011
import '../framework/task_result.dart';
1112
import '../framework/utils.dart';
1213

@@ -59,7 +60,10 @@ class PluginTest {
5960
}
6061
section('Test plugin');
6162
if (runFlutterTest) {
62-
await plugin.test();
63+
await plugin.runFlutterTest();
64+
if (!dartOnlyPlugin) {
65+
await plugin.example.runNativeTests(buildTarget);
66+
}
6367
}
6468
section('Create Flutter app');
6569
final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget,
@@ -73,7 +77,7 @@ class PluginTest {
7377
await app.build(buildTarget, validateNativeBuildProject: !dartOnlyPlugin);
7478
if (runFlutterTest) {
7579
section('Test app');
76-
await app.test();
80+
await app.runFlutterTest();
7781
}
7882
} finally {
7983
await plugin.delete();
@@ -98,6 +102,10 @@ class _FlutterProject {
98102

99103
File get pubspecFile => File(path.join(rootPath, 'pubspec.yaml'));
100104

105+
_FlutterProject get example {
106+
return _FlutterProject(Directory(path.join(rootPath)), 'example');
107+
}
108+
101109
Future<void> addPlugin(String plugin, {String? pluginPath}) async {
102110
final File pubspec = pubspecFile;
103111
String content = await pubspec.readAsString();
@@ -151,12 +159,42 @@ class $dartPluginClass {
151159
}
152160
}
153161

154-
Future<void> test() async {
162+
Future<void> runFlutterTest() async {
155163
await inDirectory(Directory(rootPath), () async {
156164
await flutter('test');
157165
});
158166
}
159167

168+
Future<void> runNativeTests(String buildTarget) async {
169+
// Native unit tests rely on building the app first to generate necessary
170+
// build files.
171+
await build(buildTarget, validateNativeBuildProject: false);
172+
173+
if (buildTarget == 'ios') {
174+
await testWithNewIOSSimulator('TestNativeUnitTests', (String deviceId) async {
175+
if (!await runXcodeTests(
176+
platformDirectory: path.join(rootPath, 'ios'),
177+
destination: 'id=$deviceId',
178+
configuration: 'Debug',
179+
testName: 'native_plugin_unit_tests_ios',
180+
skipCodesign: true,
181+
)) {
182+
throw TaskResult.failure('Platform unit tests failed');
183+
}
184+
});
185+
} else if (buildTarget == 'macos') {
186+
if (!await runXcodeTests(
187+
platformDirectory: path.join(rootPath, 'macos'),
188+
destination: 'platform=macOS',
189+
configuration: 'Debug',
190+
testName: 'native_plugin_unit_tests_macos',
191+
skipCodesign: true,
192+
)) {
193+
throw TaskResult.failure('Platform unit tests failed');
194+
}
195+
}
196+
}
197+
160198
static Future<_FlutterProject> create(
161199
Directory directory,
162200
List<String> options,

packages/flutter_tools/templates/app_shared/ios-objc.tmpl/Runner.xcodeproj/project.pbxproj.tmpl

+125
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11+
331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 331C80F3294D02FB00263BE5 /* RunnerTests.m */; };
1112
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
1213
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
1314
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
@@ -16,6 +17,16 @@
1617
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
1718
/* End PBXBuildFile section */
1819

20+
/* Begin PBXContainerItemProxy section */
21+
331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */ = {
22+
isa = PBXContainerItemProxy;
23+
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
24+
proxyType = 1;
25+
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
26+
remoteInfo = Runner;
27+
};
28+
/* End PBXContainerItemProxy section */
29+
1930
/* Begin PBXCopyFilesBuildPhase section */
2031
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
2132
isa = PBXCopyFilesBuildPhase;
@@ -32,6 +43,8 @@
3243
/* Begin PBXFileReference section */
3344
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
3445
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
46+
331C80F1294D02FB00263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
47+
331C80F3294D02FB00263BE5 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
3548
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3649
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
3750
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
@@ -47,6 +60,13 @@
4760
/* End PBXFileReference section */
4861

4962
/* Begin PBXFrameworksBuildPhase section */
63+
331C80EE294D02FB00263BE5 /* Frameworks */ = {
64+
isa = PBXFrameworksBuildPhase;
65+
buildActionMask = 2147483647;
66+
files = (
67+
);
68+
runOnlyForDeploymentPostprocessing = 0;
69+
};
5070
97C146EB1CF9000F007C117D /* Frameworks */ = {
5171
isa = PBXFrameworksBuildPhase;
5272
buildActionMask = 2147483647;
@@ -57,6 +77,14 @@
5777
/* End PBXFrameworksBuildPhase section */
5878

5979
/* Begin PBXGroup section */
80+
331C80F2294D02FB00263BE5 /* RunnerTests */ = {
81+
isa = PBXGroup;
82+
children = (
83+
331C80F3294D02FB00263BE5 /* RunnerTests.m */,
84+
);
85+
path = RunnerTests;
86+
sourceTree = "<group>";
87+
};
6088
9740EEB11CF90186004384FC /* Flutter */ = {
6189
isa = PBXGroup;
6290
children = (
@@ -73,6 +101,7 @@
73101
children = (
74102
9740EEB11CF90186004384FC /* Flutter */,
75103
97C146F01CF9000F007C117D /* Runner */,
104+
331C80F2294D02FB00263BE5 /* RunnerTests */,
76105
97C146EF1CF9000F007C117D /* Products */,
77106
);
78107
sourceTree = "<group>";
@@ -81,6 +110,7 @@
81110
isa = PBXGroup;
82111
children = (
83112
97C146EE1CF9000F007C117D /* Runner.app */,
113+
331C80F1294D02FB00263BE5 /* RunnerTests.xctest */,
84114
);
85115
name = Products;
86116
sourceTree = "<group>";
@@ -112,6 +142,24 @@
112142
/* End PBXGroup section */
113143

114144
/* Begin PBXNativeTarget section */
145+
331C80F0294D02FB00263BE5 /* RunnerTests */ = {
146+
isa = PBXNativeTarget;
147+
buildConfigurationList = 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
148+
buildPhases = (
149+
331C80ED294D02FB00263BE5 /* Sources */,
150+
331C80EE294D02FB00263BE5 /* Frameworks */,
151+
331C80EF294D02FB00263BE5 /* Resources */,
152+
);
153+
buildRules = (
154+
);
155+
dependencies = (
156+
331C80F6294D02FB00263BE5 /* PBXTargetDependency */,
157+
);
158+
name = RunnerTests;
159+
productName = RunnerTests;
160+
productReference = 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */;
161+
productType = "com.apple.product-type.bundle.unit-test";
162+
};
115163
97C146ED1CF9000F007C117D /* Runner */ = {
116164
isa = PBXNativeTarget;
117165
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -141,6 +189,10 @@
141189
LastUpgradeCheck = 1300;
142190
ORGANIZATIONNAME = "";
143191
TargetAttributes = {
192+
331C80F0294D02FB00263BE5 = {
193+
CreatedOnToolsVersion = 14.0;
194+
TestTargetID = 97C146ED1CF9000F007C117D;
195+
};
144196
97C146ED1CF9000F007C117D = {
145197
CreatedOnToolsVersion = 7.3.1;
146198
};
@@ -160,11 +212,19 @@
160212
projectRoot = "";
161213
targets = (
162214
97C146ED1CF9000F007C117D /* Runner */,
215+
331C80F0294D02FB00263BE5 /* RunnerTests */,
163216
);
164217
};
165218
/* End PBXProject section */
166219

167220
/* Begin PBXResourcesBuildPhase section */
221+
331C80EF294D02FB00263BE5 /* Resources */ = {
222+
isa = PBXResourcesBuildPhase;
223+
buildActionMask = 2147483647;
224+
files = (
225+
);
226+
runOnlyForDeploymentPostprocessing = 0;
227+
};
168228
97C146EC1CF9000F007C117D /* Resources */ = {
169229
isa = PBXResourcesBuildPhase;
170230
buildActionMask = 2147483647;
@@ -212,6 +272,14 @@
212272
/* End PBXShellScriptBuildPhase section */
213273

214274
/* Begin PBXSourcesBuildPhase section */
275+
331C80ED294D02FB00263BE5 /* Sources */ = {
276+
isa = PBXSourcesBuildPhase;
277+
buildActionMask = 2147483647;
278+
files = (
279+
331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */,
280+
);
281+
runOnlyForDeploymentPostprocessing = 0;
282+
};
215283
97C146EA1CF9000F007C117D /* Sources */ = {
216284
isa = PBXSourcesBuildPhase;
217285
buildActionMask = 2147483647;
@@ -224,6 +292,14 @@
224292
};
225293
/* End PBXSourcesBuildPhase section */
226294

295+
/* Begin PBXTargetDependency section */
296+
331C80F6294D02FB00263BE5 /* PBXTargetDependency */ = {
297+
isa = PBXTargetDependency;
298+
target = 97C146ED1CF9000F007C117D /* Runner */;
299+
targetProxy = 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */;
300+
};
301+
/* End PBXTargetDependency section */
302+
227303
/* Begin PBXVariantGroup section */
228304
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
229305
isa = PBXVariantGroup;
@@ -315,6 +391,45 @@
315391
};
316392
name = Profile;
317393
};
394+
331C80F8294D02FB00263BE5 /* Debug */ = {
395+
isa = XCBuildConfiguration;
396+
buildSettings = {
397+
BUNDLE_LOADER = "$(TEST_HOST)";
398+
CURRENT_PROJECT_VERSION = 1;
399+
GENERATE_INFOPLIST_FILE = YES;
400+
MARKETING_VERSION = 1.0;
401+
PRODUCT_BUNDLE_IDENTIFIER = {{iosIdentifier}}.RunnerTests;
402+
PRODUCT_NAME = "$(TARGET_NAME)";
403+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
404+
};
405+
name = Debug;
406+
};
407+
331C80F9294D02FB00263BE5 /* Release */ = {
408+
isa = XCBuildConfiguration;
409+
buildSettings = {
410+
BUNDLE_LOADER = "$(TEST_HOST)";
411+
CURRENT_PROJECT_VERSION = 1;
412+
GENERATE_INFOPLIST_FILE = YES;
413+
MARKETING_VERSION = 1.0;
414+
PRODUCT_BUNDLE_IDENTIFIER = {{iosIdentifier}}.RunnerTests;
415+
PRODUCT_NAME = "$(TARGET_NAME)";
416+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
417+
};
418+
name = Release;
419+
};
420+
331C80FA294D02FB00263BE5 /* Profile */ = {
421+
isa = XCBuildConfiguration;
422+
buildSettings = {
423+
BUNDLE_LOADER = "$(TEST_HOST)";
424+
CURRENT_PROJECT_VERSION = 1;
425+
GENERATE_INFOPLIST_FILE = YES;
426+
MARKETING_VERSION = 1.0;
427+
PRODUCT_BUNDLE_IDENTIFIER = {{iosIdentifier}}.RunnerTests;
428+
PRODUCT_NAME = "$(TARGET_NAME)";
429+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
430+
};
431+
name = Profile;
432+
};
318433
97C147031CF9000F007C117D /* Debug */ = {
319434
isa = XCBuildConfiguration;
320435
buildSettings = {
@@ -465,6 +580,16 @@
465580
/* End XCBuildConfiguration section */
466581

467582
/* Begin XCConfigurationList section */
583+
331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
584+
isa = XCConfigurationList;
585+
buildConfigurations = (
586+
331C80F8294D02FB00263BE5 /* Debug */,
587+
331C80F9294D02FB00263BE5 /* Release */,
588+
331C80FA294D02FB00263BE5 /* Profile */,
589+
);
590+
defaultConfigurationIsVisible = 0;
591+
defaultConfigurationName = Release;
592+
};
468593
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
469594
isa = XCConfigurationList;
470595
buildConfigurations = (

packages/flutter_tools/templates/app_shared/ios-objc.tmpl/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

+11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
</BuildableReference>
3838
</MacroExpansion>
3939
<Testables>
40+
<TestableReference
41+
skipped = "NO"
42+
parallelizable = "YES">
43+
<BuildableReference
44+
BuildableIdentifier = "primary"
45+
BlueprintIdentifier = "331C80F0294D02FB00263BE5"
46+
BuildableName = "RunnerTests.xctest"
47+
BlueprintName = "RunnerTests"
48+
ReferencedContainer = "container:Runner.xcodeproj">
49+
</BuildableReference>
50+
</TestableReference>
4051
</Testables>
4152
</TestAction>
4253
<LaunchAction
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#import <Flutter/Flutter.h>
2+
#import <UIKit/UIKit.h>
3+
#import <XCTest/XCTest.h>
4+
{{#withPlatformChannelPluginHook}}
5+
6+
@import {{pluginProjectName}};
7+
8+
// This demonstrates a simple unit test of the Objective-C portion of this plugin's implementation.
9+
//
10+
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
11+
{{/withPlatformChannelPluginHook}}
12+
13+
@interface RunnerTests : XCTestCase
14+
15+
@end
16+
17+
@implementation RunnerTests
18+
19+
{{#withPlatformChannelPluginHook}}
20+
- (void)testExample {
21+
{{pluginClass}} *plugin = [[{{pluginClass}} alloc] init];
22+
23+
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getPlatformVersion"
24+
arguments:nil];
25+
XCTestExpectation *expectation = [self expectationWithDescription:@"result block must be called"];
26+
[plugin handleMethodCall:call
27+
result:^(id result) {
28+
NSString *expected = [NSString
29+
stringWithFormat:@"iOS %@", UIDevice.currentDevice.systemVersion];
30+
XCTAssertEqualObjects(result, expected);
31+
[expectation fulfill];
32+
}];
33+
[self waitForExpectationsWithTimeout:1 handler:nil];
34+
}
35+
{{/withPlatformChannelPluginHook}}
36+
{{^withPlatformChannelPluginHook}}
37+
- (void)testExample {
38+
// If you add code to the Runner application, consider adding tests here.
39+
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
40+
}
41+
{{/withPlatformChannelPluginHook}}
42+
43+
@end

0 commit comments

Comments
 (0)