31Jan
Three Essential Rules for Architecting iOS Unit Tests in 2020
Three Essential Rules for Architecting iOS Unit Tests in 2020

Unit testing on the iOS operating system is one of the best features you’ll be able to involve yourself in. Not only does it effortlessly allow you to optimize your code and ensure it’s working in the best way possible, but it also allows you to single out specific features and test them without needing to run your entire app at once manually.

It’s super handy, and Apple has given a ton of support over to this feature to help you make the most of it. Still, even despite this, it’s surprising how few iOS developers write unit tests and implement them into their daily practices and operational processes.

The truth is that a lot needs to go into creating a successful unit test that actually sets out to achieve what you want it to achieve and gives you professional and reliable results. In today’s guide, we’re going to detail the three rules you need to know to get the very best experience possible.

What is Unit Testing on iOS?

Just so we’re all on the same page when it comes to unit testing iOS apps, a unit test is a small test you carry out on a component of your app to ensure it works properly. For example, if the app is an alarm clock that goes off at a user-specified time, you would check to make sure it does, in fact, go off at the right time. This function is a unit.

You should also run unit tests as automated tests to ensure there is minimal chance of human error, giving you inaccurate results, and you should always run these tests on every single function of the app, to ensure everything is running and working properly. This is a great way to find potential errors to fix, to get your app to the best possible working capability it can be.

If you do find something – this is known in the industry as a regression, since your app is literally regressing in terms of performance and functionality – running these small tests means you should be able to quickly find the issue at hand and get it back to full working order.

Now that we’re on the same page let’s jump into the three essential rules you need to know!

Using Inversive Behaviours (Negative Tests)

To get things started, now we’re on the same page as to what unit testing is, it’s important to note that you’re not just using these tests to see whether everything works successfully, but also to see how your app responds when things aren’t right, such as when there are errors when a user is using your app.

This is also known as negative testing, and unit tests are essential when carrying this out. Commonly when you test your individual components, you’re going to be finding out what they do and what they’re able to achieve, but this approach allows you to also see what they don’t do, which is essential when it comes to seeing the capabilities and limits of your app and which gives you much better knowledge of your app in general.

An example of this could be a simple stopwatch lap timer. When the button is pressed on the app, the stopwatch takes a reading for the lap time but keeps timing. Now you run unit tests to see how this component can be used and what it does.

Will it trigger at certain times? Will it allow you to do two readings? Will it erase the first reading if you press the button a second time? Will the timer start back from the beginning or continue from where it left off? The questions you can ask here are endless. We all know what coding apps can be like at times, but the answers can all be found in unit testing.

Using fake data, you’ll be able to test all possible outcomes to ensure there are no conflicts, and thus the app doesn’t do what you don’t want it to do. You can change the criteria however you want, no matter what your app does to test all possible situations, both extremely rare and incredibly possible, as well as everything in-between.

class Alarm {
              var triggerTime: Date?
              var didTrigger: Bool = false

              func shouldTrigger(currentMoment: Date = Date()) -> Bool {

                guard let triggerTime = triggerTime,
                     didTrigger == false else {
                  return false
                }
             if currentMoment > triggerTime {
               return true
                }
              return false
              }
            }

Isolate your Components. Don’t Overcomplicate Things!

Although it’s obvious you want to put all your hard work to the test and see if everything is working as it should be, you need to make sure you’re patient and isolating the components of your app and then using the unit tests separately. By testing one component at a time, you’re going to get much more accurate results on what you want to improve.

To put this simply, when you’re using XCTestCase files, you should have one for every component you have. This is how independent you should have them split up and organized with. Any tests for a single component you carry out should then be linked to that file.

Organizing in this way means you can easily identify areas and components in your app that you haven’t yet tested or may have missed. Of course, if you don’t have a test case file, you’ve probably not ran a test on it, as long as you’re using this method correctly.

However, this is backed by the necessity to ensure that you’re labeling your files correctly and making sure you’re using an appropriate naming system. If you don’t, you’re not going to be able to identify which files are which, nor which components have been tested.

There’s no denying that this kind of process can be a bit daunting for those of you who are new to the whole iOS development process and are just started out, but don’t worry; this is an incredibly good habit to develop and form. In some cases, especially likely if you’re using design processes and patterns like MVC and MVVM, you may not even have your components isolated, to begin with.

It happens; the point of isolating your components is to remember that this is how you’ve coded.You’ll want to remember to break everything down with the future projects you’re working on. However, if you’re going to start using the isolation method now, you’ll need to write yourself a view controller to access and start breaking down the files into their isolated states.

Hand in hand with what I was just saying above, if you’re not sure what you’re doing, search for some MVC and MVVN ideas online using common apps so you can get some inspiration on what you’re looking for and what first steps you should take. You can even emulate some of the ideas here until you’ve got a better understanding of what you need to achieve.

Below is the Rick Astley code that we’ll discuss further later:

class RickAstley {
                    let gonnaGiveYouUp = false
                    let gonnaLetYouDown = false
                    let gonnaRunAround = false
                    let gonnaDesertYou = false
                    let gonnaMakeYouCry = false
                    let gonnaSayGoodbye = false
                    let gonnaTellALie = false
                    let gonnaHurtYou = false
                 }

Break Down Components Further

As we’ve been speaking about above, you’re probably at a stage now where you have isolated your components, and let’s say you’ve got one in front of you now. The next step is actually to test it. However, the best approach here is to break this component down further to its raw behaviors.

You can then test these behaviors individually to see what they do and to ensure they work, both positively and negatively. A really great example of this comes from Ted Bedixson, who breaks this down as the classic Rick Astley track.

His oversimplified code reads as follows:

class RickAstley {
                    let gonnaGiveYouUp = false
                    let gonnaLetYouDown = false
                    let gonnaRunAround = false
                    let gonnaDesertYou = false
                    let gonnaMakeYouCry = false
                    let gonnaSayGoodbye = false
                    let gonnaTellALie = false
                    let gonnaHurtYou = false
                 }

With the test parameters reading like:

class RickAstley {
                    let rick = RickAstley()
                    func testThatRickWillNeverGiveYouUp() {
                      XCTAssertFalse(rick.gonnaGiveYouUp)
                    }
                    func testThatRickWillNeverLetYouDown() {
                      XCTAssertFalse(rick.gonnaLetYouDown)
                    }
                    func testThatRickWillNeverRunAround() {
                      XCTAssertFalse(rick.gonnaRunAround)
                    }
                    func testThatRickWillNeverDesertYou() {
                      XCTAssertFalse(rick.gonnaDesertYou)
                    }
                 }

And it goes on as so.

Ted admits this is a straightforward example, but it should give you a clean idea as to what you should expect. Of course, your own tests within your own app are probably (hopefully) going to be a little more complicated, but the more you master this process, the better at it you’ll become, and the better the results will be.

BONUS RULE: Be as Descriptive as Possible

It’s important to remember that detail is key when it comes to testing because you don’t want to leave your tests and come back to them a few hours, days, or weeks later, only to find that you have no idea what you were doing or what you meant by what you were saying. If you’re working in a team, this is even more important.

When you’re using your unit tests, the results will serve as the documentation that couples with your components to ensure you have everything you need to crack and combat any errors or obstacles you come up against. Again, the more detail you can include, the better off you’ll be!

The tests’ names should always show you and other people exactly what each piece of code’s expected behaviour is and which aspects you are testing.

class AlarmTests: XCTestCase {

                    let alarm = Alarm()
                    override func setUp() {
                      super.setUp()
                      //Give the alarm a fake trigger time.
                      var fakeDateComponents = DateComponents()
                      fakeDateComponents.day = 13
                      fakeDateComponents.month = 10
                      fakeDateComponents.year = 2017
                      fakeDateComponents.hour = 4
                      fakeDateComponents.minute = 30
                      fakeDateComponents.second = 0

                      let simulatedDate = Calendar.current.date(from: fakeDateComponents)
                      alarm.triggerTime = simulatedDate
                      alarm.didTrigger = false
                    }
                    func testThatAlarmTriggersAtAppropriateMoment() {
                      //Simulate one second after alarm should trigger.
                      var fakeDateComponents = DateComponents()
                      fakeDateComponents.day = 13
                      fakeDateComponents.month = 10
                      fakeDateComponents.year = 2017
                      fakeDateComponents.hour = 4
                      fakeDateComponents.minute = 30
                      fakeDateComponents.second = 1

                      let simulatedDate = Calendar.current.date(from: fakeDateComponents)!
                      XCTAssertTrue(alarm.shouldTrigger(currentMoment: simulatedDate))
                    }
                    func testThatAlarmDoesNotTriggerBeforeTheAppropriateMoment() {

                      var fakeDateComponents = DateComponents()
                      fakeDateComponents.day = 13
                      fakeDateComponents.month = 10
                      fakeDateComponents.year = 2017
                      fakeDateComponents.hour = 4
                      fakeDateComponents.minute = 29
                      fakeDateComponents.second = 59

                      let simulatedDate = Calendar.current.date(from: fakeDateComponents)!

                      XCTAssertFalse(alarm.shouldTrigger(currentMoment: simulatedDate))                    
                    }
                    func testThatAlarmWillNotTriggerAfterItHasAlreadyTriggered() {
                      alarm.didTrigger = true
                      XCTAssertFalse(alarm.shouldTrigger())
                    }
                 }

Conclusion

As you can see, there are plenty of rules you can follow to help ensure your unit testing process is as effective and as efficient as possible, as well as guaranteeing you get the best results, and your app is the best it can be. Whether you’re just starting out with this process or you’re going to take your unit tests to the next level, don’t forget how important it is to get these processes right.


Ellie Coverdale, a technical writer at Boomessays and Oxessays, shares her tips and tricks on app development and getting the most out of technology. She also helps consult businesses and enhance coders writing skills for Australianhelp in her free time.

Basic Prototype Design

For the purposes of this article, we’ll concentrate on using prototypes in design. We’ll cover definitions of a design process and prototyping, explain the different types of prototypes, as well as make a few suggestions on how to start prototyping.

Java Lambda Expressions

A lambda expression is a piece of code that is giving an alternative way to the anonymous class to pass the function as a parameter to other subsequent flows of code such as methods, constructors, etc.. In this approach, a function can be referenced with a variable and passed as a reference to be executed in a subsequent flow of code execution.

One Reply to “Three Essential Rules for Architecting iOS Unit Tests in 2020”

  1. Thanks a lot!

Leave a Reply