WidgetCenter: Installed Widgets and Unit Tests (Part 1)
How to get the installed Widgets for iOS 14+ and correctly add Unit Tests for it.
Welcome to the first part of a series of how I wrote Unit Tests for fetching the newly installed Widgets for iOS 14. This part will show how my first implementation looked like and what I learned from it. In the following posts, I will offer a refactored version of the previous code and the role that design principles played.
You can find the complete code in this repository.
Understanding the Requirements
In the first iteration of the requirements, the client wanted to know when a user installs a new Widget. He intended to measure which Widget sizes the user selected and which ones he deleted.
Unfortunately, no delegate or callback is triggered when a user finished installing or deleting a specific widget. As a result, we iterated from which Widgets the user has selected to which ones he has installed.
Subsequently, we also defined that we would track it in every new open of the app, as widgets are not something that people constantly add or delete while the app is open. Finally, the client also dropped the requirement for tracking the deleted widgets because the actual value lies in understanding the used ones.
Final Requirements
As the PO, I’d like to know which Widget sizes the user has installed every time he starts the app. How to write Unit Tests when interacting with WidgetCenter?
I created an Interactor that was responsible for interacting with the WidgetCenter, which allows getting an array of the installed WidgetFamily as following:
Approaches for Unit Tests
There are three possible approaches to use the WidgetCenter in our Unit Tests:
- Installing temporal widgets for the Unit Tests
- Subclassing of WidgetCenter
- Protocol-Based Mocking
1. Installing temporal widgets for the Unit Tests
I discarded it as I haven’t found a way to install widgets programmatically. If this would be possible would have the advantage that no Subclassing or Mocking may be required, as we could use the default WidgetCenter for the Tests. That means, for the production code, we could
init(widgetCenter: WidgetCenter = WidgetCenter.shared())
And for the Unit Tests, we could pass as argument WidgetCenter.ephemeral (if that existed, similarly to URLSession.ephemeral). We would not use .shared() as that may conflict with the currently installed widgets.
2. Subclassing of WidgetCenter
I usually try to avoid Subclassing for the following reasons:
- It can be dangerous when we subclass types we don’t own, as we do make wrong assumptions about its mocked behavior. As a result, some unexpected crashes may occur.
- We should be able to refactor the existing code without breaking the tests. In subclassing, we may assert that we called specific functions before we can test the expected behavior. Consequently, it creates a tight coupling between the production code and the Unit Tests. In general, Unit Tests shouldn’t care about implementation details but asserting an expected behavior.
However, I see the benefit you don’t need to adapt your production code to write the Unit Tests.
3. Protocol-Based Mocking
It is my preferred way when testing 3rd party frameworks, which allow mimicking its interface so that we can spy on it. Furthermore, we removed the problem of assumptions that were present in the Subclassing approach. However, it comes with two downsides. First, as we are stubbing the responses, we can’t know whether we are using the framework correctly in production; thus, bugs may still exist.
Secondly, we introduce a lot of noise in our production code, as the protocols are created solely for testing purposes.
Implementation
For the reasons provided above, I based my Unit Tests in the Protocol-Based Mocking.
I added an Interactor called “WidgetCenterService” responsible for interacting directly with the WidgetCenter and tracking the WidgetFamily returned by it. The Diagram of dependencies was as followed:
As you see, the main problem with this design is that the Interactor knows about Infrastructure Details. It knows about WidgetCenter, and the class WidgetCenterService had to use Generics to used our mimic Protocol “WidgetCenterProtocol.” Consequently, the Infrastructure layer leaked its implementation details in the WidgetCenterService. Furthermore, It also uses the object “WidgetFamily” in WidgetCenterService, which made this service depend on WidgetKit.
For the Protocol-Based Mocking, I also added two protocols that mimicks the behavior I’d like to have. “WidgetCenterProtocol” and “WidgetInfoProtocol”. WidgetCenterProtocol requires an “associatedType” for using WidgetInfoProtocol, that forced to use Generics when declaring the class. We cannot assign this protocol as a type for a property because the compiler shows an error.
Protocol-Based Mocking
Interactor
Initialization
Example of Unit Tests
Learnings
- The Interactor shouldn’t know about infrastructure implementation details. The WidgetFamily appears in the Result. As a result, It ended up depending on WidgetKit.
- As I was too focused on the infrastructure details, my interactor function name also leaked implementation details. What are Configurations? Wouldn’t it be better a more descriptive method, e.g., saveInstalledWidgetSizes?
- The name “WidgetCenterService” sounds generic. If I’m only saving the WidgetSizes, why not call it “WidgetSaver”? Or “WidgetSizeSaver”.
- For Protocol Based-Mocking, you cannot test your assumptions about the framework. If the behavior of the framework changes, your tests will still pass because you are just setting the desired behavior, you want them to have.
- Protocol Based-Mocking created noise in the production code because to write Unit Tests, I added protocols needed solely for testing purposes.
Insights for a refactored implementation
- Consider the naming of what the class and methods of the Interactor do without leaking infrastructure implementation details.
- The infrastructure depends on the Interactor, not the other way around (Dependency Inversion). Create the infrastructure layer outside of the Interactor.
- Understand your client’s primary goal, so you can propose alternatives to reach that goal.
I thank you for reading the article!