Example of method doing too much – unit testing

Leave a comment

Was working on a refactor of this method when I realized it was doing too much.

- (void)showAppropriateView
{
    if ([self isOffline]) {
        [self.viewManager showOfflineView];
    } else if (!self.nullstateScreenHasBeenDisplayed || [self searchBarHasNoText]) {
        [self.viewManager showNullstateView];
    } else if (self.errorHasOccurred) {
        [self.viewManager showErrorView];
    } else if (self.tracks.count > 0) {
        [self.collectionView reloadData];
        [self.viewManager showContentView];
    } else {
        [self.viewManager showNoResultsView];
        [self updateNoResultsSearchText];
    }
}

On the surface it doesn’t look that bad. Just figuring out what view to display, and then displaying it.

But there are really x2 things going on with this method. One is displaying the view. But the other is doing the other things as a result of that view being displayed.

Before breaking I had to pull out the non display view stuff. Then I was free to have another method focus on the view logic itself.

Final result looked like this.

- (void)showAppropriateView
{
    if (self.tracks.count > 0) {
        [self.collectionView reloadData];
    } else if (self.tracks.count == 0) {
        [self updateNoResultsSearchText];
    }

    [self.viewManager showAppropriateViewWhenOffline:[self isOffline]
                     nullstateScreenHasBeenDisplayed:!self.nullstateScreenHasBeenDisplayed
                                  searchBarHasNoText: [self searchBarHasNoText]
                                    errorHasOccurred:self.errorHasOccurred
                                          trackCount:self.tracks.count];
}

Unit testing without mocks 1

Leave a comment

Here is an example of how to remove a dependency and only pass in what you need. Avoids mocking Notification. Instead just extract value.

+ (CGFloat)constraintForKeyboardWithHeight:(CGFloat)keyboardHeight
                                viewHeight:(CGFloat)viewHeight
                               imageHeight:(CGFloat)imageHeight
                               labelHeight:(CGFloat)labelHeight;
{
    if( IS_IPHONE_5 || IS_IPAD )
    {
        navigationBarHeightPortrait = 64;
    }

    CGFloat errorMesageHeight = [self errorMessageHeightForImageHeight:imageHeight labelHeight:labelHeight];
    CGFloat topConstraintHeightPortrait = (viewHeight - keyboardHeight - errorMesageHeight - navigationBarHeightPortrait - bottomMessaageBarHeight)/2;

    // We are currently ignoring device orientation and only returning portrait
    return topConstraintHeightPortrait;
}

#pragma mark Private

+ (CGFloat)keyboardHeightForNotification:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];
    NSValue *keyboardFrameBegin = [userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey];
    CGRect keyboardFrameBeginRect = [keyboardFrameBegin CGRectValue];
    CGFloat keyboardHeight = keyboardFrameBeginRect.size.height;
    return keyboardHeight;
}

How to authenticate your iOS app using the Spotify iOS SDK and the SFSafariViewController

1 Comment

To help me understand all the moving pieces of the Spotify iOS SDK I created this picture here showing how to setup authentication for a basic ViewController app.

Screen Shot 2017-04-19 at 2.27.18 PM.png

Spotify App

First you need to create a Spotify application and set your clientID, redirect URI, the application bundle id of your app (else SSO won’t work).

Screen Shot 2017-04-19 at 2.40.26 PM.png

AppDelegate

There are two parts here. One sets up the SPTAuth object with all your client id details (that it reads from a local Config.h file you create).

And the other notifies the ViewController once your app has been authenticated.

AppDelegate.h

Screen Shot 2017-04-19 at 2.42.30 PM.png

#import "AppDelegate.h"

#import <SpotifyAuthentication/SpotifyAuthentication.h>
#import <SpotifyMetadata/SpotifyMetadata.h>
#import <SpotifyAudioPlayback/SpotifyAudioPlayback.h>
#import "Config.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Set up shared authentication information
    SPTAuth *auth = [SPTAuth defaultInstance];
    auth.clientID = @kClientId;
    auth.requestedScopes = @[SPTAuthStreamingScope];
    auth.redirectURL = [NSURL URLWithString:@kCallbackURL];
    auth.sessionUserDefaultsKey = @kSessionUserDefaultsKey;
    return YES;
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    SPTAuth *auth = [SPTAuth defaultInstance];

    SPTAuthCallback authCallback = ^(NSError *error, SPTSession *session) {
        // This is the callback that'll be triggered when auth is completed (or fails).

        if (error) {
            NSLog(@"*** Auth error: %@", error);
        } else {
            auth.session = session;
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:@"sessionUpdated" object:self];
    };

    /*
     Handle the callback from the authentication service. -[SPAuth -canHandleURL:]
     helps us filter out URLs that aren't authentication URLs (i.e., URLs you use elsewhere in your application).
     */

    if ([auth canHandleURL:url]) {
        [auth handleAuthCallbackWithTriggeredAuthURL:url callback:authCallback];
        return YES;
    }

    return NO;
}

@end

Config.h

Screen Shot 2017-04-19 at 2.45.13 PM.png

#ifndef Config_h
#define Config_h

// Your client ID
#define kClientId "xxx"

// Your applications callback URL
#define kCallbackURL "messages-app://"

#define kSessionUserDefaultsKey "SpotifySession"

#endif /* Config_h */

Info.plist

In order for the Spotify SafariController to flip back to your app, your app needs to support a URL Schema – the same one your defined in your Spotify application.

Screen Shot 2017-04-19 at 2.48.40 PM.png

If you don’t set this you will see an error like this

Screen Shot 2017-04-19 at 2.03.29 PM.png

ViewController

Here we listen for notifications from our AppDelegate when we get the OK that we are authenticated.

Screen Shot 2017-04-19 at 2.52.27 PM.png

And secondly we open a SafariViewController with a URL we get from our SPTAuth default instance.

Screen Shot 2017-04-19 at 2.53.44 PM.png

This line here is where all the magic happens. It works because we setup the values for it in AppDelegate. If you don’t do that there you will get an error like this – “Missing clientId”

Screen Shot 2017-04-19 at 2.04.14 PM.png

When you print out this URL it looks like this:

https://accounts.spotify.com/authorize?
nolinks=true&nosignup=true&
response_type=token&
scope=streaming&
utm_source=spotify-sdk&
utm_medium=ios-sdk&
utm_campaign=ios-sdk&
redirect_uri=messages-app%3A%2F%2F&
show_dialog=true&
client_id=xxx

This is the URL we open with our Safari controller which leads to a flow of screens like this.

Screen Shot 2017-04-19 at 2.35.09 PM.png

Screen Shot 2017-04-19 at 2.35.14 PM.png

Screen Shot 2017-04-19 at 3.00.18 PM.png

And once you login, it should all work and you will see a log message like so.

Screen Shot 2017-04-19 at 3.02.53 PM.png

iMessage crash with keyboard compact view

1 Comment

So iMessage doesn’t like it when you try to open a keyboard in compact view.

It also doesn’t like it if you swipe to another iMessage app by touching a search bar or text field, as that activates the keyboard and kills the composer.

The way to fix this is to not allow the keyboard to show while in compact view. I did this by setting a flag in the ViewController that houses the searchbar, and then only responding YES to shouldBegin editing if this was set.

MessageViewController.m

- (void)willTransitionToPresentationStyle:(MSMessagesAppPresentationStyle)presentationStyle
{
    if (presentationStyle == MSMessagesAppPresentationStyleCompact) {
        self.searchViewController.isPresentationStyleCompact = YES;
        [self presentViewController:self.searchViewController];
    } else {
        self.searchViewController.isPresentationStyleCompact = NO;
    }
}

SearchViewController.m

- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar
{
    if (self.isPresentationStyleCompact) {
        return NO;
    } else {
        return YES;
    }
}

Links

http://stackoverflow.com/questions/39361172/imessage-app-crash-in-expanded-mode-with-keyboard-of-textfield

https://forums.developer.apple.com/thread/52850

OCMockito Examples

Leave a comment

I got tired of not understanding how OCMockito worked and created the following dirt simple examples to show me how.

SimpleTest.m

#import <XCTest/XCTest.h>

#import "Handler.h"
#import "Transport.h"

#import <OCHamcrestIOS/OCHamcrestIOS.h>
#import <OCMockitoIOS/OCMockitoIOS.h>


@interface HandlerTest : XCTestCase
@property(nonatomic, strong) Handler *sut;
@property(nonatomic, strong) Transport *mockTransport;
@end

@implementation HandlerTest

- (void)setUp
{
    [super setUp];
    self.mockTransport = mock([Transport class]);
    self.sut = [[Handler alloc] initWithTransport:self.mockTransport];
}

- (void)testSimpleVerify
{
    [self.sut simpleMethod];
    [verify(self.mockTransport) foo];
}

- (void)testSimpleVerifyWithSpecificArgument
{
    [self.sut simpleMethodWithArgument:@"bar"];
    [verify(self.mockTransport) fooWithArgument:@"bar"];
}

- (void)testSimpleStub
{
    [given([self.mockTransport connected]) willReturnBool:YES];
    XCTAssert([self.sut isTransportConnected]);
}

- (void)testMethodWithCallback
{
    [self.sut simpleMethodWithCallback];
    [verify(self.mockTransport) fooWithCallback:anything()];
}

- (void)testMethodWithCallbackAndParameters
{
    [self.sut simpleMethodWithCallbackAndParameters];

    HCArgumentCaptor *argument = [HCArgumentCaptor new];
    [verify(self.mockTransport) fooWithCallbackAndParameters:(id)argument];

    void (^callback)(NSArray*, BOOL success) = argument.value;

    NSArray* result = [NSArray new];
    callback(result, YES);

    assertThat(self.sut.model, equalTo(result));
}

@end

Handler.h

#import <Foundation/Foundation.h>
#import "Transport.h"

@interface Handler : NSObject

- (instancetype)initWithTransport:(Transport *)transport;

@property (nonatomic, strong) NSArray *model;

- (void)simpleMethod;
- (void)simpleMethodWithArgument:(NSString *)argument1;
- (void)simpleMethodWithCallback;
- (void)simpleMethodWithCallbackAndParameters;
- (BOOL)isTransportConnected;

@end

Handler.m

#import "Handler.h"
#import "Transport.h"

@interface Handler()
@property (nonatomic, strong) Transport *transport;
@end

@implementation Handler

- (instancetype)initWithTransport:(Transport *)transport
{
    self = [super init];
    if (self) {
        _transport = transport;
    }
    return self;
}

- (void)simpleMethod
{
    [self.transport foo];
}

- (void)simpleMethodWithArgument:(NSString *)argument1
{
    [self.transport fooWithArgument:argument1];
}

- (BOOL)isTransportConnected
{
    return self.transport.connected;
}

- (void)simpleMethodWithCallback
{
    [self.transport fooWithCallback:^(id data, NSError *error) {

    }];
}

- (void)simpleMethodWithCallbackAndParameters
{
    [self.transport fooWithCallbackAndParameters:^(NSArray *result, BOOL success) {
        if (success) {
            self.model = result;
        }
    }];
}

@end

Transport.h

#import <Foundation/Foundation.h>

@interface Transport : NSObject
@property (nonatomic, assign) BOOL connected;
- (void)foo;
- (void)fooWithArgument:(NSString *)argument1;
- (void)fooWithCallback:(void(^)(id data, NSError *error))block;
- (void)fooWithCallbackAndParameters:(void(^)(NSArray *result, BOOL success))block;
@end

Transport.m

#import "Transport.h"

@implementation Transport
- (void)foo {}
- (void)fooWithArgument:(NSString *)argument1 {}
- (void)fooWithCallback:(void(^)(id data, NSError *error))block {}
- (void)fooWithCallbackAndParameters:(void(^)(NSArray *result, BOOL success))block {}
@end

Unit testing tip – additional override

Leave a comment

If you have have a method you want to test, but it does some stuff you like for convenience, and you don’t really want to mess for the sake of testing, create with some overrides and do your testing there.

+ (void)foo:(NSURLSessionConfiguration *)sessionConfiguration
{
    TokenManager *tokenManager = [[TokenManager alloc] init];
    AccessToken *accessToken = tokenManager.accessToken;

    [self addAuthorizationHeaderToSessionConfiguruation:sessionConfiguration token:accessToken];
}

+ (void)foo:(NSURLSessionConfiguration *)sessionConfiguration token:(AccessToken *)accessToken
{
    NSString *accessTokenHeaderValue = [NSString stringWithFormat:@"%@ %@", accessToken.tokenType, accessToken.token];
    sessionConfiguration.HTTPAdditionalHeaders = @{@"Authorization" : accessTokenHeaderValue};
}

The first method is the convenient public one.
The second is the one you can test and inject other things into.

How to encode something on a background thread and then post it on the main one iOS

1 Comment

// encode image on background thread
                           dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
                               NSData *imageData = result.namedArguments[@"image_data"];
                               UIImage *image = [UIImage imageWithData:imageData];

                               // callback on main thread
                               dispatch_async(dispatch_get_main_queue(), ^{
                                   callback(image, nil);
                               });

Older Entries Newer Entries

%d bloggers like this: