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:
- AddressBook – where our production code is
- 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:
ongingRequests
mutable array is exposed in a public interfaceDataProvider
is a singleton, and singletons are cumbersome for testing because they carry state for the lifetime of application- 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:
- one for all tests
- 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.
More articles
fromMarko Cicak
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
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.
Blog author
Marko Cicak
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.