Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support library test bundles #13

Merged
merged 3 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions FBSnapshotTestCase.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
827137911C63ABE900354E42 /* UIImage+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564101B59C3F500A4E4BF /* UIImage+Compare.h */; };
827137921C63ABF000354E42 /* UIImage+Diff.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564121B59C3F500A4E4BF /* UIImage+Diff.h */; };
827137931C63ABF000354E42 /* UIImage+Snapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564141B59C3F500A4E4BF /* UIImage+Snapshot.h */; };
827137941C63ABF000354E42 /* UIApplication+StrictKeyWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */; };
827137951C63ABF400354E42 /* UIImage+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564111B59C3F500A4E4BF /* UIImage+Compare.m */; };
827137961C63ABF400354E42 /* UIImage+Diff.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564131B59C3F500A4E4BF /* UIImage+Diff.m */; };
827137971C63ABF400354E42 /* UIImage+Snapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */; };
827137981C63ABF400354E42 /* UIApplication+StrictKeyWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */; };
827137991C63ABF900354E42 /* FBSnapshotTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = B31988201AB7849400B0A900 /* FBSnapshotTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; };
8271379A1C63ABF900354E42 /* FBSnapshotTestCasePlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 13CBB39B1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h */; settings = {ATTRIBUTES = (Public, ); }; };
8271379B1C63ABF900354E42 /* FBSnapshotTestController.h in Headers */ = {isa = PBXBuildFile; fileRef = B31988221AB7849400B0A900 /* FBSnapshotTestController.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand All @@ -47,8 +45,6 @@
B32447DE1AB78B5E00B1D6FF /* square.png in Resources */ = {isa = PBXBuildFile; fileRef = B32447DB1AB78B5E00B1D6FF /* square.png */; };
B76C68291C6BD6D200586E5B /* rect.png in Resources */ = {isa = PBXBuildFile; fileRef = B76C68271C6BD68100586E5B /* rect.png */; };
B76C682A1C6BD6D500586E5B /* rect.png in Resources */ = {isa = PBXBuildFile; fileRef = B76C68271C6BD68100586E5B /* rect.png */; };
BC45D5211C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */; };
BC45D5221C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */; };
E5C2CD621B1F399A00669887 /* square_with_pixel.png in Resources */ = {isa = PBXBuildFile; fileRef = E5C2CD611B1F399A00669887 /* square_with_pixel.png */; };
F0D698F51B204E120005CAC9 /* SwiftSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D698F41B204E120005CAC9 /* SwiftSupport.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -94,8 +90,6 @@
B32447DA1AB78B5E00B1D6FF /* square-copy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "square-copy.png"; sourceTree = "<group>"; };
B32447DB1AB78B5E00B1D6FF /* square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square.png; sourceTree = "<group>"; };
B76C68271C6BD68100586E5B /* rect.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = rect.png; sourceTree = "<group>"; };
BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIApplication+StrictKeyWindow.h"; sourceTree = "<group>"; };
BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+StrictKeyWindow.m"; sourceTree = "<group>"; };
E5C2CD611B1F399A00669887 /* square_with_pixel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square_with_pixel.png; sourceTree = "<group>"; };
F0D698F41B204E120005CAC9 /* SwiftSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSupport.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -143,8 +137,6 @@
133564131B59C3F500A4E4BF /* UIImage+Diff.m */,
133564141B59C3F500A4E4BF /* UIImage+Snapshot.h */,
133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */,
BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */,
BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */,
);
path = Categories;
sourceTree = "<group>";
Expand Down Expand Up @@ -226,7 +218,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
827137941C63ABF000354E42 /* UIApplication+StrictKeyWindow.h in Headers */,
8271379A1C63ABF900354E42 /* FBSnapshotTestCasePlatform.h in Headers */,
827137911C63ABE900354E42 /* UIImage+Compare.h in Headers */,
827137991C63ABF900354E42 /* FBSnapshotTestCase.h in Headers */,
Expand All @@ -240,7 +231,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
BC45D5211C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h in Headers */,
B31988281AB7849400B0A900 /* FBSnapshotTestCase.h in Headers */,
13CBB39D1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h in Headers */,
B319882A1AB7849400B0A900 /* FBSnapshotTestController.h in Headers */,
Expand Down Expand Up @@ -420,7 +410,6 @@
files = (
827137961C63ABF400354E42 /* UIImage+Diff.m in Sources */,
8271379E1C63ABFD00354E42 /* FBSnapshotTestController.m in Sources */,
827137981C63ABF400354E42 /* UIApplication+StrictKeyWindow.m in Sources */,
8271379D1C63ABFD00354E42 /* FBSnapshotTestCasePlatform.m in Sources */,
8271379C1C63ABFD00354E42 /* FBSnapshotTestCase.m in Sources */,
827137951C63ABF400354E42 /* UIImage+Compare.m in Sources */,
Expand All @@ -442,7 +431,6 @@
buildActionMask = 2147483647;
files = (
133564171B59C3F500A4E4BF /* UIImage+Compare.m in Sources */,
BC45D5221C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m in Sources */,
B31988291AB7849400B0A900 /* FBSnapshotTestCase.m in Sources */,
133564191B59C3F500A4E4BF /* UIImage+Diff.m in Sources */,
1335641B1B59C3F500A4E4BF /* UIImage+Snapshot.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
19 changes: 0 additions & 19 deletions FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.h

This file was deleted.

26 changes: 0 additions & 26 deletions FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.m

This file was deleted.

3 changes: 1 addition & 2 deletions FBSnapshotTestCase/Categories/UIImage+Snapshot.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

#import <FBSnapshotTestCase/UIImage+Snapshot.h>
#import <FBSnapshotTestCase/UIApplication+StrictKeyWindow.h>

@implementation UIImage (Snapshot)

Expand Down Expand Up @@ -43,7 +42,7 @@ + (UIImage *)fb_imageForView:(UIView *)view
UIWindow *window = [view isKindOfClass:[UIWindow class]] ? (UIWindow *)view : view.window;
BOOL removeFromSuperview = NO;
if (!window) {
window = [[UIApplication sharedApplication] fb_strictKeyWindow];
window = [[UIApplication sharedApplication] keyWindow];
}

if (!view.window && view != window) {
Expand Down
3 changes: 1 addition & 2 deletions FBSnapshotTestCase/FBSnapshotTestCasePlatform.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

#import <FBSnapshotTestCase/FBSnapshotTestCasePlatform.h>
#import <FBSnapshotTestCase/UIApplication+StrictKeyWindow.h>
#import <UIKit/UIKit.h>

BOOL FBSnapshotTestCaseIs64Bit(void)
Expand All @@ -34,7 +33,7 @@ BOOL FBSnapshotTestCaseIs64Bit(void)
NSString *FBDeviceAgnosticNormalizedFileName(NSString *fileName)
{
UIDevice *device = [UIDevice currentDevice];
UIWindow *keyWindow = [[UIApplication sharedApplication] fb_strictKeyWindow];
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
CGSize screenSize = keyWindow.bounds.size;
NSString *os = device.systemVersion;

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
iOSSnapshotTestCase (previously named FBSnapshotTestCase)
======================

[![Build Status](https://travis-ci.org/uber/ios-snapshot-test-case.svg)](https://travis-ci.org/uber/ios-snapshot-test-case)
[![Build Status](https://travis-ci.org/uber/ios-snapshot-test-case.svg)](https://travis-ci.org/uber/ios-snapshot-test-case)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/iOSSnapshotTestCase.svg)](https://img.shields.io/cocoapods/v/iOSSnapshotTestCase.svg)

What it does
Expand Down Expand Up @@ -82,11 +82,17 @@ Features
Notes
-----

Your unit test must be an "application test", not a "logic test." (That is, it
must be run within the Simulator so that it has access to UIKit.) In Xcode 5
Your unit tests _should_ be inside an "application" bundle, not a "logic/library" test bundle. (That is, it
should be run within the Simulator so that it has access to UIKit.)

In Xcode 5
and later new projects only offer application tests, but older projects will
have separate targets for the two types.

*However*, if you are writing snapshot tests inside a library/framework, you might want to keep your test bundle as a library test bundle without a Test Host.

Read more on this [here](docs/LibraryVsApplicationTestBundles.md).

Authors
-------

Expand Down
99 changes: 99 additions & 0 deletions docs/LibraryVsApplicationTestBundles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Library vs Application Test Bundles

* Authors: [Alan Zeino](https://github.com/alanzeino)

## Introduction

Developers writing unit tests for iOS using XCTest typically use Application Test Bundles without much consideration as to _why_ their tests need to be inside an Application Test Bundle, as opposed to a Library Test Bundle. This document describes what you can and can't do with both, and why you might want to use a Library Test Bundle.

Library Test Bundles were once called _Logic_ Test Bundles in Apple's nomenclature. In the context of this document, both Library and Logic are interchangeable.

### Application tests

Unit tests that test parts of an application (such as UIViewControllers, UIWindows, UIViews) should typically be part of an Application test bundle. An Application test bundle requires a Test Host (an application to run your tests in) and at test run time, a simulator too. The attached Test Host provides access to some iOS APIs that only work inside Application test bundles. In our experience, we've seen these:

* `-[UIControl sendActionsForControlEvents:]` — This API is commonly used to trigger actions at runtime and sometimes you might want to use it inside a test to trigger a particular code path which is ordinarily run when a user performs an action. While it does not work inside a Library test bundle, we've written our own version for unit tests (see 'Code Snippets' below) that works well for this need.
* `UIAppearance` — Most `UIAppearance` APIs break when there is no test host present.
* `UIWindow` — You cannot make a `UIWindow` you created during your test the 'key window' because `makeKeyAndVisible` crashes at test run time. One workaround is to instead set `hidden` to `false` on the `UIWindow` instance you created. However there still won't be a 'key window' so if you have code that adds a `UIView` as a subview of the `keyWindow` then that will break.
* Keychain — Keychain operations require an application test bundle.

### Library tests
Unit tests that test parts of a framework or library should be part of a Library test bundle. This does not strictly require a Test Host. Not using a Test Host has some advantages:

* No need to install anything, which makes running your tests faster and reduces the likelihood of Simulator instability
* The Test Host application will start an application lifecycle, which is state that can cause instability in your tests
* Only one host application can run at the same time in a Simulator, so tests with a Test Host cannot parallelize on one simulator. The `xctest` stub process spawned without a Test Hist isn’t a full iOS application, so multiple can run in parallel sharing a single simulator.

If you are using [Buck](https://buckbuild.com/), removing the `test_host_app` option for `apple_test()` rules will allow Buck and `xctool` to run your test bundles in parallel.

### Code Examples
#### ub_sendActionsForControlEvents:
This code snippet shows how you might replace `UIControl`'s `sendActionForControlEvents:` in a test that is inside a library test bundle. Since it doesn't have universal application we haven't included it directly in the project. If you decide to use this category, make sure it can only be seen inside unit tests and not all of your code.

```
/**
Copyright (c) 2018 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#import <UIKit/UIKit.h>
@interface UIControl (SendActions)
/**
In library test bundles with no test host, the default sendActionsForControlEvents: does not work.
This replacement mimics the same idea of that method by finding all the targets associated with the control, finding all the actions on that target for the given control event, and invoking those actions on those targets.
@param controlEvents A bitmask whose set flags specify the control events for which action messages are sent.
*/
- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents;
@end
/**
UIControlEvents has options in the range 0-8, 12-13, 16-19. 9-11 are reserved for future UIControlEventTouch* options. 14-15 are reserved for other options. If new options are added after 19, this const will need to be updated.
*/
static NSUInteger const UIControlEventsMaxOffset = 19;
@implementation UIControl (UberTesting)
- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents
{
for (NSUInteger i = 0; i < UIControlEventsMaxOffset; i++) {
UIControlEvents controlEvent = 1 << i;
if (controlEvents & controlEvent) {
for (id target in self.allTargets) {
NSArray<NSString *> *targetActions = [self actionsForTarget:target forControlEvent:controlEvent];
for (NSString *action in targetActions) {
SEL selector = NSSelectorFromString(action);
IMP imp = [target methodForSelector:selector];
void (*func)(id, SEL, id) = (void *)imp;
func(target, selector, self);
}
}
}
}
}
@end
```