Why devs don't test before shipping?
I was starting my software developer journey. I knew how to write code. But I knew almost nothing about developing software. I was finishing my master’s degree in Computer Science and started my first job as Java developer. After 2 weeks of introductory period, I got my first project and task. I setup environment. I cloned project and started working. There was only 1 problem. My code didn’t run. I got compilation errors. I was new. I pushed hard. I didn’t want to ask for help on my first day. How is that I cannot simply run code? What am I missing? After a day of struggling, I finally asked for help…
It turn out it was common. Code wasn’t compiling. Devs pushed code to stable branch even though it was not compiling. 🤯 (For the record. In Java, you need to compile first and run second. If compilation fails, you’ll see nothing.)
There were projects where only single person was capable of running system…
My mind exploded.
That was about 15 years ago. A lot has changed. Or at least I hope so…
It’s done but not working
I haven’t seen broken main
branch for years. I usually work on stable main
branch pattern. We’re releasing from main
to production. It means it always works. It always “compiles” whatever compilation means. You cannot merge to main
if you won’t pass build & quality gates.
It got better. Branches were stable. That being said, I’ve seen many cases when code is not working as expected. I’m not talking about bugs. Feature is “done” but it doesn’t work.
My question is why people don’t test code before shipping? Why people don’t test code before requesting PR review?
I bet you had a case when you pushed a commit without testing. I’ve done it as well. There are exceptions. You need to change copy, change colour of the text or remove an element from the page. Some of those one-liners are so simple that we just ship it. For those the risk of failing is minimal. With good automation in place, rollback is a breeze.
Now think about commit with 2-3 hundred lines. Adding item to the cart, loading list of products, or starting group chat. Complete feature. And it’s not working. Hundreds lines of code that are not executed. I’ve investigated those commits. They were never working. How could it be?
I was thinking about that recently. As I was implementing debounce in logs search (Why ChatGPT (+ many tutorials) are wrong about React & debounce), I found that old code is not working. Other time I found lazy loading never hitting backend. They didn’t throw error. They weren’t working.
Devs are lazy…
I’ve asked friends why we don’t test code before shipping.
Everyone said: “Devs(we) are lazy”.
What if that’s not the whole story? What if lack of testing is not the result of laziness? What if there’s something deeper? What if that’s more an effect than a cause?
I’ve experienced incomplete/not working/missing features so many times that it has to be more than laziness. I’ve come across strange bugs created by experienced & solid devs times and times again. I know them. They are not lazy.
We think awful code is written by awful devs. But in reality, it’s written by reasonable devs in awful circumstances - Sarah Mei
Let’s go that route. Let’s put aside laziness. Let’s look for other reasons.
I love inversion principle. It’s mental model popularised by Charlie Munger. The idea is simple. Invert the problem. Brain is great at finding arguments to satisfy hypothesis. In that case, I asked myself: How to setup project so that it’s impossible to test code manually?.
- You cannot setup whole environment locally. The only way to test application is to use staging environment that’s shared by whole company.
- All services are required to run application. Application consists of many services. To test single piece, you need to run all services. Otherwise, application crashes.
- Application runs only on specific hardware. You don’t use Docker nor other containers. Like everyone runs ARM, but system requires X86 architecture.
- There’s no knowledge sharing about how to run system locally. There’s single developer who knows how to run things. But there’s never good time to prepare docs.
- It takes several dozen minutes to run system locally. True story. I was working on the project, where running system took ~25 minutes 🙈
- There’s no staging env. There’s only prod. So if you want to see whole picture, the only way is to deploy to prod & check.
- Developers are NOT responsible for testing. There’s separate QA team who tests application. Developers are only writing code. So even if application is not working as expected, there’s someone else to take care of those & create ticket.
- You are not allowed to write tests. You should focus on writing production code. Everything else is waste of time.
- Whole team needs to approve Pull Request. You’re committing only once a week, maybe even once a month. Your PRs are huge.
- Your boss believes that time pressure is the best way to motivate people. You’re always behind a schedule. There’s constant time pressure to ship.
- Product Owner changes task requirements several times during sprint. If requirements are always changing, you don’t know what’s the correct way to implement feature. You often have several different versions at the same time.
- There’s no hot reload. Every change requires restarting whole build.
- You don’t have access to real world environment. Imagine. You’re building iPhone application and you cannot check application on real device. Or you need to support IE, but you only can check Chrome version.
- Slow CI. It takes 5 minutes to fix bug. But setting env & testing use case take 1 hour. If that’s the case, don’t even wish that devs will test every change.
Now we’ve got more or less “perfect” environment. Look at the list above once again and honestly answer question: How the heck can you test anything? It’s no longer laziness that’s blocking you. It’s conditions (and culture).
Of course, you’re alse responsible for those conditions. So brace yourself. I believe in software craftsmanship. As software engineers, we are responsible for whole technology stack.
Strive for perfect conditions 🤘
Now it’s time for inversion principle. Let’s prepare perfect setup, so that testing is trivial. If testing is trivial & fast, it won’t be an obstacle in development workflow.
- Run application locally with single command. Running application has to be easy. It depends on your technology stack, but there’s a way. Be it package.json script, Makefile, Gradle task, Dockerfile. IMO everyone needs the same workflow - single command workflow. If you add any dependencies, change any step, or modify params, it has to be updated. And if everyone uses the same script, it’s certain that it will be up-to-date.
- Ensure that your application is running fast. Compiling project is not the time to grab coffee. Running application has to be so fast, that it doesn’t destroy your flow state.
- Ensure hot reload during development. It’s critical to be able to constantly run your application while working. It’s a nightmare to restart application after each & every fix to see changes. It’s also connected with flow state. If you’re in the zone, don’t let
- Prepare good documentation for setting up & running project. That’s a must. I’m amazed how many projects lack any document. As a developers, we don’t like to repeat the same things over & over again. Think how much time does it take to explain to every new person how to run application.
- Setup testing scripts. Again, make it single command.
- Setup formatting tools. Most modern languages have great formatting tools. Sometimes they are already build-in like Golang formatting. Sometimes you need something external like Prettier. Still, you need one for your project. That’s nothing worse than inconsistent code.
- Setup git hooks. Run your formatters/checks/tests before committing. Automate that process. You don’t want to do any manual steps to ensure everything is in good shape.
- Setup CI & make it fast. If your tools are slow, then no one is going to use them.
- Setup staging dev, staging & prod envs. Prod is obvious. If your product is live, you’ll have one. Staging is common. But there are many flavours. Some use staging as a canary build, others use staging as a rotating env, where all things are setup. Dev env is nice to have. It’s great to be able to setup complete application in same as prod env.
There are few things not related to “code” side.
- Things change, but don’t work on half-baked ideas. Take time to define feature you want to implement. If there’s Product Owner, it’s their job to handle that. If you’re the one, take time to think. It doesn’t help to work on inspiration and change direction multiple times in single sprint.
- Remember about DOGFOODING. You need to use products you build. That’s single best thing to constantly improve.
List is never complete. If you have anything to add, please share with me.
Right setup is not easy. It requires both time and skills. The good thing is that if you experienced well-structure project, you feel the difference. And it’s much simpler if you already experienced how easy it’s to develop in such an environment.
What’s next?
Imagine you’re joining a new project. You feel that thrill of excitement. Hair on arms begins to stand up. You’re going to enter new world. There’s a lot to learn. Get beginner mindset. Think about perfect condition to onboard to project. How would you like that to look like? Write it down.
Here are some questions that can help:
- How to run everything locally?
- What are common questions when someone wants to run project?
- What automations are in place?
- What’s the process to ship new feature?
- How are you implementing new feature from the beginning till code is shipped?
- Which pieces could you improve?
- Do you have DoD (Definition of Done) written down?
Now move to your current project. Make small improvements. Setup things like you’d like to see. Think about next person joining project.