Unit Test

The importance of test in code

To create a quality product, one of the prerequisites is to test the product, not only in the sense of getting feedback from people, because we should know if it would impact their lives but in the backend sense, in the code.

The test is a principle that every developer needs to know, at least the basics. There is a technique called TDD - Test-Driven Development, which focuses on building satisfactory code, aiming for perfection in refactoring and code performance, so it is possible to find bugs and unnecessary code.

Looking at iOS development, there are two tests of equal importance, the Unit test, which is used to test the functionality of your app, allowing you to identify bugs or cracks, and the UI test, which tests the visual aspect. Fortunately, Xcode provides both of these to work with. Besides Xcode, the development community creates tools to improve the experience of creating and testing tests.

Creating tests is not easy, because it is necessary to have experience in writing modular code, besides that, we must know when to write code for the test. There must be tests for all the functionalities that the application has, besides this, the test should be considered as documentation because a future code change can identify what was changed.

Let's put the tests into practice, we will start with the basics:

Before creating a test, there are prerequisites that an iOS developer needs to know:

  • Protocols
  • Dependency injection.

Because these are two means that are used to make the code decoupled.

Note: This post is going to be about the unit test, in the future I will create one about the UI test.

Practice

When creating a Xcode project, there is this option:

Screen Shot 2021-05-06 at 18 59 24

Two folders will be created, but we will focus only on tests because the other one is about the UI:

Screen Shot 2021-05-09 at 18 45 08


It is important to do the test from the beginning of the project, but if it is not possible, we can add it like this:

go to icon, Test navigation and click:

Screen Shot 2021-05-06 at 19 24 35

Click on + on the lower left corner and click on Unit Test, and finish.

Screen Shot 2021-05-09 at 18 59 20


As you can see, we now have a folder where we will put our tests. However, we have to pay attention to one piece of code, because we need to import our project in order to have access to the classes.

Screen Shot 2021-05-09 at 19 14 56

Note: Be careful to create a project with the name that has numbers at the beginning, as it will replace with _

The functions they have in the file, it's self explanatory, let's put it into practice:

Note: To create a test function, we need to put the word test at the beginning of the function:

E.g:

func testCode(){}

This will create a diamond, which indicates that this function can be tested.

Screen Shot 2021-05-09 at 19 54 51

To test, press command + u or click on the diamonds.

Project

We will create a project in which it will be a soccer match, but all automated:

then our game will have two teams and during the 90 min match, the teams can score goals in two ways:

  • Penalty kick, which has a chance to hit or miss
  • goal

SoccerRandom.swift

let's create a SoccerRandom class

let's create a dependency injection in which we will put the teams in an array

protocol TeamsProtocol{
    var player: [String]{get set}
    var name: String {get}
}
struct Barcelona: TeamsProtocol{
    var name = "Barcelona"
    var player = [
        "Ter Stegen", "Migueza", "Pique", "Lenglet","Busquets", "Dest", "de Jong", "Pedri", "Alba", "Messi", "Griezmann"
    ]
}

struct RealMadrid: TeamsProtocol{
    var name = "Real Madrid"

    var player = [
        "Courtois", "Mendy", "Ramos", "Militao", "Nacho", "Modric", "Casemiro", "Kroos", "Hazard", "Benzema", "Junior Vinicius"
    ]
}

Note: We have the Real Madrid and Barcelona teams that have the 11-player lineup and the team name.

Let's put in the class SoccerRandom :

class SoccerRandom {

    let teams: [TeamsProtocol]

    init(teams:  [TeamsProtocol]) {
         if(teams.count == 2 ){
            self.teams = teams
        }else{
            self.teams = []
        }
    }

}

Note: We made the validation to allow only two teams.

In our match there are also the game score and the game time, so let's create the variables:

class SoccerRandom {

    let teams: [TeamsProtocol]

    var timeGame = 0

    var scoreboard: [String:Int] = ["Barcelona":0 , "Real Madrid":0]

    init(teams:  [TeamsProtocol]) {
        self.teams = teams
    }
}

Let's create the game features:

  • startgame()
  • penalty()
  • goal()

Since it is an automatic game I want the methods, Penalty() and Goal() to be random, so I'll create an enum to describe the methods and then create a random in the enum.

enum SoccerCases: CaseIterable {

    case penalty
    case goal
    static var allCases: [SoccerCases]{
        [.goal, .penalty]
    }
}

To start the game, we need to run the game for 90min, but since our project is a simulation I will create a Timer at speed 2x and make a counter in the variable timeGame, until it reaches 90.

There will be some event in the game every 15

func startGame(completed: @escaping ()->Void){
    Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { time in
        self.timeGame += 1

        if(self.timeGame <= 90){
            if((self.timeGame % 15) == 0) {

                switch SoccerCases.allCases.randomElement()! {
                case .goal:
                    self.goal()
                case .penalty:
                    self.penalty()
                }
            }
        }else{
            time.invalidate()

            completed()
        }

    })
}

When the time is up, I close the timer. The closure that exists in the method is for unit test purposes.

Now the goal and penalty functionality remains to be created:


func penalty(){
    guard let teamRandom = teams.randomElement() else { return }

    let hit = Bool.random()

    if(hit){
        for i in scoreboard.keys{
            if(i.contains(teamRandom.name)){
                self.scoreboard[i]! += 1
            }
        }
    }
}

func goal(){
    guard let teamRandom = teams.randomElement() else { return }

    for i in scoreboard.keys{
        if(i.contains(teamRandom.name)){
            self.scoreboard[i]! += 1
        }
    }
}

In the penalty, we are doing a random Boolean, if true, it was a goal for any team, and in the goal function, we are assigning a goal also for some team.

Our class looks like this:

class SoccerRandom {

    let teams: [TeamsProtocol]

    var timeGame = 0

    var scoreboard: [String:Int] = ["Barcelona":0 , "Real Madrid":0]

    init(teams:  [TeamsProtocol]) {
         if(teams.count == 2 ){
            self.teams = teams
        }else{
            self.teams = []
        }
    }

    func startGame(completed: @escaping ()->Void){
        Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { time in
            self.timeGame += 1

            if(self.timeGame <= 90){
                if((self.timeGame % 15) == 0) {

                    switch SoccerCases.allCases.randomElement()! {
                    case .goal:
                        self.goal()
                    case .penalty:
                        self.penalty()
                    }
                }
            }else{
                time.invalidate()

                completed()
            }

        })
    }

    func penalty(){
        guard let teamRandom = teams.randomElement() else { return }

        let hit = Bool.random()

        if(hit){
            for i in scoreboard.keys{
                if(i.contains(teamRandom.name)){
                    self.scoreboard[i]! += 1
                }
            }
        }
    }

    func goal(){
        guard let teamRandom = teams.randomElement() else { return }

        for i in scoreboard.keys{
            if(i.contains(teamRandom.name)){
                self.scoreboard[i]! += 1
            }
        }

    }
}

Now let's do our class test: We must test:

  • has only two teams playing
  • counter if it is working,
  • goal assignment

Test

import XCTest
@testable import __UnitTest

class __UnitTestTests: XCTestCase {
var soccerRandom = SoccerRandom(teams: [Barcelona(), RealMadrid(), Barcelona()])

override func setUpWithError() throws {

    //Verifier constructor
    XCTAssertNotNil(soccerRandom)

    //Verifier inicial time
    XCTAssertEqual(0, soccerRandom.timeGame)

    //Verifier inicial teams

    if(soccerRandom.teams.count == 2){
        XCTAssertEqual(2, soccerRandom.teams.count)
    }else{
        XCTAssertEqual(0, soccerRandom.teams.count)

    }
}

The setup function, is the first function that this class enters when running the test, so I checked some parameters of the SoccerRandom class, to check I used:

  • XCTAssertNotNil
  • XCTAssertEqual

let's check if the counter is right:


func testVerifierTimeScoreboard() throws{

    let expectation = self.expectation(description: "simple expectation")

    soccerRandom.startGame {
        XCTAssertTrue(self.soccerRandom.timeGame > 0)

        expectation.fulfill()

    }
    waitForExpectations(timeout: 45, handler: nil)

}

To check the counter, I used waitForExpectations, which could be used to make API requests.

And lastly, let's check for goal attribution:


func testSimulatorGoal() throws {
    self.soccerRandom.goal()

    let valueScoreboardTeam1 = Array(soccerRandom.scoreboard)[0].value
    let valueScoreboardTeam2 = Array(soccerRandom.scoreboard)[1].value

    if(valueScoreboardTeam1  > 0){
        XCTAssertEqual(valueScoreboardTeam2, 0)
    }else{
        XCTAssertEqual(valueScoreboardTeam1, 0)

    }
}

You may have noticed that I used some annotations to do the validations, like XCTAssertEqual, XCTAssertTrue, because that's what we validate with. There is this site that teaches the main methods that are normally used, but if you prefer you can study the Apple documentation.

Our test class looks like this:


import XCTest
@testable import __UnitTest

class __UnitTestTests: XCTestCase {
    var soccerRandom = SoccerRandom(teams: [Barcelona(), RealMadrid(), Barcelona()])

    override func setUpWithError() throws {

        //Verifier constructor
        XCTAssertNotNil(soccerRandom)

        //Verifier inicial time
        XCTAssertEqual(0, soccerRandom.timeGame)

        //Verifier inicial teams

        if(soccerRandom.teams.count == 2){
            XCTAssertEqual(2, soccerRandom.teams.count)
        }else{
            XCTAssertEqual(0, soccerRandom.teams.count)

        }
    }
    func testVerifierTimeScoreboard() throws{

        let expectation = self.expectation(description: "simple expectation")

        soccerRandom.startGame {
            XCTAssertTrue(self.soccerRandom.timeGame > 0)

            expectation.fulfill()

        }
        waitForExpectations(timeout: 45, handler: nil)

    }

    func testSimulatorGoal() throws {
        self.soccerRandom.goal()

        let valueScoreboardTeam1 = Array(soccerRandom.scoreboard)[0].value
        let valueScoreboardTeam2 = Array(soccerRandom.scoreboard)[1].value

        if(valueScoreboardTeam1  > 0){
            XCTAssertEqual(valueScoreboardTeam2, 0)
        }else{
            XCTAssertEqual(valueScoreboardTeam1, 0)

        }
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        measure {
            // Put the code you want to measure the time of here.
        }
    }

}

Conclusion

I have been studying testing for a short time, and I was lost as to how to start, to be honest, I didn't even know what it was, but since the beginning of the Java language, the term test was already being talked about, besides, many companies ask for it as a prerequisite for hiring. For any suggestion, criticism or doubt, just comment. I hope to have helped you!