Why does my application get so buggy after each release?
Why does my QA team frequently report repetitive issues and crashes?
The best solution to curb problems like these is UNIT TESTING. In this blog post, we will see how we can incorporate unit testing effectively into our codebases reducing repetitive test efforts and bug frequencies.
There are 3 types of developer level testing:
Unit testing is basically breaking down bigger complex logic into smaller testable logic.
Writing unit tests for an existing codebase could be a huge challenge. That’s the reason why we should keep developing unit test cases along with the code. So this is how an ideal development workflow should look like:
Since we don’t live in an ideal world, we will have to write test cases for an existing codebase. Unit testing can be easily approached when starting for a new project using techniques like Test Driven Development, Protocol Oriented Programming and Dependency Injection whereas while approaching with unit testing for an existing codebase/project, we will have to incorporate these techniques followed by a lot of code refactor(and by a lot, I mean A LOT of code refactor).
XCTest library provides a common framework for writing unit tests in Swift for Swift packages and applications. We can either include unit test cases when creating a new project or add a unit test target later into our project. Mention your testable target(the app to be tested) in your test class to access its classes. A unit test case lifecycle has 2 main methods
These methods get called automatically before and after each test case execution respectively. We can perform any resource allocation-deallocation in these methods.
Note: Test cases should be completely independent of one another because they execute asynchronously.
What qualifies as a valid test case? One that can pass and fail both. Most of the times, we test the core business logic of the application which is built of the following pieces:
We need to identify and isolate each testable piece making it independent of other pieces of code. For example, one method should only do one single task like checking for valid numbers in an entered text.
How? Now that we know what pieces have to be tested, let’s have a look at how we can test them.
1. Mocking is testing your code logic using dummy data or mocked data. Writing unit test cases with mocked data gives a near to real simulation of testing making it an ideal way to unit test. Listing down pieces of code that need to be mocked:
Mocking the data for unit testing can sometimes be a real pain especially network calls. We might have to put more coding efforts in mocking the data than the actual development of the code itself. Sometimes developers have to mock an entire class. Listing down 2 approaches to curb the mocking problem:
Some cases can be tested by partial mocking whereas some cases need complete mocking of the objects being tested. This approach needs a lot of code refactor if being applied to an existing codebase.
2. Stubbing is using a custom framework/library to stub network responses without actually making any network call to the server. Here, we have complete control over what responses we want to test without any internet or server dependencies. Stubbing needs a lot less coding efforts when it comes to writing the unit test cases. Some most popular libraries for stubbing are OHHTTPStubs, Mockingjay, Hippolyte for XCTest(unit) testing.
We can say we will be able to simulate most of the happy testing scenarios with stubbing. We will have to update our stubs if there are any additions or changes to the responses we receive from the actual server.
3. TDD (Test Driven Development) will significantly reduce mocking and stubbing efforts for a developer or rather make mocking easier so that we don’t need to stub the data at all for unit testing. Test-driven development as the name suggests influences the development approach in a way that the resulting code is more distinct, independent and testable.
The developer starts writing unit test cases for each feature by breaking down the bigger logics into smaller pieces and then continues developing and improvising the logic basis the unit test cases identified in the process. TDD focuses on the crux of the logical piece being tested hence isolating individual testable pieces of the code. I will try to cover TDD with an example in another article.
NOTE: We have to check for the “Why” when we want to unit test a piece of code. Ask yourself “should this piece of code be tested under unit testing” or are there other testing measures that can better automate the testing in this case. We cannot test a method that only returns a particular string like font, color, a key or a constant.
Additionally, we should make sure we do not add too much code to a class to make it testable and then write unit test cases to test the extra code added to test the original class. We don’t wanna fall into that spiral loop.
Always, find a way to write your unit test case with as minimum mocking and data manipulation as possibe. Focus on the crux of the logic.
If you want to learn more about cyclomatic complexity, watch this. It nicely explains this tongue-twisting terminology.
The ideal code coverage should be 80% — 100%. Xcode has a provision check class wise code coverage for unit testing.
Below is a picture depicting how a typical pipeline would look like when we integrate unit testing into our codebase. We can either run the unit test cases manually before making a code commit or automate with a script for running the unit cases in our CICD pipeline or do both(recommended).
This was more of an informative article about unit testing. The next article would be more coding-oriented covering dependency injection, stubbing and POP.
Feel free to comment if you have any feedback or wish to add-on something that you feel is useful.
I hope you enjoyed reading this article. Thank you!
Create your free account to unlock your custom reading experience.