Implementing Unit Testing with iOS

Unit testing a framework and saving to the disk

There's generally no issues with saving to the disk for iOS unit tests.

One catch, though, is if you're writing to the standard document directories for Simulator hosted application tests, or logic tests, then you may need to create that directory first.

Just do the following in your test's setup or somewhere else that will happen before tests run:

  • Obtain the documents directory
  • Use NSFileManager to create this path.

This is just a stab-in-the-dark, based on my own experience. So let me know if this helps you or not.

Unit Testing in Xcode, does it run the app?

The application is actually run but there is a trick you can use to prevent it from running.

int main(int argc, char* argv[]) {
int returnValue;

@autoreleasepool {
BOOL inTests = (NSClassFromString(@"SenTestCase") != nil
|| NSClassFromString(@"XCTest") != nil);

if (inTests) {
//use a special empty delegate when we are inside the tests
returnValue = UIApplicationMain(argc, argv, nil, @"TestsAppDelegate");
}
else {
//use the normal delegate
returnValue = UIApplicationMain(argc, argv, nil, @"AppDelegate");
}
}

return returnValue;
}

Unit testing for the app store

There are lots of different philosophies on unit testing. Unit testing is more of use during development, so that if you want to make changes you can make them without worrying as much your change will break something you do are not thinking is related. Basically it would be writing other methods that call your internal game methods with different parameters and check to see if the results are as expected.

You do not need to unit test for the app store - chances are you have been testing the game all along as you have developed, and that user testing is a lot more important for getting something released (as it means it probably will not crash) and also of course that makes for a better game!

One final note, if you are thinking about unit testing in the future there is a downside. It can also mean more work when re-writing code as you not only have to change your original code, but also related tests and make sure the results they are looking for make sense. For a single person that overhead may not make sense; it is more helpful when many people will be changing the same code.

Using Unit Tests While Developing Static Libraries in Obj-C

Code-wise, how are you downloading your data? Most of the time URL connection methods are asynchronous, and you get notification of data being available as a call off the run loop. You very likely are not running the run loop.

If this is the problem, read up on run loops.

How can I unit-test init methods in Objective-C?

If you are doing test first development, you don't need to test for respondsToSelector:@selector(initWithStream:). Directly calling initWithStream: in your test suite will at first not compile. This is your failing test that you should fix before writing new code. How do you fix it? By implementing the method:

- (instancetype)initWithStream:(id)stream {
return [super init];
}

Your test now compiles, which is a little bit better than before. Running the test will fail though, because obviously the implementation doesn't do what you're testing for. Now write more code to make the test actually pass:

- (instancetype)initWithStream:(id)stream {
if (!stream) {
return nil;
}
return [super init];
}

Next, you can test for stream not being nil, which will fail so you write more code to fix it.

How should write the test case for the code below?

Consider extracting the methods of the class into a separate protocol, so that we can make both the actual class and the mock class conform to that protocol, and we can test out the intended functionalities in the unit tests instead of executing the code in the actual implementation.

/*
Extract the 2 methods of MyFileManager into a separate protocol.
Now we can create a mock class which also conforms to this same protocol,
which will help us in writing unit tests.
*/
protocol FileManagerProtocol {
func isStored(atPath path: String) -> Bool
func readData(atPath path: String) -> Data?
}

class MyFileManager: FileManagerProtocol {
static let shared = MyFileManager()

// To make a singleton instance, we have to make its initializer private.
private init() {
}

func isStored(atPath path: String) -> Bool {
//ideally, even FileManager.default instance should be "injected" into this class via dependency injection.
return FileManager.default.fileExists(atPath: path)
}

func readData(atPath path: String) -> Data? {
return try? Data(contentsOf: URL(fileURLWithPath: path))
}
}

The SomeViewModel class can also get its dependencies via dependency injection.

class SomeViewModel {
var fileManager: FileManagerProtocol?

// We can now inject a "mocked" version of MyFileManager for unit tests.
// This "mocked" version will confirm to FileManagerProtocol which we created earlier.
init(fileManager: FileManagerProtocol = MyFileManager.shared) {
self.fileManager = fileManager
}

/*
I've made a small change to the below method.
I've added the path as an argument to this method below,
just to demonstrate the kind of unit tests we can write.
*/
func getCachedData(path: String = "xxxxx") -> Data? {
if let doesFileExist = self.fileManager?.isStored(atPath: path),
doesFileExist {
return self.fileManager?.readData(atPath: path)
}
return nil
}
}

The unit tests for the above implementation can look something similar to what is written below.

class TestSomeViewModel: XCTestCase {
var mockFileManager: MockFileManager!

override func setUp() {
mockFileManager = MockFileManager()
}

override func tearDown() {
mockFileManager = nil
}

func testGetCachedData_WhenPathIsXXXXX() {
let viewModel = SomeViewModel(fileManager: self.mockFileManager)
XCTAssertNotNil(viewModel.getCachedData(), "When the path is xxxxx, the getCachedData() method should not return nil.")
XCTAssertTrue(mockFileManager.isStoredMethodCalled, "When the path is xxxxx, the isStored() method should be called.")
XCTAssertTrue(mockFileManager.isReadDataMethodCalled, "When the path is xxxxx, the readData() method should be called.")
}

func testGetCachedData_WhenPathIsNotXXXXX() {
let viewModel = SomeViewModel(fileManager: self.mockFileManager)
XCTAssertNil(viewModel.getCachedData(path: "abcde"), "When the path is anything apart from xxxxx, the getCachedData() method should return nil.")
XCTAssertTrue(mockFileManager.isStoredMethodCalled, "When the path is anything apart from xxxxx, the isStored() method should be called.")
XCTAssertFalse(mockFileManager.isReadDataMethodCalled, "When the path is anything apart from xxxxx, the readData() method should not be called.")
}
}

// MockFileManager is the mocked implementation of FileManager.
// Since it conforms to FileManagerProtocol, we can implement the
// methods of FileManagerProtocol with a different implementation
// for the assertions in the unit tests.
class MockFileManager: FileManagerProtocol {
private(set) var isStoredMethodCalled = false
private(set) var isReadDataMethodCalled = false

func isStored(atPath path: String) -> Bool {
isStoredMethodCalled = true
if path.elementsEqual("xxxxx") {
return true
}
return false
}

func readData(atPath path: String) -> Data? {
isReadDataMethodCalled = true
if path.elementsEqual("xxxxx") {
return Data()
}
return nil
}
}

Feel free to copy-paste all the above classes and the unit tests to a separate playground file. To run both the unit tests in Playground, write -

TestSomeViewModel.defaultTestSuite.run()

Some other things to keep in mind :-

  1. It's recommended to first write the unit test, run it and see it fail, then write the minimum amount of code needed to pass the unit test. This is called Test Driven Development.
  2. It's easier to write tests if all the implementation classes use Dependency Injection.
  3. Consider avoiding singletons. If singletons are not used carefully, they make the code difficult to unit-test. Feel free to read more about why we should use singletons sparingly here and here.

Unit testing iOS 10 notifications

You can create a protocol for the methods you are using, and make an extension on UNUserNotificationCenter to conform to it.
This protocol would act as a "bridge" between the original UNUserNotificationCenter implementation and your mock object to replace its method implementations.

Here's an example code I wrote in a playground, and works fine:

/* UNUserNotificationCenterProtocol.swift */

// This protocol allows you to use UNUserNotificationCenter, and replace the implementation of its
// methods in you test classes.
protocol UNUserNotificationCenterProtocol: class {
// Declare only the methods that you'll be using.
func add(_ request: UNNotificationRequest,
withCompletionHandler completionHandler: ((Error?) -> Void)?)
}

// The mock class that you'll be using for your test classes. Replace the method contents with your mock
// objects.
class MockNotificationCenter: UNUserNotificationCenterProtocol {

var addRequestExpectation: XCTestExpectation?

func add(_ request: UNNotificationRequest,
withCompletionHandler completionHandler: ((Error?) -> Void)?) {
// Do anything you want here for your tests, fulfill the expectation to pass the test.
addRequestExpectation?.fulfill()
print("Mock center log")
completionHandler?(nil)
}
}

// Must extend UNUserNotificationCenter to conform to this protocol in order to use it in your class.
extension UNUserNotificationCenter: UNUserNotificationCenterProtocol {
// I'm only adding this implementation to show a log message in this example. In order to use the original implementation, don't add it here.
func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) {
print("Notification center log")
completionHandler?(nil)
}
}

/* ExampleClass.swift */

class ExampleClass {

// Even though the type is UNUserNotificationCenterProtocol, it will take UNUserNotificationCenter type
// because of the extension above.
var notificationCenter: UNUserNotificationCenterProtocol = UNUserNotificationCenter.current()

func doSomething() {
// Create a request.
let content = UNNotificationContent()
let request = UNNotificationRequest(identifier: "Request",
content: content,
trigger: nil)
notificationCenter.add(request) { (error: Error?) in
// completion handler code
}
}
}

let exampleClass = ExampleClass()
exampleClass.doSomething() // This should log "Notification center log"

EDITED:
/* TestClass.Swift (unit test class) */

class TestClass {
// Class being tested
var exampleClass: ExampleClass!
// Create your mock class.
var mockNotificationCenter = MockNotificationCenter()

func setUp() {
super.setUp()
exampleClass = ExampleClass()
exampleClass.notificationCenter = mockNotificationCenter
}

func testDoSomething() {
mockNotificationCenter.addRequestExpectation = expectation(description: "Add request should've been called")
exampleClass.doSomething()
waitForExpectations(timeout: 1)
}
}
// Once you run the test, the expectation will be called and "Mock Center Log" will be printed

Keep in mind that every time you use a new method, you'll have to add it to the protocol, or the compiler will complain.

Hope this helps!

Target individual XCTest unit test cases in Xcode 5 to a specific iOS device for a universal app?

To target device specific tests one would need to edit the schemes for a project. Under Product > Scheme > Edit Schemes one can choose to select device specific tests per device.

Scheme Editor



Related Topics



Leave a reply



Submit