Unit Testing is the fundamental practice of testing the smallest components of the Software. The purpose of unit testing is to validate if each component of the software is working as they are supposed to, in isolation. The term “isolation” is very important. A unit is the smallest testable part of any software like - functions, procedures and interfaces.
A unit test provides a strict, written contract that the piece of code must satisfy. As a result, it provides several benefits. Unit tests help find problems early in the development cycle. Ideally, each test case is independent from others. Unit tests are typically written and run by software developers to ensure that code meets its design and behaves as intended.
Benefits of Unit Testing
1.Makes the Process Agile
When a product is developed in agile, change is one of the most important elements to keep in mind. When app’s features are growing, bringing about change in pre-tested code is very risky as well as costly. In this case, if test is preceded in right place then we can move ahead for refactoring confidently. Unit testing really goes hand-in-hand with agile programming of all flavors because it builds in tests that allows to make changes more easily. In other words, unit tests facilitate safe refactoring.
2. Quality of Code
Unit testing improves the quality of the code. It identifies every defect that may have come up before code is sent further for integration testing. Writing tests before actual coding makes one think harder about the problem. It exposes the edge cases and results in better code.
3. Finds Software Bugs Early
Since unit testing is carried out by developers who test individual code before integration, issues can be found very early. This can resolve problems promptly without impacting the other pieces of the code.
4. Facilitates Changes and Simplifies Integration
Unit testing permits to refactor code or upgrade system libraries even at a later date. This makes sure that the module still works correctly. It detects changes that may break a design contract. This helps with maintaining and changing the code. Unit testing reduces defects in the newly developed features or reduces bugs when changing the existing functionality.
5. Provides Documentation
Unit Tests provides live documentation of an application. Developers who want to learn the functionality provided by a particular unit can refer to the Unit Test. This helps them to jump in depth of understanding the unit’s Application Programming Interface (API). The API specifies a component in terms of their inputs, outputs, and underlying types.
6. Debugging Process
Unit testing helps simplify the debugging process. If a test fails, then only the latest changes made in the code need to be debugged.
7. Reduce Costs
Bugs detected earlier are easier to fix. Bugs detected in later phase are usually the result of many changes, and it is not really clear which change caused the bug. This is monotonous job to find the exact bug. Hence, this can raise the cost of entire program drafted, which can be very high right. Just like during system testing or during acceptance testing! Hence, Unit testing helps to reduce it.
Testable vs Untestable Code
While writing code, people usually don’t set their mind for testing. Some code gets so messed up that they are impossible to test. But it is very important that codes are completely testable. If code is not aligning with the test then it shows serious issue.
To study the case, let's go through one simple example. I will be using swift language and stick with apple’s default testing framework XCTest for the demos and examples. Let’s roll.
The above code returns the season from the current timestamp of the system. It works fine but this code is impossible to test and is not correct from testing point of view. Date() is one of the hidden input that will change the result of the function in different period of the time. Let’s try to test the code now View-model containing above function is system under test (sut). I want to test the function for month June and the tests looks like…… Phew..! Lots of setup code to change the environment and again roll back to normal environment in tear down section.
Tests like this violates lots of fundamental practices. Bulky logics in setup and teardown section is really expensive and is certainly not going to execute faster. This is unreliable as well, because it may fail even though system under test has no bugs at all. Moreover, this is not actually unit testing, it is more like between unit testing and integration testing. Due to this reason it needs particular environment setup to test simple edge case.
Ultimately, it turns out that all these testability problems are caused by the poor construction of our function findSeason().Currently our method suffers different problems.
It is tightly coupled to the concrete data source
In this method the data source is internally absorbed. This method cannot be reused to process time of day from the date and time emitted from other sources or passed as argument. The method works only with the date and time of the particular machine that executes the code. Tight coupling is the primary root of most testability problems.
It violates the Single Responsibility Principle (SRP)
If a single function has multiple responsibilities then the function is against SRP. Here our method consumes the information and also processes it. Another indicator of SRP violation is when a single class or method has more than one reason to change. From this perspective, the findSeason() method could be changed either because of internal logic adjustments, or because the date and time source had to be changed.
It lies about the information required to get its job done.
Normally, the function signature must be enough to inform the consumer about function’s behavior. Here However, developer must read each line inside the function to know the source of data.
It is hard to predict and maintain
If the behavior of the method relies on the global mutable state of the system or variables then the method’s behavior is not predictable. It is necessary to consider its current value, along with the whole sequence of events that could have changed it in earlier process. In a real-world application, trying to unravel all that stuff becomes a real headache.
Let's look out for the solution. Fortunately, we can make this code easily testable via separating the tightly coupled components which makes the result non-deterministic. We can achieve this introducing the argument method.
Now, we have changed the function signature, we have to pass the Date Type data as argument. So, the data source has been decoupled from the function and the purpose of method is now singular. The output of the function solely depends upon the input of the function; as a result, making the function deterministic. And, the test code of the function.
See! Simple refactor in function signature can resolve all the issues discussed earlier. Excellent...! The Function is Testable now. Yet, the clients using the function may be untestable and this creates the tight couple in higher level of abstraction. This can be solved by Dependency Injection which provides the IoC (Inversion of Control), will be discussed in separate section of the blog.
Synchronous Code vs Asynchronous Code Test
First of all, to ride through the terminology, Synchronous execution of the code means your program starts at the first line of source code and each line of code executed sequentially thereafter. Your code executes line by line, one line at a time. If any function is called then the program execution waits until that function returns before continuing to the next line of code.
Unlike synchronous, Asynchronous code executes out of order. In other words, program execution doesn't wait for the result of the function, instead continues to complete other tasks and resumes the pending operation after the function returns the result later. Sounds great…!
In terms of Testing, Synchronous Code are usually tested without any hesitation and goes well down the neck. But, Asynchronous code are hectic while its comes to the testing. Practically, that's what developers believe. Let’s start with simple synchronous code testing Here the simple function which calculates the simple interest. And the Test of the above code Not that complex huh?
But as we go on developing software and start writing unit test, every developer encounters the problem of testing the asynchronous code? They can be chunk of code that makes network requests, that performs work on multiple threads, or schedules delayed operations.
The core of the problem is that, test is considered over as soon as its function returns. Because of this, any asynchronous code will be ignored, since it’ll run after the test has ended. Not only the code is hard to test, it also leads to false positivity. Let's look in the example below, this test is testing the downloadWebData function which downloads the data from url asynchronously.
The problem is that downloadWebData() is always doing its work asynchronously, meaning that the closure will be executed too late. So even if our assert will fail, our test will still always pass.
For Apple developers,XCTest framework has provided the key feature called expectation. An Expectation is a predefined outcome that you want your asynchronous code to perform.
A common way of solving problems like the above is to use expectations. It essentially provides a path to tell the test runner that they have to wait for a while before proceeding. Let’s update our test from above to use an expectation
As you can see above, using expectations can be a great way to write tests against asynchronous APIs which use completion handlers. This way, we can simply fulfil our expectation in that closure. It’s usually a good fit for code which we can’t really speed up and need to wait for. Without having to introduce unnecessary waiting time, we can proceed as soon as our asynchronous operation completes.
Using the above techniques can help us make asynchronous testing much easier in a wide range of scenarios. Yet, its is necessary to know what would be even easier. Could it be omitting asynchronous testing in the first place? What if we could turn our asynchronous code synchronous, just for testing? Ummm… Lets think…! Can we really do that? Ofcourse... Yes.!!!
First our source code in Network manager that needs to be tested looks like this.
Up to now, we tested our function with the help of expectation. We called our downloadWebData() function and wait for a result to be returned. This required our tests to execute with an Internet connection, which makes execution time much slower. This is because it takes time for real request to come in act.
Now let's try mocking. What we want to do here is to make NetworkManager use a fake session in our test code. This doesn't actually perform any requests over the network, but instead lets us control exactly how the network will behave.
We will create our mocks as subclass that overrides the method we are expecting to be called with specific results that can control in our tests. Let's start our mocking process by creating mock of URLSessionDataTaskMock
Looks convincing! We will further try same process to URLSession, but here we will override the dataTask method.
We are now ready to go! Let's use them. We now need to add dependency injection to our NetworkManager which allows us to inject a mocked session. In default URLSession.shared is used.
Finally, lets test our NetworkManager class with the mocked classes and fake data. Baammm…! Test succeeded…! Now let's grab a smoothie..!
We have tested our NetworkManger which operates asynchronous in a synchronous way. We have finally achieved the required!
Unit Testing is one of the key components of the Software development process. It is recommended as must. This may look time consuming at the beginning and senseless, but during the process you will realize its significance.
Let's think about tests while writing the code, it will make carve path to write testable ones along with good code.
Testing asynchronous code will always be complex than synchronous but with the right technique it will be easy. No matter what techniques adopted, increasing wait time is not an option.
At last, the feeling of developing a software is driven by the code. Sounds more interesting, right? Thats what is done in TDD(Test Driven Development. Let's meet at next end we will talk about it!
No amount of testing can prove a software right, a single test can prove a software wrong.