TDD for an Android ViewModel
Learn how to divide your code to have each part its responsibility, and have a tested and maintainable code. We'll in this guide demo how to TDD an Android ViewModel.
Introduction
I’m going to show in this article how I build step-by-step an android view model using TDD approach.
The feature I’ll cover is a simple screen with a canvas where user can sign and save his signature.
Design is something like this :
This is the functional requirements we’ll develop :
User may save a signature drawn in canvas.
If a file exists for the signature then a non-modifiable image should show the file content.
If there is no file for signature then an empty canvas should be shown.
User wants to revert a saved signature by clicking the clear canvas button. It then hides the non-modifiable image and shows the canvas.
If the signature is not saved, then back should not be allowed and a dialog explaining there are modifications not saved should appear.
On another side, if signature is saved then back is allowed.
And we’ll try to follow the TDD guidelines :
You are not allowed to write any production code unless it is to make a failing unit test pass.
You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
Req1. Existing files should show non-modifiable image
Let’s start covering ‘existing files should show non-modifiable image’ requirement.
I do not want to not push production code first but test first.
I create a test with the future classes and calls I want.
Here I wrote a lot of things that do not exist for now.
We see we implement a GetSignatures interface
Signatures class
We use a SignatureViewModel with a getSignature parameter
and a SignatureContract with its sealed class State
GetSignatures interface is an abstraction. I do not care about file system here, I just want an abstraction for getting signature path files.
I create a data class representing a Signature, it’s just a String wrapper (the file path if it exists)
I create the minimum view model code to make the tests compile.
I am using BaseViewModel that you can find here, I quote Catalin who explains why I’m using this implementation :
Handles the state and exposes it to the composable as a Compose runtime State object. It is also capable of receiving an initial state and to mutate it at any time. Any update of its value will trigger a recomposition of the widget tree that uses it.
Intercepts events and subscribes to them in order to react and handle them appropriately.
Is capable of creating side-effects and exposes them back to the composable.
Next I create a Contract for my view model. It is composed of what is the view state, what are the events user can do and what are side effects UI should react.
It compiles now if all dependencies are imported in the test.
If I run the test without any modification I expect to have an error.
For fixing this I’m doing minimal change in the production code to make the test pass.
Now that test passes we check if there’s something to refactor. There’s nothing to refactor in the production code but in test we can make some improvements. Commit d2cbaaecf11efdeb9f049432f282ebf8a01fe390
I make a function for creating a GetSignature instance
And it makes the code cleaner
We move to the next requirement.
Req2. No signature should show an empty canvas
For passing all tests with minimal change in production code I do this in view model
It allows me to have initial state based on what getSignature parameter returns.
We have nothing to refactor then we move to the next requirement.
Req3. User wants to revert a saved signature by clicking the clear canvas button. It then hides the non-modifiable image and shows a canvas.
Here for doing that I’ll put the string property to null. It will only stay on view model scope and will not be saved. So as the state will change, UI will reflect this and we’ll be able to show the canvas.
I am creating a ClearCanvas event and I want the view model handles it and set path to null.
Once again I did not create production code before, it’s my test who forces me to create production code. As you see ClearCanvas does not exist for now.
I simply create it in contract class. Now all test compile and I expect to have an error in the last created test.
I go in the handle events function and add all known cases with ‘add remaining branches’ feature.
It passes and there is nothing to refactor, let’s move on.
Req4. If signature is not saved, then back should not be allowed and a dialog explaining modification is not saved should appear.
What I mean by signature is not saved is simply that user clicked on the clear canvas button and signature string is null.
I create the test first.
I need two things here : a RequestBack event and a ShowDialogQuitWithoutSave side effect.
I have as expected an error
I simply add this line before else
All tests pass
Req5. If signature is already saved then back is allowed
What I mean by signature is already saved is simply that user did not click on the clear canvas button and there is already something displayed in image. Technically string is not null.
Here I introduce a Back side effect
But I have an error in assert call because I launch a RequestBack and according to previous test it always launch ShowDialogQuitWithoutSave effect.
All I have to do is update RequestBack branch in handleEvents method.
This works well but I can refactor the code so I can’t continue for now.
I prefer to split the logic for retrieving an effect based on state and events on one side and on another side launching an effect.
I re-launch all the tests and refactoring worked well. I can continue. And so on.
User may save a signature drawn in canvas. It should do back on save
We introduce an addSignature collaborator and a Save event. Once again, I just want to want be sure that view model call add Signature collaborator. I don’t want to test if it appears in file system. It is the addSignature implementation responsibility and it is out of scope of this article.
In view model test, I added this helper function :
I need to change all the view model instanciations since I introduced a new parameter. I relaunch all tests, all except the new one pass.
This is what I expected since I have in handleEvents function :
To make it pass I need to change the save branch in handleEvents method :
Then, as every tests pass I check if there is something to refactor and I find that I can wrap all view models parameter in one class for avoiding too much parameters.
I relaunch all tests and check if refactoring didn’t break anything. Alright, everything is ok. so I can continue. And so on. Here is the way how I build incrementally view models.
Conclusion
We’ve seen how to build an android view model using TDD approach.
With practice, you will know how to divide your code to have each part its responsibility, and have a tested and maintainable code.
We know that with this code, we just need to plug our composable functions, just observe states and effects and it will work with an important code coverage.