by Azura Sakan Taufik
March 2023
Motivation
source: Apple Developer Documentation
Letβs start with a simple question, Why?
Weβre constantly writing code in our code base. Who can ensure that the changes we made will not affect other parts/areas of the code? Especially when weβre collaborating. Different developer often have a different way of thinking. When our coworker is working on the code weβve previously written, there may be changes and if not tested properly, can cause flaws in the business logic.
Unit testing allows us to have confidence that any changes we made will not cause regressions down the line.
Beware that implementing unit test in the middle of an existing project will be difficult, but, itβ will be worth it. This is because we may have made some poor design decisions that does not take into consideration about testing which may lead to high coupling of the codebase. We have to reduce coupling in order for our code to be testable, so it will require more effort and time on our end.
When creating a unit test, keep in mind of:
- The component you want to test
- The behaviour you want to assert
Prioritize features based on:
- Number of bug reports
- Highest regression impacts
Good to Know: Types of Testing
source: Apple Developer Documentation
in addition there is also performance testing but we can worry about that later after we cover this pyramid.
Unit Testing
- Should assert the expected behaviour of a single path through a method or function in your project
- For multiple paths, write one test for each scenario, in our app it would be like writing a test for each type of user (anon, regon, suber) for example when opening a detail article, whatβs the expected results?
- Writing the test
- Pick a class or function as βsubject under testβ, this means 1 class = 1 unit test file
- Create a method that starts with the word
test
- Method should take no arguments & returns
Void
- Method should take no arguments & returns
Structure & Naming Conventions
source: Swiftful Thinking and Keep It Swift
Test File Name
The same as the subject under test with additional suffix βTestsβ Example:
- Subject under test: LoginByPhoneNumberVM
- Test file: LoginByPhoneNumberVMTests or optional using
_
underscore to separate for redability LoginByPhoneNumberVM_Tests
Test File
- setUp function is invoked before any of the tests are run
- tearDown function is called after the tests are run
[! info] Working Backwards Itβs a common practice to make your test fail first to ensure that it is running properly, then fix it according to your needs.
Test Name
test_UnitOfWork_StateUnderTest_ExpectedBehaviour()
or
test_methodName_withConditions_shouldExpectation()
- Unit of Work: A unit of work is a use case in the system that startes with a public method and ends up with one of three types of results:
- a return value/exception,
- a state change to the system which changes its behavior,
- or a call to a third party (when we use mocks). so a unit of work can be a small as a method, or as large as a class, or even multiple classes. as long is it all runs in memory, and is fully under our control.
- State Under Test: the current condition of the unit
- Expected Behaviour: the outcome we want from the specified condition
examples:
- test_onCardthumbTapped_withSuber_shouldBeOpened
- test_onSubscriptionTapped_withActivePurchaseToken_shouldShowApulo
π₯ This pattern is followed by many authors including Hacking With Swift, and is introduced by Roy Osherove in 2005. Itβs said to be efficient in avoiding unreadability and missing test cases.
Also, when weβre stress testing itβs good to differentiate them by adding a suffix of stress()
at the end of the function name
Test Content
Use the structure of :
- Given (some context)
- When (certain action is applied)
- Then (shouldβ¦) This is similar to what Apple advocates for in their documentation:
- Arrange (dependencies)
- Create any objects or data structures you need to use
- Replace complex dependencies with easy-to-configure βstubsβ to ensure deterministic result
- Adopt protocol-oriented programming
- Act
- Call method/function using previously arranged dependencies
- Assert
- Compare the expected & actual behaviour example:
Folder Structure
Mock vs Stub
source: tuntsdev
Mock
Helps us use stubs that can be asserted to validate a flow
Stub
Is a simple fake object to help you write your tests Example: fake json response
Dependency Injection
source: tuntsdev
When an object or function depends on another object or function fo functionality
Reducing Coupling
Replace concrete types with protocol
- When a class contains many functions and properties that can have different implementations
- Common area of problem:
- Accessing external state
- User documents/databases
- example: tap buka email pada snackbar, di aplikasi kita pasti membuka default app tapi di test kita bisa menulis implementasi yang berbeda karena belum tentu semua user memiliki aplikasi email pada hp nya
- Cases that donβt have deterministic values
- Network connections
- Random value generator
- Accessing external state
Replace named type with metatype value
- When a class creates or uses instances of aother class
- Common area of problem:
- Creating a new document on the filesystem due to a user action
- Interpret JSON & create new CoreData managed objects
- example: download epaper
Subclass and override untestable methods
- When a class combines custom logic with interactions and behaviour
- Common area of problem:
- View Models
- View Controllers
- example: showing subscription status in sidenav/akun, we need to fetch data from user default in order to know what data to display
Inject a singleton
- Turn the singleton into a parameter that can be replaced to support isolation for testing
Summary
Tips | Problem | Solution |
---|---|---|
Replace concrete types with protocol | we canβt have different implementations according to the test case that we want | create a protocol that lists the methods & properties used by your code, conform to the protocol in your tests and define your own implementation |
Replace named type with metatype value | the object is created by the code we want to test, the object does not exist until the code is run | define a variable on the class under test that represents theΒ typeΒ of object it should construct, this way in your test you can access/use the variable |
Subclass and override untestable methods | coupled between logic and ui interaction | subclass the view controller or view model and βstub outβ the methods that produces complexity and use the subclass to bne implemented in your tests |
Inject a singleton |
Types of Assertions
source: AppsDeveloperBlog
Unconditional Fail
XCTFail
Equality Tests
XCTAssertEqual
XCTAssertEqualWithAccuracy
XCTAssertNotEqual
XCTAssertGreaterThan
XCTAssertLessThan
XCTAssertLessThanOrEqual
Boolean Tests
XCTAssertTrue
XCTAssertFalse
Nil Tests
XCTAssertNil
XCTAssertNotNil
XCTAssertUnwrap
Exception Tests
XCTAssertThrowsError
XCTAssertNoThrow
Custom Assertion Tests
source: Swift by Sundell
References
- Royβs Book
- https://www.swiftbysundell.com/discover/unit-testing/
- https://developer.apple.com/documentation/xcode/improving-your-app-s-performance
- https://qualitycoding.org/unit-test-naming/
- https://medium.com/@dhawaldawar/how-to-mock-urlsession-using-urlprotocol-8b74f389a67a
- https://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html
- https://github.com/tunds/SwiftUIiOSTakeHomeTest
Whatβs Next?
- Since testing folder structure should follow the structure of the original code, itβs best for us to organize the existing folder first to avoid confusion and double work.
- Ensure everything is already using Async/Await in order for all the code to be the same. If Iβm not mistaken, writing unit test for Swiftβs async/await concurrency is slightly different with Combine.
- Define the scope of our unit tests based on our current architecture: service, repo, vm
- Trial and error to identify coupling in our code base especially with singletons
- Start from client api & api service first before moving on to next scope
Concerns
- Testing a function which outputs a navigation process? for example: after finishing recaptcha if success we want the sheet to be dismissed and the fullscreen cover to be dismissed as well. How do we test this? or should we just ensure the registered api & membership api are called?
- Tried mocking the client api & an api service but ran into a a little bit of confusion:
- Since ClientAPI requires a specific service to be dispatched, does it mean we donβt need to create a test for that struct?
- When trying to mock for an api service, ran ito confusion because most tutorials and resources do not use βclient apiβ to help dispatching the service, instead most of them calls
URLSessionDataTask
from within the api service, which was pretty easy to understand. However, as in our case, because we are using client api, we need to mock theURLSession
that is within that struct. - At first I thought we simply need to create a
protocol
so that we can have aMockClientAPI
. However, it turns out that its not the case sinceURLSession
cannot be sublcassed. Init inURLSession
is not allowed, and we need to init data & response to return the method that we use which isdata(for request: URLRequest)
. - Because of this limitation, I tried another way that allows for us to still use
URLSession
but instead of mocking that class right away, we mock theURLProtocol
which will be assigned as the protocol inside ofURLSessionConfiguration
for ourURLSession
. Although, the behaviour is a little bit different. The thorough explanation can be found here. - However, in our case, these configurations βhappensβ inside the
loadData
function inClientAPI
, when what we need is for that configuration to happen inside the unit test.