I refactored the reactjs implementation of todoMVC. And when I was done I realized that going through the process made me learn a lot especially about testing. Let me lay out where integration tests and manual tests played their roles in this process.
When preparing for a munichjs talk I gave last week about reactjs (and testing) I tried to find a common task that hopefully everyone had at least heard about before. TodoMVC seemed perfect, which is basically one specification and many implementations in all kinds of different web frameworks, made to compare them.
My plan was to use the commonly known todoMVC project to transfer all the learnings from a bigger reactjs project that we worked on the first half of 2014. Choosing todoMVC I thought would make it easier to do so. But it needed some refactoring to apply the learnings we had made in that big project. In the end I think it worked out quite well.
I am the testing-guy. Maybe a bit extreme at times, but it gives me a good feeling. And since I know I don’t know much yet, I like the excessive path at times to learn what the middle way might be.
Approaching the todoMVC app, I looked for the tests first. I didn’t find any. Alright, that means I will have to make the effort to ensure that my refactorings will keep the app working as expected. My refactorings are serving a very special purpose. I want to show that react doesn’t require to deal with state for implementing business logic. It pushes you to a clean separation of concerns especially between data and UI. And almost for free comes a good testability.
Bottom line is to prove the claim „reactjs – DOM as it was intended“, as I like to phrase it.
Refactoring, 3, 2, 1, go
I know where I want the code to head. The refactoring was a mix of comprehension refactoring and application of the boy scout rule.
As you can see in my commit history, I like to take tiny steps (baby steps) and almost every time I make a too big step I undo later anyways.
Refactoring is one thing, but knowing that I didn’t break anything is another, and that is where we normally apply testing. In a well-tested application we can use some kind of coverage tool to tell us how well the app is covered and then we know to which degree we can rely on the existing tests or if we have to write some more.
In an completely untested app, as it was the case here, we have to decide how to approach a verifiable app. My first thought was, I write some integration tests which click through the UI and I verify some results. I would use selenium or an alike tool for it and I might be fine. Thinking that through it seemed too much effort, since I didn’t plan to invest more than a day in this, so I didn’t. Though two days later I thought of using a golden master comparison of the complete web site content after every interaction, that may would have been a much faster approach, but I didn’t think of it at this point.
There was another thing that actually made me hesitate to write some automated integration tests at that point. It was my lack of knowledge of what the todoMVC app really does. Yeah, ok it does some todo list handling, but which features exactly I didn’t know. So I decided to go for a manual testing approach. I can hear some people scream: „What, no automated tests?“ and I say „No“. And I tell you why.
Actually I am a big fan and strong believer in automated tests, it’s the only way you will get reliable and reproducible results. And I know that from experience. It was painful when we did only manual testing. Actually this painful experience led us at uxebu to become strong believers and learners in the testing culture.
It is very tempting to do the first couple of refactorings only supported by manual tests. There is no test setup time, you feel to become productive very quickly. And sticking to small refactorings gives a sense of security.
And even though I didn’t feel comfortable with this in the beginning, I learned about the value throughout the process. So I started out just doing manual tests. In the beginning there was this voice in my head „automate the tests!“ which I didn’t listen to. In the first place I didn’t listen to it because I thought my refactoring might just be small things, and won’t take more than one or two hours (which turned out to be wrong). Pretty quickly I realized that my manual tests do not only serve the purpose of verifying that the app works. I learned a lot about the app and the features it provides. As I said in the beginning, I didn’t read the todoMVC spec, and tbh I also didn’t plan to do so. I love exploring apps by refactoring them (I successfully did that the first time I refactored the Tennis Kata and am hooked since).
I started to create kind of a rhythm of what to test after every refactoring step. In combination with the deeper learnings about the source code I adapted this test list. I found out that I can also manipulate the todo-items even when a filter is applied, so I added that to my (manual) test list.
After a couple of hours in and quite a number of refactoring steps, I realized that my test list is stabilizing and I felt it was the right time to create automated tests. And than I cheated. At this time I stopped refactoring, since I got pretty close to where I needed the code to be for my talk. Until here I actually just moved around UI logic code. I only touched a small business logic part. Remember my main purpose here was to show a certain aspect of reactjs, I got there.
Explore, stabilize, commoditize
I thought I would feel pretty bad about the result of my pure manual testing approach, but to be honest I don’t. After Dan North implanted „explore, stabilize, commoditize“ into my brain last year I feel like this applies in more places than expected. So it does here too. I optimized for discovery first. I tried to discover all the features of this app, I „explored“. By repeating this process often I got to a stabilized set of tests that now need to be commoditized, which means automated in this case.
Starting the automation of the tests just now, does also give me a way better feeling of what tests I should write. What groups of test sets I create, what features are important and where do I might need to put less focus. I basically learned about the app by using (and refactoring) it. From time to time I also opened the original todoMVC app (without the refactorings) to compare that I didn’t break a certain thing.
This was the perfect app to do this kind of explorative testing. I believe that we should take the time to manual test before most of our refactoring work, especially when the apps have UIs. And this manual tests should serve as the base for what integration tests to write for legacy code, it shall not be the excuse to not automate the tests. I gained much more confidence in the feature set of the todoMVC app, I even learned about edge cases that I hadn’t thought would exist. I can perfectly write tests for the app now. I feel I can give estimated guesses about when an integration test can be deleted because it’s feature set has been validated by the according unit test. Having only the necessary set of integration tests left is our final goal anyways! Right?
Integration tests and false believe
One issue still stands out for me. I experience a lot when working with legacy apps. It is how and when to question the existing set of tests. For one you don’t want to miss a test that might lead to breaking part of the app and two you also don’t want to spend endless amount of time writing new tests that might not add real value. (I guess I should finally read Michael Feathers’ book).
So, even when a good set of (integration) tests exists don’t let the false sense of security pull you onto the dark side. Do always question your set of tests, verify the requirements, environments the app runs in and expectations of the customers as soon as you can. You will not only learn more about the app but your safety net and the strategies how to expand it will improve. Testing is one of those endless learnings that a software craftsman should be doing.
Keep it up and stay flexible!