📜 ⬆️ ⬇️

Writing Unit Tests. Mocking objects

Who needs unit tests? Not for you - your code is perfect. But still, you just need to read this article, which should tell more about writing unit tests on Swift. Suddenly it will come in handy later.

Unit testing is a great way to write flawless code; Testing will help you find the most mistakes at an early stage of writing a project. As experience shows: if you have difficulties in testing the code, then you will have difficulties with its support or debugging.

Unit testing works with isolated “microcomponents”. Often you need to “mock” classes - that is, to provide a fake with a functional implementation to isolate a specific microcomponent, so that it can be tested. In Objective-C, there are several third-party frameworks that help implement this. But they are not yet available in Swift .
')
In this tutorial, you will learn how to write your own mock objects, fakes, and stubs to cover with a dowry a simple app that helps you remember the birthdays of your friends.

let's start

Download the startup project this is a contact storage application. You will not work on the functionality of the base application; rather, you will write several tests for it to make sure that the application is working properly.

Compile and run the application, and then check how it works. Click the plus button and then add John Appleseed to the general contact list:

image

For storage of contacts, applications uses Core Data .

image

Do not panic! You do not need Core Data experience for this lesson; for this you do not need to have any special skills.

Advantages and disadvantages of unit testing

When it comes to writing tests, you will come across both good and bad news. The bad news is that unit testing has the following disadvantages:



Although there is no perfect solution, there is a bright side - writing tests has the following advantages:



Basic application structure

A large amount of code in applications is based on the Master-Detail Application template with Core Data enabled. But there are some significant improvements to the code pattern. Open the project in Xcode and look at the project navigator:

image

Consider the following details:


A collection of files in PeopleList to avoid large view controllers. In order to avoid large view controllers, you can shift some responsibilities to other classes that connect to view controllers using a simple protocol. You can learn more about large view controllers and how to avoid them by reading this interesting, albeit older, article .

In this case, the protocol is defined in PeopleListDataProviderProtocol.swift ; open it and see. The class that conforms to this protocol must have the properties managedObjectContext and tableView , and must define the addPerson (_ :) and fetch () methods. In addition, it must comply with the UITableViewDataSource protocol.

The view controller PeopleListViewController has a dataProvider property, which corresponds to the PeopleListDataProviderProtocol protocol. This property is set to the PeopleListDataProvider instance in the AppDelegate.swift file.

Add new people to your contact list using ABPeoplePickerNavigationController . This class allows you, as a developer, to have access to user contacts without needing permission.

PeopleListDataProvider is responsible for filling out the table view and accessing Core Data .

Note: Several classes and methods in the project are declared public; so that the target for tests can access classes and methods. Target for tests is outside the application module. If you do not add an access modifier, classes and methods are defined as internal . This means that they are only available in the same module. To access them from outside the module (for example, from a test target), you must add a public access modifier .

Well, it's time to write a few tests!

Writing mock objects

Mock objects allow you to check whether a method call is made or a property is set. For example, on viewDidLoad () from PeopleListViewController , the table view is set to the tableView property of the dataProvider .

You write a test to check what is actually happening.

Preparation of the application for testing

First, you need to prepare a project for writing tests.

Select a project in the project navigator, then select Build Settings in the Birthdays testing tag. Find the Defines Module , and change the settings to Yes , as shown below:

image

Then select the BirthdaysTests folder and go to File \ New \ File .... Select iOS \ Source \ Test Case Class , then click Next, name it PeopleListViewControllerTests , make sure you create the Swift file, click Next again, then click Create .

If Xcode prompts you to create a unifying header, select No. This is an error in Xcode that occurs when there are no files in the target, and you add a new Swift file.

Open the newly created PeopleListViewControllerTests.swift . Import the module you just included by adding the import Birthdays statement right after the other import statements, as shown below:

import UIKit import XCTest import Birthdays 

Remove the following two template methods:

 func testExample() { // This is an example of a functional test case. XCTAssert(true, "Pass") } func testPerformanceExample() { // This is an example of a performance test case. self.measureBlock() { // Put the code you want to measure the time of here. } } 

You now need a copy of PeopleListViewController , so you can use it in tests.

Add the following line to the top of the PeopleListViewControllerTests

 var viewController: PeopleListViewController! 

Then replace the setUp () method with the following code:

 override func setUp() { super.setUp() viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("PeopleListViewController") as! PeopleListViewController } 

It uses the storyboard to create an instance of PeopleListViewController , and assigns it to the viewController .

Select Product \ Test ; Xcode compiles the project and runs any existing tests. Although you have no tests yet, this will allow you to make sure that everything is set up correctly. After a few seconds, Xcode should report that all tests passed successfully.

You are now on your way to creating your first mock object.

Writing the first mock object

Since you are going to work with Core Data , add the following import up PeopleListViewControllerTests.swift , immediately after the import Birthdays line :

 import CoreData 

Then add the following code to the definition of the PeopleListViewControllerTests class:

 class MockDataProvider: NSObject, PeopleListDataProviderProtocol { var managedObjectContext: NSManagedObjectContext? weak var tableView: UITableView! func addPerson(personInfo: PersonInfo) { } func fetch() { } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { return UITableViewCell() } } 

This looks like a rather complicated mock object. However, this is simply the required absolute minimum, since you are going to assign an instance of this mock class to the PeopleListViewController dataProvider property. Your mock class must also comply with the PeopleListDataProviderProtocol , as well as the UITableViewDataSource protocol.

Select Product \ Test ; the project will be re-compiled, and your tests passed to ur. But now you are all set for the first unit test using a mock object.

It is necessary to divide unit testing into three parts, naming them, given , when , and then . ' Given ' sets up the environment; ' when ' executes the code you want to test; and then checks the expected result.

Your test will verify that the tableView property of the data provider is set after the viewDidLoad () method has been executed.

Add the following test to PeopleListViewControllerTests:

 func testDataProviderHasTableViewPropertySetAfterLoading() { // given // 1 let mockDataProvider = MockDataProvider() viewController.dataProvider = mockDataProvider // when // 2 XCTAssertNil(mockDataProvider.tableView, "Before loading the table view should be nil") // 3 let _ = viewController.view // then // 4 XCTAssertTrue(mockDataProvider.tableView != nil, "The table view should be set") XCTAssert(mockDataProvider.tableView === viewController.tableView, "The table view should be set to the table view of the data source") } 

Here is how the above test works:
  1. Creates an instance of MockDataProvider and sets it to the view controller property for dataProvider.
  2. Confirms that the tableView property is nil before the test begins.
  3. Has access to the view to run viewDidLoad () .
  4. Confirms that the property of the test class tableView is not nil and the property is set in the tableView of the view controller.


Then select Product \ Test again; as soon as the tests are completed, open the navigator ( Cmd + 5 - a convenient shortcut). And you should see the following:

image

Your first testing with a mock object was successful!

Testing the addPerson (_ :) method

The next test is to make sure that choosing a contact from the list will call the addPerson (_ :) method

Add the following property to the MockDataProvider class:

 var addPersonGotCalled = false 

Then replace the addPerson (_ :) method with the following:

 func addPerson(personInfo: PersonInfo) { addPersonGotCalled = true } 

Now, when you call addPerson (_ :) , you register it in the instance by setting the value to true for MockDataProvider .

You will have to import the AddressBookUI framework before you can add a method to test this behavior.

Add the following import to PeopleListViewControllerTests.swift :

 import AddressBookUI 

Now add the following test method to the rest of the test scripts:

 func testCallsAddPersonOfThePeopleDataSourceAfterAddingAPersion() { // given let mockDataSource = MockDataProvider() // 1 viewController.dataProvider = mockDataSource // when // 2 let record: ABRecord = ABPersonCreate().takeRetainedValue() ABRecordSetValue(record, kABPersonFirstNameProperty, "TestFirstname", nil) ABRecordSetValue(record, kABPersonLastNameProperty, "TestLastname", nil) ABRecordSetValue(record, kABPersonBirthdayProperty, NSDate(), nil) // 3 viewController.peoplePickerNavigationController(ABPeoplePickerNavigationController(), didSelectPerson: record) // then // 4 XCTAssert(mockDataSource.addPersonGotCalled, "addPerson should have been called") } 

So what happens here?

  1. First you install the data provider of the view controller to an instance of your fake data provider.
  2. Then you create a contact using ABPersonCreate () .
  3. Here you manually call the delegate method peoplePickerNavigationController (_: didSelectPerson :) . Typically, manually calling delegate methods is a sign of bad code, but good for testing purposes.
  4. Finally, you confirm that addPerson (_ :) was called by checking that addPersonGotCalled is true.


Select Product \ Test to run the tests. So it turns out, this is quite an easy task!

But wait! Do not hurry! How do you know that tests actually test what you think they are testing?

image

Test your tests

A quick way to verify that a test is actually checking something is to remove the object that the test is checking.

Open PeopleListViewController.swift and comment out the following line peoplePickerNavigationController (_: didSelectPerson :) :

 dataProvider?.addPerson(person) 

Run the tests again; The last test you just wrote should now fail. Masterpiece - you know that your tests actually test something. You should check your tests; at the very least, you should check the most complex tests to make sure they work.

image

Uncomment the line to get the code back into operation; run the tests again to make sure everything works.

Mocking Apple Framework Classes

You could use singletons such as NSUserDefaults.standardUserDefaults () and NSNotificationCenter.defaultCenter () , but how would you test the default value? Apple does not allow you to check the status of these classes.

You could add a test class as an observer of the expected result. But it can slow down your tests and make them unreliable, since they depend on the implementation of those classes. Or the value could be set from another part of your code, and you did not check the isolated behavior.

To get around these limitations, you can use mock objects instead of these singletons.

Note: When replacing Apple classes with a mock object, it is very important to test the interaction with that class, and not with the behavior of that class, since implementation details may change at any time.

Compile and run the application; Add John Appleseed and David Taylor to the list of people and switch the sorting between “Last Name” and “First Name” . You will see that the order of the contacts in the list depends on the sorting.

The code that is responsible for sorting is in the changeSort () method in PeopleListViewController.swift :

 @IBAction func changeSorting(sender: UISegmentedControl) { userDefaults.setInteger(sender.selectedSegmentIndex, forKey: "sort") dataProvider?.fetch() } 

It adds the selected segment index for key sorting to NSUserDefaults and calls the fetch () method. The fetch () method should read this new sort order with NSUserDefaults and update the contact list shown in PeopleListDataProvider :

 let sortKey = NSUserDefaults.standardUserDefaults().integerForKey("sort") == 0 ? "lastName" : "firstName" let sortDescriptor = NSSortDescriptor(key: sortKey, ascending: true) let sortDescriptors = [sortDescriptor] fetchedResultsController.fetchRequest.sortDescriptors = sortDescriptors var error: NSError? = nil if !fetchedResultsController.performFetch(&error) { println("error: \(error)") } tableView.reloadData() } 

PeopleListDataProvider uses NSFetchedResultsController to fetch data from Core Data . To replace the sorting of the list, fetch () creates an array using sorting descriptors and sets it in the query query of the selected result controller. It then performs a selection to update the list and call the reloadData () method for the table.

You will now add a test to make sure that the preferred sorting order of the user is correctly set to NSUserDefaults .

Open PeopleListViewControllerTests.swift and add the following class definition below the Mockdataprovider class definition :
 class MockUserDefaults: NSUserDefaults { var sortWasChanged = false override func setInteger(value: Int, forKey defaultName: String) { if defaultName == "sort" { sortWasChanged = true } } } 

MockUserDefaults is a subclass of NSUserDefaults ; it has the sortWWCChanged boolean property with a default value of false . It also overrides the setInteger (_: forKey :) method , which changes the value of sortWasChanged to true .

Add the following test below the last test in the PeopleListViewControllerTests class:

 func testSortingCanBeChanged() { // given // 1 let mockUserDefaults = MockUserDefaults(suiteName: "testing")! viewController.userDefaults = mockUserDefaults // when // 2 let segmentedControl = UISegmentedControl() segmentedControl.selectedSegmentIndex = 0 segmentedControl.addTarget(viewController, action: "changeSorting:", forControlEvents: .ValueChanged) segmentedControl.sendActionsForControlEvents(.ValueChanged) // then // 3 XCTAssertTrue(mockUserDefaults.sortWasChanged, "Sort value in user defaults should be altered") } 

Here is the report of this verification:
  1. You first assign an instance of MockUserDefaults to the userDefaults of the view controller; This technique is known as dependency injection.
  2. Then create an instance of UISegmentedControl , add the view controller as a termset for .ValueChanged .
  3. Finally, you confirm that setInteger (_: forKey :) the default user mock object was invoked. Notice that you check if the value was actually stored in NSUserDefaults .


Run your test suite — they should all complete successfully.

As for the case when you have a really complicated API or framework, but you really want to test a small component, don't “dig in” deep into the framework!

That's when you “fake” it, not create it! :]

Writing Fakes Objects

Fakes objects behave like the full implementation of the classes they fake. You use them as substitutes for classes or structures that are too difficult to work with.

In the case of an application, you do not need to add entries and select them from Core Data . So instead you fake Core Data . Sounds a little scary, doesn't it?

Select the BirthdaysTests folder and go to File \ New \ File .... Select the iOS \ Source \ Test Case Class template and click Next . Name your class PeopleListDataProviderTests , click Next and then Create .

Again, remove unnecessary tests in the test class you created:

 func testExample() { // ... } func testPerformanceExample() { // ... } 

Add the following two imports to the new class:

 import Birthdays import CoreData 

Then add the following properties:

 var storeCoordinator: NSPersistentStoreCoordinator! var managedObjectContext: NSManagedObjectContext! var managedObjectModel: NSManagedObjectModel! var store: NSPersistentStore! var dataProvider: PeopleListDataProvider! 

Properties contain the main components that are used in the Core Data stack. To get started with Core Data , see our Core Data Tutorial: Getting Started.

Add the following code to the setUp () method:

 // 1 managedObjectModel = NSManagedObjectModel.mergedModelFromBundles(nil) storeCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) store = storeCoordinator.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil, error: nil) managedObjectContext = NSManagedObjectContext() managedObjectContext.persistentStoreCoordinator = storeCoordinator // 2 dataProvider = PeopleListDataProvider() dataProvider.managedObjectContext = managedObjectContext 

Here is what happens in the above code:

  1. setUp () creates a managed object context using in-memory storage. Typically, Core Data is a file in the device file system. For these tests, you create a 'permanent' storage in the device's memory.
  2. Then you create an instance of PeopleListDataProvider and a managed object context with an in-memory store that is set as a managedObjectContext. This means that your new data provider will work as real, but will not add or delete objects in Core Data.


Add the following two properties to PeopleListDataProviderTests :

 var tableView: UITableView! var testRecord: PersonInfo! 

Now add the following code to the end of the setUp () method:
 let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("PeopleListViewController") as! PeopleListViewController viewController.dataProvider = dataProvider tableView = viewController.tableView testRecord = PersonInfo(firstName: "TestFirstName", lastName: "TestLastName", birthday: NSDate()) 

This configures the table view by instantiating the view controller from the storyboard and creates an instance of PersonInfo that will be used in the tests.

When the test is completed, you will need to “reset” the managed object context.

Replace the tearDown () method with the following code:

 override func tearDown() { managedObjectContext = nil var error: NSError? = nil XCTAssert(storeCoordinator.removePersistentStore(store, error: &error), "couldn't remove persistent store: \(error)") super.tearDown() } 

This code sets the managedObjectContext to nil to free up memory and remove the persistent store from the store coordinator. You can run each test with a new test repository.

Now - you can write a test! Add the following test to your test class:

 func testThatStoreIsSetUp() { XCTAssertNotNil(store, "no persistent store") } 

This test verifies that the store is not nil. Run a new test - everything should be successful.

The following test will check if the data source provides the expected number of rows.

Add the following test to the test class:

 func testOnePersonInThePersistantStoreResultsInOneRow() { dataProvider.addPerson(testRecord) XCTAssertEqual(tableView.dataSource!.tableView(tableView, numberOfRowsInSection: 0), 1, "After adding one person number of rows is not 1") } 

First add the contact to the test repository, then confirm that the number of rows is 1.
Run the tests - they should all succeed.

By creating a fake “permanent” storage, you ensure fast testing and allow the disk to remain clean, so you can be sure that the application will run when you start, as expected.

Written test You can also check the number of sections and lines after you have added two or more test contacts; it all depends on the level of confidence you are trying to achieve in the project.

If you have ever worked with several teams on a project at once, you know that not all parts of the project are ready at the same time, but you already need to test your code. But how can you test part of your code for something that does not exist, for example a web service?

Stub'y come to the rescue!

Writing Stubs

The stubs forge the response to the object's method calls. You will use stubs to test your code calling a web service, which may be still in development.

The web team for your project was tasked with creating a web service with the same functionality as the application. The user creates an account on the service and can then synchronize data between the application and the service. But the web team didn’t even start their part of the work, and you’ve almost finished the development. Looks like you should write a stub to replace the web server component.

In this section, you will focus on writing tests of two methods: one for selecting contacts added to the site and one to add contacts from your application to the web service. In the real scenario, you will need a login and account and error handling, but we will do this another time.

Open APICommunicatorProtocol.swift ; This protocol declares two methods for getting contacts from a web service and adding contacts.

You could move instances of Person, but this would require a different managed object context. Using structures has become much easier in this case.

You will now create stubs to support the interaction of the view controller with the APICommunicator instance.

Open PeopleListViewControllerTests.swift and add the following class definition within the PeopleListViewControllerTests class:

 // 1 class MockAPICommunicator: APICommunicatorProtocol { var allPersonInfo = [PersonInfo]() var postPersonGotCalled = false // 2 func getPeople() -> (NSError?, [PersonInfo]?) { return (nil, allPersonInfo) } // 3 func postPerson(personInfo: PersonInfo) -> NSError? { postPersonGotCalled = true return nil } } 

Something needs to be noted:
  1. Even if the APICommunicator is a structure, the simulated implementation is a class. In this case, it will be more convenient to use the class, because your tests require you to modify the data. This is a little easier to do in class than in structure.
  2. The getPeople () method returns what is stored in allPersonInfo. Instead of downloading data from the network, you simply store the contact information in a simple array.
  3. The postPerson (_ :) method sets the postPersonGotCalled value to true.


Now it's time to test your Stub API to make sure all contacts returned from the API are added to the repository when you call the addPerson () method

Add the following test method to PeopleListViewControllerTests :

 func testFetchingPeopleFromAPICallsAddPeople() { // given // 1 let mockDataProvider = MockDataProvider() viewController.dataProvider = mockDataProvider // 2 let mockCommunicator = MockAPICommunicator() mockCommunicator.allPersonInfo = [PersonInfo(firstName: "firstname", lastName: "lastname", birthday: NSDate())] viewController.communicator = mockCommunicator // when viewController.fetchPeopleFromAPI() // then // 3 XCTAssert(mockDataProvider.addPersonGotCalled, "addPerson should have been called") } 

Here is what happens in the above code:
  1. First, you configure the mockDataProvider and mockCommunicator imitation objects that you will use in the test.
  2. Then you set up some fake contacts and call the fetchPeopleFromAPI () method to make a fake network call.
  3. Finally test the addPerson (_ :) method.

Compile and run the tests.

And what's next?

Download the final version of the project ; this version also includes some additional tests that were not covered in this article.

You learned how to write mock objects, fakes and stubs for testing microcomponents in your application and figured out how XCTest works in Swift .

This article presents only an initial understanding of the tests; I'm sure you already have ideas for writing tests for your applications.

For more unit testing information, see Test Driven Development (TDD) and Behavior Driven Development (BDD) . These are application development methodologies (and, frankly, represent a completely new way of thinking) where you write tests before you write code.

Unit testing is only one part of a complete test suite; comprehensive testing is the next logical step. A simple way to get started with integrated testing is to use UIAutomation . If you are serious about testing your applications, then you need to use UIAutomation !

ps Since the article was written earlier 09/09/2015, Swift 1.2 was used to write the examples. I made some changes to the examples due to the release of the new Swift language version. Source code for projects can be found here and here .

Source: https://habr.com/ru/post/267255/


All Articles