paint-brush
Swift XCTest: setUp and tearDown Are Not Dead Yetby@andreota
3,729 reads
3,729 reads

Swift XCTest: setUp and tearDown Are Not Dead Yet

by André OtaDecember 2nd, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Apple instructs us to do our unit tests using the XCTest framework. We are making use of two very important methods: setUp and tear down. For each test, Xcode will create a new instance of our test class. This means we do not have to use the setUp method and then our **sut** will be created and we won't have problems accessing it. Since we are done using it, there is no problem on removing it from our memory. So when we are going to run **test1** or **test2**, an instance of **PlayerListManagerTests will be create.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Swift XCTest: setUp and tearDown Are Not Dead Yet
André Ota HackerNoon profile picture

One of the first things I learned as an iOS developer was to avoid using force unwraps (!) as much as I could. After all, they were as dangerous as they were convenient - you don't want your app to crash in your user's hand, do you?


That's why I never really liked how Apple instructs us to do our unit tests using the XCTest framework. It would be something like this:


😤 The ugly way


final class PlayerListManagerTests: XCTestCase {
    private var sut: PlayerListManager!

    override func setUp() {
        // This will run before each test
        sut = PlayerListManager()
    }
    
    override func tearDown() {
        // This will run after each test
        sut = nil
    }

    func test1() {
        let player = PlayerModel(id: 0, name: "Diego Maradona")
        sut.add(player)

        XCTAssertEqual(1, sut.list.count)
        XCTAssertEqual(player, sut.list.first)
    }

    func test2() throws {
        let player = PlayerModel(id: 0, name: "Edson (Pelé))
        sut.add(player)

        let auxPlayer = try XCTUnwrap(sut.getPlayerFrom(index: 0))
        XCTAssertEqual(player, auxPlayer)
    }
}


As you can see, our sut is a non-optional instance variable. This means we do not have to initialize it along with our class, but if we try to access it before it has been created our application will crash. However, we are making use of two very important methods:


  • setUp: This will be called before we execute each one of our tests. This means, before we execute either test1 or test2, we will call the setUp method, and then our sut will be created and we won't have problems accessing it.


  • tearDown: This will be called after each test is executed. This means that after we execute either test1 or test2, we will call the tearDown method and our sut will be deinitialized. Since we are done using it, there is no problem with removing it from our memory.


So, it means that before we execute each one of our tests we are going to create our objects and after we finished using them we clean everything, so we can move on for our next test. Right?


Well, kinda. Actually, what we've discovered after some time is that for each test, Xcode will create a new instance of our test class. So when we are going to run test1, an instance of PlayerListManagerTests will be created. Then, when we move on to test2, another instance of PlayerListManagerTests will be initialized.


Therefore, it doesn't matter if we create our objects and clean them after the test. Since we are going to be using different instances of our class, we could very well do some things like this:


😍 The beautiful way


final class PlayerListManagerTests: XCTestCase {
    private let sut = PlayerListManager()

    func test1() {
        let player = PlayerModel(id: 0, name: "Diego Maradona")
        sut.add(player)

        XCTAssertEqual(1, sut.list.count)
        XCTAssertEqual(player, sut.list.first)
    }

    func test2() throws {
        let player = PlayerModel(id: 0, name: "Edson (Pelé))
        sut.add(player)

        let auxPlayer = try XCTUnwrap(sut.getPlayerFrom(index: 0))
        XCTAssertEqual(player, auxPlayer)
    }
}


As you can see, there is no need for setUp and tearDown. And it's true, the code above will work. And it looks a lot better since we are not using force unwrap, and we're writing less code.


However, the above has a big and dangerous downside.


😰 The reality


Now, we know that Xcode created a lot of PlayerListManagerTests instances, one for each of our tests. Can you guess what would happen if these instances were not removed from memory after they run? What if they were kept in memory until all the tests have finished?


Well, if you guessed something like “all the objects we created will still be allocated and consuming space in memory", then you're right. And guess what, this is exactly what happens. Xcode won't clean our classes and our objects until all our tests have finished.


💩 The problem


Surprised? Well, I was too the first time I heard about it. So, can you see what would happen if we had a lot of tests and a lot of objects?


Touché! We would be also using a lot of memory, maybe more than we actually have. Of course, it may be hard to achieve a stage where our machine would stop working because there is no memory left. However, we should be aware that it can happen and understand that setUp and tearDown are not as useless as they look at first sight.


So, how are you going to write your tests from now on? Do you think this memory downside is great enough for you to care? Or maybe you have not even started writing your unit tests? 😏