Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Introduction to TDD in iOS

2.11.2015 | 6 minutes of reading time

I believe we are well past the point of discussing whether an app should have tests or not, so I won’t bother explaining why an app should have tests and why they are useful.
In this blog post we’ll start from an empty project and show how test driven development may look like on the iOS platform.

What’s TDD all about?

By utilizing TDD, you are first writing tests (that initially fail), and after that you develop code which makes the tests pass. Once you have the working code, you may wish to refactor it in order to comply with coding standards. This ensures that your app is testable, tested, and it generally improves app quality. You let tests drive your app design.

What are we developing?

The app we’re going to develop is an AddressBook which syncs contacts from a server. The complete app with the mock Sinatra server can be found here .

Let’s see the first example. Create an empty Xcode Single View Application project named AddressBook.

Xcode immediately creates two groups for us:

  1. AddressBook – where our production code is
  2. AddressBookTests – where our test code lives

All test classes we create belong to the Test target and all production code belongs only to the main target. That way no test code is shipped with the app.

What is going to be our first test?

When the app starts, the contacts view is shown and the first thing the view controller should do is to fetch all contacts from the server. So we can test just that.

Let’s create ContactsViewControllerTest.m which will test ContactsViewController class (which doesn’t exist yet).

Add a viewController property for the view controller this test class is testing.

1#import "ContactsViewController.h"
2 
3@interface ContactsViewControllerTest : XCTestCase
4{
5    ContactsViewController* viewController;
6}
7@end
8 
9@implementation ContactsViewControllerTest
10 
11- (void) setUp
12{
13    [super setUp];
14    viewController = ContactsViewController.new;
15}
16 
17- (void) tearDown
18{
19    viewController = nil;
20    [super tearDown];
21}
22 
23@end

This code is not even compilable because ContactsViewController doesn’t exist yet. Let’s create it and add it to the app target.

1@interface ContactsViewController : UIViewController
2@end

Now, our code can be compiled, but it doesn’t test anything yet. Let’s add our testAllContactsAreRetrievedUponViewAppearing method. This method will test that contacts from the server have been requested as soon as ContactsViewController has appeared.
The way we can test this, is that we assert that there are no ongoing requests before this controller appears. After appearing we assert that there is one ongoing request of type DPAllContacts (DP stands for DataProvider).
The test we’re going to write is an integration test since it tests two app layers: ContactsViewController (UI layer) and DataProvider.

Now, this code cannot compile. To compile we need to create DataProvider and add ongoingRequests array in it. Also, create an empty DPAllContacts class.

1@interface DataProvider : NSObject
2 
3@property(nonatomic, strong) NSMutableArray* ongoingRequests;
4 
5+ (DataProvider*) instance;
6 
7@end

Our test code can now compile, but it fails since there are no ongoing requests once the view controller is shown.

Let’s add the call in -[ContactsViewController viewWillAppear:] method:

1@implementation ContactsViewController
2 
3-(void)viewWillAppear:(BOOL)animated
4{
5    [super viewWillAppear:animated];
6    [DataProvider.instance retrieveAllContacts];
7}

We also need to add the retrieveAllContacts method to our DataProvider façade. That method should add a DPAllContacts instance to the ongoingRequests array.
Now the test passes:

The test passes, and it’s time for the 3rd step: refactoring. There are several issues here:

  1. ongingRequests mutable array is exposed in a public interface
  2. DataProvider is a singleton, and singletons are cumbersome for testing because they carry state for the lifetime of application
  3. The test we’ve written is an integration test. Usually, it’s too soon to have integration test so early in a development. Instead, we should have a unit test where DataProvider should be a mock object.

Problem 1: Hiding implementation details

DataProvider interface exposes ongoingRequests mutable array which is an implemention detail. Besides, it allows someone else to modify it, which should be sole responsability of DataProvider.

Solving problem no.1 is easy: just move ongingRequests mutable array into .m file and expose the public method -(NSArray*)getOngoingRequests in interface file. The implementation can be trivial:

1- (NSArray*) getOngoingRequests
2{
3    return [NSArray arrayWithArray:self.restActions];
4}

We should also adopt our testAllContactsAreRetrievedUponViewAppeared method to reflect these changes.

Problem 2: Dealing with singletons

Singletons are quite common in iOS: UIApplication, NSFileManager, NSUserDefaults, CSSearchableIndex, NSNotificationCenter, UIAccelerimeter… Even though they seem convenient to use, they are quite cumbersome when writing tests. The reason for this is that singletons maintain state across the whole app life, or while executing tests. That means that one test could affect the next one, which is a big no-no in Unit testing.
Some common singletons in everyday iOS app are:

  • Context – where common properties are kept such as a current user and/or other global properties
  • TransferManager – layer which handles file uploads and downloads
  • DataProvider – layer which communicates with REST API
  • History – support for undo/redo operations
  • CoreDataStack – database layer

We can deal with singletons in several ways. One is to pass everything as a parameter to a, say, UIViewController and that one could pass references to other objects as needed.
One other solution is to, instead of having multiple singletons, have only one real singleton which would have references to all other potential singletons. I usually name that singleton ServiceRegistry.

If we now define ServiceRegistry as

1#define SREG ((ServiceRegistry*)[ServiceRegistry instance])

in PrefixHeader.pch file, then just by typing SREG. we get a list of all services in our app:

We can do the same with our DataProvider. By creating ServiceRegistry as singleton which will hold a reference to it, DataProvider is no longer a singleton and thus it’s much easier to mock. Which leads us to a solution of problem no.3.

Problem 3: Create mock for DataProvider

Now we have a DataProvider property in ServiceRegistry class:

1@interface ServiceRegistry : NSObject
2 
3@property(nonatomic, strong) CoreDataStack* coredata;
4@property(nonatomic, strong) Context* context;
5@property(nonatomic, strong) DataProvider* dataProvider;
6 
7+ (ServiceRegistry*) instance;
8 
9@end

but what we actually want is to have a protocol to a DataProvider which can be either a production or mock class:

1@interface ServiceRegistry : NSObject
2 
3@property(nonatomic, strong) CoreDataStack* coredata;
4@property(nonatomic, strong) Context* context;
5@property(nonatomic, strong) id <DataProvider> dataProvider;
6 
7+ (ServiceRegistry*) instance;
8 
9@end

There would be two implementations of DataProvider protocol: DataProviderProd which belongs to the main target and DataProviderMock which belongs to the test target.

DataProviderMock interface:

1@interface DataProviderMock : NSObject <DataProvider>
2@property(nonatomic, assign) BOOL retrieveAllContactsCalled;
3@end

DataProviderMock implementation:

1@implementation DataProviderMock
2- (void) retrieveAllContacts
3{
4    self.retrieveAllContactsCalled = YES;
5}
6@end

It has only one BOOL property (or properties should it be needed) so that we can check if retrieveAllContacts was called by ContactsViewController. Now, ContactsViewControllerTest looks like:

1@interface ContactsViewControllerTest : XCTestCase
2{
3    ContactsViewController* viewController;
4    DataProviderMock* dataProvider;
5}
6@end
7 
8@implementation ContactsViewControllerTest
9 
10- (void) setUp
11{
12    [super setUp];
13    dataProvider = DataProviderMock.new;
14    SREG.dataProvider = dataProvider;
15    viewController = [ContactsViewController new];
16}
17 
18- (void) tearDown
19{
20    viewController = nil;
21    dataProvider = nil;
22    [super tearDown];
23}
24 
25- (void) testAllContactsAreRetrievedUponViewAppeared
26{
27    XCTAssertFalse(dataProvider.retrieveAllContactsCalled);
28    [viewController viewWillAppear:NO];
29    XCTAssertTrue(dataProvider.retrieveAllContactsCalled);
30}
31 
32@end

Xcode vs. AppCode?

If you use AppCode for iOS development, there is one additional step that needs to be done. In AppCode, goto Run → Edit Configurations, click on the + button and select XCTest/Kiwi. It is useful to create two test configurations:

  1. one for all tests
  2. one for the specific test case

With the second one we can test only one test case without executing all tests after a single line change. To test a single class, enter the class name in the Class text field as shown in the image below.

Conclusion

We saw that by writing tests, we let them drive our app design. We had to create a DataProvider layer which would serve as a façade to our backend server. We also created ContactsViewController and made sure that it retrieves all contacts after it appears.
The whole project (with somewhat modified code) is available on bitbucket .
This was a basic and introductory article on how to write tests and how TDD may look like in iOS development. There are many articles and books about TDD. Feel free to start with the Wikipedia article , and if you’re interested in a book, I definitely recommend ‘Test-Driven iOS Development’ by Graham Lee.

share post

Likes

0

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.