How to Test a Private Method Inside a Widget
It is recommended that we should not test private methods. However, there are several reasons why we might want to test a private method.
First things first, always try to test private methods via public methods. We keep methods private for encapsulation, and private methods are NOT designed for consumers of the API. So, testing those private methods might lead to some unexpected behaviour.
However, that being said, there are several reasons why you may want to test the private method.
Working with Legacy Code: Our code does not have any tests, and we want to start small by writing tests for a method. Sometimes, these methods are private.
Bug Fixes: When fixing a bug inside a method, it may be necessary to test only that specific method, rather than the entire widget, and that method is private.
Widget Dependencies: Many classes are required to run a widget in testing, such as calls to APIs, databases, or platform plugins. Sometimes, it's easier to test just a private method.
Method cannot be made public due to limitations in the API design or the risk of misuse.
Pure Function: A function with no side effects that takes input as an argument and returns a value as output. Even if this is not the case, you can refactor an existing method into a pure function and test only that function.
If you fall under any of these categories mentioned above, then continue reading.
In the case of Flutter, this usually happens when we have a private method inside a widget. It may seem difficult, but the fix is actually simple.
Create Forwarder Method
For example, let's say we have a private method _buildMessage()
inside a ChatMessage
widget :
Now, the ComplexTextWidget
has several internal dependencies that we do not want to mock. We only want to test the result of _buildMessage()
.
To fix this issue, we can add a test forwarder method like this:
Adding the @visibleForTesting
annotation ensures that the code is only used for testing purposes. It can display a lint error if used in production code, which helps us prevent misuse.
Testing in StatelessWidget
To test a method inside a StatelessWidget
, we simply need to create an instance in the test like this:
Note that this is a unit test and not a widget test, i.e., this technique can be used on any Dart class or even in any other object-oriented language. In object-oriented programming, we refer to this as the delegation method.
Remember the steps:
Copy and paste the private method you want to test.
Add a
test
prefix to the new function to make it clear that it is only for testing.Forward the arguments to the private method and return the value from the private method.
Add the
@VisibleForTesting
annotation for lint checking.Use that method in the test.
Testing in StatefulWidget
Testing a private method in StatefulWidget
is tricky because it contains a private _ChatMessageState
class that cannot be accessed from a test. For example:
There are multiple ways to fix this.
First, create a double forwarder from ChatMessage
to _ChatMessageState
, which I don't recommend because it creates unnecessary complexity.
Second, move the private method up to the ChatMessage
class with a forwarder:
And the third is to simply move the private and forwarder functions as top-level functions:
Bonus
This method delegation/forwarder approach can be used in multiple variations.
Hidden Forwarder
When we don't want to expose some functionality outside of the package, but we still want to allow clients to use it. Check this.
Finding Seam
Seam involves identifying points in the code where you can make changes safely without affecting the behaviour of the entire system. It's like finding a seam in a T-shirt.
After finding the seam, we can then replace the original function call with the new function call, effectively "sewing" the new code into the old code. If you want to see this example in action, you can watch my video on YouTube.
Conclusion
Software engineering is all about understanding tradeoffs.
So, before you choose an approach, ask yourself: "Is this complexity worth it?"
If I am doing a hot bug fix in production, then yes, because this approach helps me to ship faster with a safety net. But if I am building a feature, then I should take some time to refactor it and test it via public method or widget test.
That’s it for now.
If you have any questions, please let me know in the comment section or you can join my subscriber chat.
If you like this post, please help spread the word in your network. Thank you so much!