In any professional area in society, testing your achieved work is one of the most important tasks. It's the final proof that your product or service really works as everyone expect and you are ready to offer it to your clients or users. If you don't test your results, you shall face a lot of consequences, from not satisfied users to lost lives in an accident. So, you really must worry with it.
In this article, we will talk about unit tests in a simple iOS project considering a layer-based architecture with 3 classes communicating with each other through delegation and input protocols. We will discuss how to make each class to not interfere in the other tests, since they depend on each other.
The project architecture
As we said, we have three classes in our project: UpperLayer, MiddleLayer and LowLayer. Each one talks with the next lever with an input protocol with its its interface methods. The communication with the lower level of each layer will happen through a delegate, which informs that instance about important events in the next layer. Take a look on the image below to have a better understanding:
In this small project we are building, we will be testing only the MiddleLevel class. The UpperLevel is responsible for executing arithmetic operations with integers, which are triggered by the MiddleLevel and the results are returned to it via delegation. Then, the MiddleLevel triggers its delegate to tell the results. In this context, the LowerLevel is the MiddleLevel delegate, which may represent something like a UIViewController or something like that.
Open the Xcode and create a new project called "MathTestDemo" as a Single View Application.
Create three new classes: LowerLevel, MiddleLevel and UpperLevel. The MiddleLevel and the UpperLevel must have their own input protocols, which contain their methods, and their own delegates. Naturally, MiddleLevel will be UpperLevel's delegate and LowerLevel will be MiddleLevel's delegate.
Now that we know the main architecture, let's build a unit test class for the MiddleLevel
Create a new class under the Unit Test Case Class template inside MathTestDemoTests folder choosing the unit test target and name the new class as MiddleLevelTests
The test classes extend from XCTestCase, a specific class that contains an environment and lifecycle that is useful for unit testing. Each method or computed variable of MiddleLevel will be called in a test case, which corresponds to a method inside MiddleLevelTests named with the test word in its prefix.
Before talking about test suits, we must notice something very important: MiddleLevel calls UpperLevel in each of its methods requesting an operation. Since we are testing only the MiddleLevel class, something that is wrong in UpperLevel can interfere in our test, so we must focus on testing only what is coded in MiddleLevel functions. The solution for that is something called Mock.
Maybe you still don't understand why each layer in the project communicates with the others through protocols instead of declaring the class itself as a property. The reason for that is we just want some object that implements those operations, not necessarily the class we shall use in the application target. Mocks are classes that belong to the test target and just return a direct result to its delegate, in the case our tested class. By this way we don't need to rely on any complex operation.
This is really important when we test a class that makes an API call. Since we are not testing the API service itself, we just mock the JSON response, send it to the provider and check if it makes the correct operations with the data.
Enough talking, create a new class in the test target called UpperLevelMock:
See that it also has a delegate of UpperLevel and tells it the operations result, that is a constant with 10 as value. Notice that we don't make any complex operation, we return directly a result, since the operations don't matter for us, we just want a result to be tested in the MiddleLevel.
Anytime we need to test a method that relies on another class represented by a protocol, we must mock this class to define which response we are expecting and avoid letting that class interfere in our tests.
I forgot to tell about that "@testable import" thing. As we are in a different target from our main application, we don't have access to its classes, protocols and methods, so we need to include it in our test class and mocks. The "@testable" keyword allows our test target to see all the internal properties of that target, not only the public ones
Back to our test class
Now that we created a new mock for our UpperLevel, let's define the lifecycle of a XCTestCase class. We have two methods that are important to us: setup and tearDown. Setup is called before each test case and its function as the name says is to setup everything, create the object that about to be tested, instantiate the mocks and any data we need. Teardown is made to clean the memory, we define all the pointers as nil, arrays empty and get back to the state before that test case. With this information, we can design this way:
Define your MiddleLevelTests class:
Wait a sec…How are we supposed to get the results since our MiddleLevel sends them to a delegate? We need a delegate to access them! Well, my solution for that is:
The test class itself receives the results via delegation
As we said, all the test cases run as functions with the test prefix on it. Each one must check the preconditions before that method execution and pos-conditions. We check them via the XCAssert operator:
As you can see, we are running a unit test of the add function within the MiddleLevel class. We check the pre and pos conditions in both the beginning and ending. First we assert that the sum variable is nil, the test fails every time the assertive is not verified. Then we apply the method we are going to test, followed by the pos condition, which checks the final result.
There are other types of XCAssert, such as XCTAssertTrue, that picks a boolean expression that we must guarantee it's true, XCAssertFalse, which works in the opposite way, etc.
Our test class tests two methods: the add and subtract operations, so what is really happening is that we mocked our UpperLevel, not allowing it to influence in our tests and assigned the suit under test to our test class itself, this way we are executing only the MiddleLevel methods simply and fetching its results though delegation:
Out final test class must be:
To run your test cases, press COMMAND+U. All your unit tests will be executed and you may check each test method if it was succeed our failed.
Those green checkmarks indicate that our test cases succeeded, but, if one of those asserts fails, it will be marked as red informing us what didn't work.
In this article we demonstrated how to proceed with unit tests considering a linear and clean architecture as each layer communicates with the other through input protocols and delegates. We saw how to test each single method and how to run all the unit tests at once. How you enjoyed ;)