Achieving Quality by Prevention

Pavel Studený
Outreach Prague
Published in
5 min readFeb 20, 2024

In my previous article, I mentioned a few ways to detect product quality issues by measuring its behavior in production. In this blogpost, let’s have a look at how to release a high quality product so that your detecting mechanisms keep being green.

The prevention category focuses on delivering high quality code that integrates well with other parts of the system. The most notable techniques are toolset & development environment, pair programming & code reviews, test automation and manual testing. This article covers them one by one, the way we do them at Outreach.

Toolset & Development Environment

When you open your computer in the morning, you want to keep your attention on the code, without distractions or delays. When you modify existing code, you will appreciate conciseness and readability. How do you achieve all of this?

Language

You can start with the choice of programming language. Your IDE can give you better hints if you use a typed language. When working with unfamiliar parts of your code base, you get a better and faster overview of function parameters, structure members etc. This was one of the main reasons why so many people switch from JavaScript to TypeScript. We have switched in Outreach for our frontend projects as well a few years ago.

There are less surprises if there is a fairly limited number of ways to express your algorithm. That’s why Kotlin is more popular than Scala.

Frameworks

Have you ever had to keep scrolling up and down looking at a function to understand what it does? It’s recommended that a function fits into one screen. There are frameworks, such as Spring in the JVM world, that shrink most of your methods to 4 or 5 lines and keep them very readable, although, on the other hand, mastering Spring isn’t easy and quick.

Remove Delays

You also want your language to build fast. It’s really annoying and disruptive to wait 10 minutes for a build, only to find out that you forgot to write a semicolon. We use the Go language at Outreach backend, which has one of the fastest builds. Luckily enough, modern IDEs and build tools provide real time compilation, hot module reload and can also run your unit tests, lint rules etc. on the fly.

Pair Programming & Code Reviews

Stats on Outreach largest repository show that code reviews found a bug in about 10% of the pull requests. All the pull requests must have unit tests and must be small enough to be reviewable and despite that, 10% of them have to be fixed before they can be merged. And I’m not sure about your company, but I believe that 10% is a pretty good number.

What doesn’t happen that often is that an important factor is missed and the entire pull request needs to be thrown away and started from scratch again, but I have seen this happening occasionally to new members joining the team or starting to work in a new area. In such cases, pair programming is a much better option, because it removes the throw-away work and improves learning. A good idea is to go like this: The less experienced developer writes the code and the more experienced developer navigates and gives instructions.

Automated & Manual Testing

As mentioned, it’s often a good habit for a pull request to contain unit tests. When a task is clearly defined and contains enough logic, you can even write a test first, implement a minimum part of the code so that the test passes and then you write the next test. This is so called test driven development (TDD). On the other hand, with an unclear task, when you need to explore multiple options, writing tests too early makes it harder to iterate fast. However, it is more likely that you would refactor such an unclear task eventually and that’s why you want to cover the initial code by unit tests that make the refactoring safe.

describe('isExternalMessageSuccessPayload', () => {
it('returns true for success payload', () => {
expect(
isExternalMessageSuccessPayload({
user: '',
loginVersion: '2',
})
).toBe(true);
});

it.each`
payload
${null}
${undefined}
${{}}
${{ text: 'something' }}
${1234}
`('returns false for invalid payload', ({ payload }) => {
expect(isExternalMessageSuccessPayload(payload)).toBe(false);
});
});

In addition to unit tests, you should have at least some integration or end to end tests. From Outreach and other companies’ experience, these are costly to maintain and prone to random failures. Whether using Selenium or Cypress, we have always had to automatically rerun the failed tests multiple times and only then report a failure. That’s why these tests should only cover the most critical scenarios.

After finishing a feature, we always run a bug bash before releasing the feature to our customers. I have not seen a bug bash where we would not find a bug. Maybe once. Usually, on a feature that takes a few weeks to implement, there are 10 or 15 bugs found on the bug bash. When possible, multiple departments and professions join — designers, product managers, developers, testers, technical support.

After fixing the bugs, we can release the high quality code covered by test automation, with telemetry in place, safely and confidently. Then we can go have a beer. My team loves having a beer together. Cheers!

--

--