Nowadays, the learning curve of any programmer goes through a certain number of steps. One of these steps is discovering tests. This discovery is generally progressive and, for many of us, somewhat upside down. Here’s the story of my personal experience with which I think some of you will relate.
A waste of time
It all starts in college : we learn how to code and everything’s fine. We don’t know what tests are beyond manual testing. We have no clue about the different types of tests. Until the day when, in a software engineering class, we are told that they exist, and that they’re a widespread practice.
The first thought that pops in our head is : “But that’s a waste of time, I know my application is going to work, I wrote the code myself!” We launch the application, we make a quick manual test and it works, so why additional testing? Furthermore, our homework is due for the next day and we are still far from being done, so it’s not the time to add more work by writing tests…
And this mentality continues. We take small contracts, and since we are alone working on the code and the projects are generally rather small, we tell ourselves that testing is a waste of time. We complete our degree and begin a Master. We work on an open project that is much bigger than anything we have worked on until now, but there is still no testing. The idea of useless tests is still very present in our mind.
The Master is about done and we get on the job market. We are recruited by a big company that has been making software for over 35 years. We tell ourselves that they must know what they’re doing. Even there, tests are absent, reinforcing even further our impression that they are useless. We deliver quickly and our boss is happy.
It gets complicated…
Obviously, it brings non-testable and very coupled code, but we don’t know it yet. As time goes by, it gets increasingly complicated to add new features to the application. We don’t really understand why, it was easy before. At this point, there are generally two explanations:
- Previously, our project manager was only giving us easy tasks so not to discourage us while we learned.
- The project has become a bunch of “spaghetti” code that can no longer be maintained.
We also find ourselves spending more and more time correcting bugs instead of adding features. And most of the bugs we fix were actually caused by us. That’s when we remember that tests exist… We need to fix the bugs that we have introduced during the year and we ask ourselves what has gone through our head for us to write something so bad.
The day following a big “Commit”, we come back to the office to realize that several emails are waiting in our inbox to tell us that we broke last night’s compilation with our Commit. We immediately start panicking. Fortunately, the fix is not too complicated, but we are summoned to the boss’ office because we slowed everyone down by delaying the compilation.
Thereafter, every time we have a big Commit to pass, we are increasingly stressed out because we don’t want to have to go back to the boss’ office. The house compilation tools don’t detect all the dependencies in the code and we realize that we can’t count on it, increasing our fear further.
At this point, the company has more than 350 employees of which half are developers. How do we spread the message that we should add tests in the application when the big bosses above us are recruiting more staff for Quality Assurance? What boss with 30 years of experience will want to listen to the new guy and add tests to the app? Especially since this app has almost one million lines of code. There is nothing to be done and we start being depressed. Our productivity decreases because there is now a list of twelve compilation steps to perform before being able to pass a Commit.
The Fresh Start
Comes a day when we can’t stand this routine anymore and we find a new job. A small business with a dozen employees doing contracts for different enterprises. The fresh start is magical! We learn plenty of new technologies and innovative ways to work. And the best : tests are omnipresent in the projects. We finally understand their use and we want to write a ton.
We write code, we test it manually and we write a test to make sure that nobody else will break it. Thereby, our code is much less coupled and easier to reuse. Our code coverage is quasi universal. Everything is going well!
A New Obstacle
A little later, we have to modify a small part of our code to add a new feature. The code is simple, uncoupled, so the change is easy. We do the modification, we test manually to make sure that it works and we write new tests for the mod. We relaunch all the tests of the solution and… it doesn’t work. We look at the tests that broke : they are the ones we wrote at the same time as the code.
Why are the tests now broken? The reason is simple : the tests are directly linked to the code. They test each class, function, loop, condition, individually. So, if we change the code, without changing the logic, the tests break. So we modify the tests so they pass, we remove those that are no longer necessary, we add new ones and we Commit it all.
But we quickly realize that it keeps taking us more and more time to add new features. Even though we conduct tests and make sure that the code is as clean as possible. It should not, therefore, take more time. Our code might be really clean, but we quickly understand that the unit testing code is not really clean and that it slows us down a lot, especially when we need to modify the tests each time we add a feature.
Furthermore, doing any refactoring breaks the tests, even though the logic should not have changed. So we don’t want to do any refactoring because it breaks the tests and it’s long to correct them. In the long run, we end up with code that is less and less clean, as we had before adding tests.
Comes the day we learn about Test-driven development. This practice seems strange and must be coming from a crazed mind who could not bear testing anymore. However, we only hear good things about it so we start studying it.
Writing a test before the code itself has been written! In our mind, it does not seem to work but, after a few videos, books and personal tries, we start seeing its usefulness. Suddenly, our tests are no longer linked to the implementation. Refactoring does not break the tests anymore, unless we made a mistake doing it. It looks like a gift from heaven!
Thereby, our mentality also changes. We now want to test the business logic instead of the code’s logic. It also reflects on when we write the tests after writing the code : our additional tests look for other possible cases, or borderline cases, that may already be resolved by our code, but without necessarily reflecting the code’s implementation. Their purpose is to further strengthen the business logic in the code and make sure that it won’t break.
The Business Need
We also manage to write tests with larger and larger coverage, without it being complicated. So it allows us to conduct less and less manual testing. The coverage we have may not be 100% failproof, but it is much more useful when we have to modify the code.
It also becomes easier to write tests that run through several layers of the application or that are able to deal with test cases that are much more complex. So we can very simply write integration tests without changing our way of thinking, which seemed like a monumental task before.
We try to add tests everywhere in the app, including in the sections we judged non-testable like the user interface, and we realize it’s easier than we initially thought. The implementation is more and more neglected in favour of the business need, to the point where we see the test cases appear directly in the stories before having even thought about the work that will be required to add the feature.
Our learning curve continues. We attend training and we see the trainer use this principle to correct an application. It looks so simple when he takes the keyboard and the code is placed in the right spot like magic…
For me, behind this whole story hides an essential lesson : it’s important not to put aside practices that you think useless when you are still new to the field. Some practices we use in software development are complex and can require years before being mastered, but it’s often these that end up being the most useful and rewarding.