Mobile app testing ensures your application is working perfectly without any freezes, bugs or crashes. There are unit, auto, system and acceptance tests, and in this article we will talk about unit testing for mobile apps.
First, let’s discuss what unit tests are in general. Unit testing checks whether certain parts of the code work correctly. In other words, it helps us produce more robust and maintainable code. Each one small test is dedicated to a small feature. All of them are connected, however, so you get a kind of chain. If any part of this chain fail, the whole chain will fail as well.
As a part of mobile application testing unit tests may be written at any phase of development or even before development (test-driven development, or TDD, is one of the iOS unit testing best practices). With this kind of development, you let your tests drive the development process. They indicate what you do, what you should do next and what the result should be. Testing controls the design.
Thus, unit testing is the first step to perfectly working code. But it also is the primary means for your software quality assurance. In this article we’ll explain how to do mobile apps unit testing on an iOS example and give you a small iOS unit testing tutorial.
Do I Need to Conduct Unit Testing?
Does everyone need to do unit testing? The short answer is no. The long answer is that it is a very good tool, though it isn’t necessary in all situations.
You don’t need unit testing if:
- your app is very small and simple. If your app is small, it will be faster to check everything manually because there’s not that much logic to be checked.
- you don’t plan your app to work in the long term and you need it only for a demonstration.
- you’re a super-human who writes perfect code without any mistakes.
If you don’t fall into any of these categories, then unit testing is what you need. At first, this kind of testing may seem too time-consuming. Lack of time is mentioned as one of the most serious problems related to testing, but actually performing unit testing can save a lot of time and improve development speed. In addition, it can serve as a kind of documentation for your library. We at Mobindustry spend up to 30 percent of our development time on testing.
Unit tests have two main purposes:
- Reduce the number of bugs by making sure that our class/module/component is working as expected with all possible inputs.
- Reduce the number of regressions by making sure that our new functionality, bug fixes or refactoring hasn’t broken any existing functionality.
How Do Unit Tests Work?
How to create iOS unit testing? Xcode has built-in support for unit tests and works as an iOS unit test framework. You can use Xcode by pressing cmd+U or via the menu by selecting Product -> Test. xCode will run all active tests and generate a report with results (passed/failed). All failed assertions are indicated. Code coverage data is also generated in this framework, which allows you to get to any class and check which lines of code were called and how many times they were called.
Types of Unit Tests
Unit test for iOS can either check the UI or the logic of the application. Logic ones have a few subtypes:
- Regular ones are used to check straightforward functionality.
func testExample() {
let account = Account()
account.amount = 100.0
account.withdraw(20.0)
XCTAssert(account.amount == 80.0, "Account should have current amount of 80.0 but has
(account.amount)")
}
This is used to simply check if the withdraw() method is working correctly.
- Asynchronous tests use XCTestExpectation to check for expected outcomes.
func testExample() {
let url = URL(string: "http://www.google.com")
let expectation = XCTestExpectation(description: "expectation_description")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
XCTFail("Error: (error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
expectation.fulfill()
} else {
XCTFail("Request failed with status code (statusCode)")
expectation.fulfill()
}
}
}
task.resume()
waitForExpectations(timeout: 10) { (error) in
XCTFail("Expectation (expectation.description) has failed")
}
}
This one will check if Google’s website is available (i.e. if it returns status code 200).
XCTestExpactation is used to wait for the async task to finish. An Expectation will either finish successfully by calling the fulfill() method or will fail if the timeout time has passed but fulfill() hasn’t been called.
- Measure ones check heavy logic:
func testExample() {
self.measure {
let lotOfObjects = []
for obj in lotOfObjects {
obj.makeCalculations()
}
}
}
You can create this test to measure how much time and how many resources are spent on particular calculations inside the measure() block.
Tips on Unit Testing for Mobile Apps
Remember the structure
The common AAA approach to structuring is the most common and convenient way to write tests. AAA stands for:
- Arrange – the part that allocates all necessary objects and sets up initial parameters
- Act – the part that executes the logic
- Assert – the part that checks if the obtained results match the expected results
Сode written this way is easy to read and use, which means it’s easier to maintain.
Cover only necessary things with tests
Some people think it’s necessary to cover every single part of your code with a unit test. This approach is actually time-consuming, complex and completely useless.
There are four types of code you may have in your project. Let’s look at them starting with the most extreme options:
- Simple code without any dependencies: Probably everything is already clear with this kind of code and there’s no point in assessing it.
- Complex code with lots of dependencies: If you have this kind of code, it’s a good idea to refactor. There’s no need to cover this code because we’ll rewrite it, which means new classes will appear and method signatures will change. Why write something that won’t be used? Of course, such code also needs quality assessment, only on a higher level.
- Complex code without dependencies: This code contains algorithms or business logic. This is the perfect kind of code for testing because these are very important parts of the system.
- Moderately complex code with dependencies: Such code links different components together. Tests are very important here; they tell how exactly components must interact.
Usually, they cover the following areas of code:
- Common functionality, core models, classes and their interaction
- Main UI workflows
- Crucial logic
- Bug fixes
Unit tests may be applied not only for these areas. They can actually be used for testing almost any functionality.
Don’t cover trivial functions and system functions.
Test only one thing at a time
The idea is to divide your application into different independent modules that are able to work by themselves without interacting with other parts of the application. This allows you to work on each module separately and simplify the code. If you check several things at the same time, sooner or later your code will become overloaded and too complex. It will become difficult to support it and use.
Name your tests logically
It’s very important to name your tests properly and assign them to the correct parts of your project. Many projects have tests that no one is running because of inconvenient placement and naming. Here are some tips for logical structure:
- Choose a logical location in your version control system: They have to be part of your version control. Depending on your needs they can be organized differently, but here’s a common recommendation—if your app is monolithic, put all of them in a Tests folder; if it has many components, store relevant tests in each component’s folder.
- Choose a convention for naming: One of the best ways to do this is to add a test project to each project you build.
- Use the same convention for naming test classes that you use for naming project classes: If you have a class ProblemResolver, it’s a good idea to add a class called ProblemResolverTests. Each class should work only on one thing; otherwise, your tests will be very hard to manage.
Work with mocking objects
You can use different fake objects to make the testing process easier. There’s no need to allocate and manage a huge class or component just to use a single method from it. Such fake objects can be divided into two groups:
- Stubs
- Mocks
Stubs are fake methods for returning a predetermined response. The main use for stubs is when interacting with an API. The idea is not to wait for the actual connection to finish but to return predefined data or a predefined error instantly.
Let’s assume we have an app where you can add a friend connection. There’s a FriendListViewController that displays a list of all your friends. This friend list can be downloaded from the backend, so there’s an ApiManager class with a method like this:
ApiManager.shared.getListOfFriends(withCompletion: { (friends) in
let friendsListVC = FriendListViewController.init(friends: friends)
//Present friendsListVC
}) { (error) in
//Display error
}
This method has a completion handler, and we need to wait till it finishes.
But we actually don’t need to wait because we don’t need any APIs or even an internet connection. We need to make sure FriendListViewController is working correctly, and all we need for that is some list of friends; it doesn’t matter where they come from. So we’ll introduce an ApiManagerStubs class that will duplicate methods from the actual ApiManager but will not perform any actual API requests.
For this particular test, we’ll write a method for getting a list of friends:
func getListOfFriends() -> [Friend] {
let friend1 = Friend.init(name: "Bob", surname: "Barker", age:34);
let friend2 = Friend.init(name: "John", surname: "Doe", age:30);
return [friend1, friend2];
}
After that we can easily allocate and test FriendListViewController.
func testExample() {
let friends = ApiManagerStubs.shared..getListOfFriends()
let friendsListVC = FriendListViewController.init(friends: friends)
//Testing friendsListVC.tableView
}
Mocks are fake objects that partially duplicate real objects, allowing you to check if a method was called or a property was set after certain events.
In the example above, when a friend is deleted we need to clear the chat history for this friend. We have some class like ChatManager for this functionality. But again, for this, we don’t need to actually delete any data or call any APIs; we just need to check if the corresponding callbacks are triggered. So we’ll introduce the mock object ChatManagerMock, which behaves like ChatManager, only instead of deleting data it just remembers which callbacks were triggered.
class ChatsManagerMock {
var deleteFriendWasCalled = false
func clearChatHistoryForFriend(_ friend: Friend) {
deleteFriendWasCalled = true
}
}
Now we can implement our test as follows:
func testExample() {
let friends = ApiManagerStubs.shared.getListOfFriends()
let friendsListVC = FriendListViewController.init(friends: friends)
let mock = ChatsManagerMock()
friendsListVC.chatsManager = mock
//Trigger Delete action for first friend in the list
friendsListVC.deleteFriendButtonAction(nil, index: 0)
XCTAssert(mock.deleteFriendWasCalled, "deleteFriendWasCalled should have been called")
}
Conclusion
Unit tests are a very powerful tool to help developers avoid bugs and spot them quickly when they do appear. They help you maintain your code, but at the same time they need maintenance as well. The big problem of many projects is tests that no one uses, and to avoid it you need to stick to some rules. Your tests must:
- be reliable;
- not depend on the environment they’re run in;
- be easy to maintain;
- be easy to read and understand (even a beginner QA has to be able to understand what’s going on);
- have a standard naming convention; and
- be launched regularly and be automated.
Following these basic rules will help you get a working system. Each test will check only one thing. Your tests will be specifications for class method; it will tell all about what input they expect and what other system components expect as the output. There are very few systems like this. Such systems have an actual specification, and there’s usually not very much text in it—only basic features, server schemes and a “getting started” guideline. The life of such projects no longer depends on people; developers may come and go while the system is reliably tested and tells all about itself with the help of its own tests.
About the Author / Svetlana Cherednichenko
Svetlana Cherednichenko is a content writer. Currently she is a part of Mobindustry marketing team. She is an active member of the debate movement and she is a winner of two regional tournaments. Now she is in the process of improving her skills in graphic design and learning new frameworks. She graduated from the Dnipropetrovsk National University. Svetlana likes to travel, read about tech news and play video games. Connect with her on LinkedIn.