Saturday 7 October 2023

Unit Testing in Android

Unit testing is writing code to test a small, isolated unit of application code. Given a certain input, determine the expected return value, and or public state is updated. In Android it's important for general reasons along with increasing the iteration speed of seeing results from code written, without going through long build times, or navigating to specific flows within an application.


Well tested code ensures new code doesn't break old functionality, and that the new code works to requirements. It gives confidence to refactoring in the future, and saves time for teams in bugs found, logged, fixed and retested. It means less bugs and crashes found in production. Done really well it will increase development velocity.


Tests should be quick to write, fast running, repeatable and readable. They should provide more value than the time it took to write and maintain them, and help the developer decide on code structure and flow to maintain simplicity and serviceability. Build your team and code base to the point where they are comfortable not asking for extra time in estimations to add them.


Test parts of the code that give the best return for the time invested. Asserting if a function calls a particular method on a mocked class is mostly useless. Testing complex logic with many control returns correctly for different input edge cases is much more worth the time investment. I don't recommend aiming for a coverage percentage, this encourages useless tests that waste time and effort.


Test for the outcome rather than the implementation. Imagine your class under test as a black box. With known inputs and expected outputs and public state. If the class does not have some kind of input, or public output what does it actually do? If you test implementation it will become a maintenance burden.


Refactoring code should require minimal modification to unit tests, if the requirements remained the same. This makes the test code robust, and require less maintenance. If you're testing the implementation, a refactor might mean rewriting the tests entirely.


Unit tests can help structure the architecture and choose which libraries to use, but I won't expose internal state just for tests, this falls into testing implementation. Something like @VisibleForTesting should be a last resort, and viewed as tech debt to be later addressed.


Tests should be instantly readable and as simple as possible, this makes maintenance is easier. A test failure should give hints. Follow patterns such as "given, when, then", and names that will help determine what is broken. Iterate several times over your important tests at first, and over time speed and simplicity should improve.


Don't test what you don't own. Assume the database library will return the data you've requested. Just test for the correct return values for the given inputs, along with the unhappy paths of any errors or exceptions the classes can return for given situations, such as an API becoming unreachable.


In Android I tend to leave the Framework out of the test as much as possible. I'll go as close to the view layer as testing ViewModels. If you're doing things with Context (like resolving strings, or calling startIntent), I normally wrap that functionality in classes to abstract away that detail to the ViewModel, and make testing easier as well.


Fake or Mock dependencies, the more tests you write the better your judgement on which to use. Each lends itself better to different situations. A good Fake can save time and simplify tests, but a Mock is often quicker and more straight forward. If unsure, begin with using Mock for everything, and then meditate on whether a Fake could improve the harder to read tests.


Some integration tests in between UI and external dependencies can be helpful, if possible. They're harder to pull off, but give a greater indication of the whole feature working together cohesively, and covers testing the boring parts without having to write specific tests for them. Think of it as the last safety net. Don't waste too much time achieving this if the codebase is not cooperative.


If starting a new code base, it is easiest to bring in unit testing and agreed on standards from the start. The architecture and code flow will benefit from a developer considering good unit testing strategies while designing.


If adding tests to an old code base with no tests. Agree on a standard and start small. Get some very simple tests working and running in CI. Then begin with training and humble requirements to test selectively. Make it part of the culture to request one or two useful tests at code review. Some refactoring may be required to make the code more testable. Break this work up into smaller tasks and iterate towards the team's unit testing end goals.


If improving on a code base with a large number of failing or flaky tests, it's alright to commit 'unit test bankruptcy', and disable all the tests that need fixing (they're not providing value at the moment any way), then begin the iterating towards adding new useful, stable tests, and eventually fixing or replacing the flaky and broken.


Lastly, don't let the client or product owner determine if there will be unit testing in the code or not. You don't tell your doctor whether or not they have time to wash their hands. We are the experts in this field, and we will determine what is needed as part of our responsibility to write robust, elegant code for our users.

No comments:

Post a Comment