Migrating to swift: Thoughts about unit testing

If you are planning to start a new iOS project you will certainly choose swift as programming language and this decision would be without doubt the right one.

But a lot of companies have already big iOS apps with thousands LoC of legacy objective-c. ImmobileinScout24 is one of those companies with a code base started 5 years ago. Several month ago we decided to implement new features and code refactoring in swift.
Our goal was to have a smooth migration to the amazing new open source language swift. That means we will have a mixed code base for a pretty long time.

commit

One challenge we had to face doing this migration is to keep our more than 2000 unit test cases intact and to keep improving our test coverage.
For new classes written in swift we could “easily” write the unit tests using swift. But what about swift classes used in objective-c code? Usually you need to mock those swift classes in your objective-c unit test and as you may know it is not so easy to do so yet.

Let me show you an example of a simple UIViewController in my case an UserViewController that uses a tracking service to track that the screen has been shown.
In the first step both classes the UserViewController and the TrachingService are written in objective-c:

class TrackingService;

@interface UserViewController : UIViewController

- (instancetype)initWithTrackingService:(TrackingService *)trackingService;

@end

#import "UserViewController.h"
#import "TrackingService.h"

@implementation UserViewController {
 TrackingService *_trackingService;
}

- (instancetype)initWithTrackingService:(TrackingService *)trackingService{
  self = [super init];
  if(self){
    _trackingService = trackingService;
  }
  return self;
}

- (void)viewDidAppear:(BOOL)animated {
 [super viewDidAppear:animated];
 [_trackingService trackUserScreenShown];

}

@interface TrackingService : NSObject
- (void) trackUserScreenShown;
@end

#import "TrackingService.h"

@implementation TrackingService

- (void)trackUserScreenShown {
  //Do your tracking
  NSLog(@"track user screen shown") ;
}

@end

Now let us write a unit test in objective-c to test if the tracking service is called correctly when the UserViewController is displayed. For the test we will use XCTest und OCMock:

#import <UIKit/UIKit.h>;
#import <XCTest/XCTest.h>;
#import <OCMock/OCMock.h>;
#import "UserViewController.h"
#import "TrackingService.h"

@interface UserViewControllerTest : XCTestCase

@end

@implementation UserViewControllerTest {
}

- (void) testShouldCallTrackScreenShown{

  //GIVEN
  id trackingServiceMock = [OCMockObject niceMockForClass:[TrackingService class]] ;
  UserViewController *controllerUnderTest = [[UserViewController alloc] initWithUserService:trackingServiceMock];
  [[trackingServiceMock expect] trackUserScreenShown];

  //WHEN
  [controllerUnderTest viewDidAppear:NO];

  //THEN
  [trackingServiceMock verify];
}
@end

Now let us migrate the tracking service to swift:

import Foundation

class TrackingService: NSObject {

  func trackUserScreenShown() {
    //Do your tracking
    print("track user screen shown")
  }
}

Because our swift service inherits from NSObject our objective-c test should (theoretically) works without changes. We could (theoretically) create an OCMockObject from the swift class. But the problem is that we still have a limitation that apple did not fix yet. We cannot access swift classes from an objective-c unit test.

To solve that i wrote a protocol in the test class declaring the same method defined in the swift class:

@protocol TrackingServiceForTest
- (void) trackUserScreenShown;
@end

Then i used that protocol to create my mock and it worked fine. The unit test would look like:

@protocol TrackingServiceForTest
- (void) trackUserScreenShown;
@end

@interface UserViewControllerTest : XCTestCase

@end

@implementation UserViewControllerTest {
}

- (void) testShouldCallTrackScreenShown{

  //GIVEN
  id trackingServiceMock = [OCMockObject niceMockForProtocol:@protocol(TrackingServiceForTest)] ;
  UserViewController *controllerUnderTest = [[UserViewController alloc] initWithTrackingService:trackingServiceMock];
  [[trackingServiceMock expect] trackUserScreenShown];

  //WHEN
  [controllerUnderTest viewDidAppear:NO];

  //THEN
  [trackingServiceMock verify];

}
@end

Migrating a big objective-c iOS-App maybe a challenging task and may take a lot of time to complete. For me there are a lot of reasons to believe that it’s worth the effort. Keeping your unit tests performing well during the migration is highly recommended to ensure your app quality.

Leave a Reply

Your email address will not be published. Required fields are marked *