How do you know if you wrote readable and maintainable code?

Dissecting the question, we have two operative terms here: “readable” and “maintainable.”

Let’s talk about what these mean.

“Readable” would describe code that is understood without much inspection or explanation by another developer. You have to choose what parameters you want to describe the “other developer” to know what would be readable to them.

Some things are somewhat universal and limited by human factors. For example, few people can follow poorly named variables. (There’s your first heuristic – are there clear names for variables, classes, methods, and other references?)

Other things are a bit more nuanced. For example, if the developer uses the language you are writing in on a regular basis.

Or, is the developer familiar with the domain the project operates within? How experienced are they as a developer? Do they have a particular background that might make the code more or less readable to them?

But what if you don’t know who the other developer is?

This is why we develop standards, patterns, and best practices. For example, JavaScript code tends to use camelCase, so writing your code with camelCase provides a sense of fluidity (which plays into readability). Knowing the common patterns and style that the language typically uses is important. (As an additional note, your organization may have your own style defined; follow it.)

Some simple, practical heuristics to follow:

  • Use descriptive variable names. Longer variables are more easily read.
  • Use whitespace! Compilers and minifiers mean that whitespace is free. Take advantage of this.
  • Practice finding a balance between abstraction and practicality. You do not need 10 layers of redirection for simple tasks; start with the simplest approach and abstract during a refactoring process.
  • “Make it work, make it right, make it fast.” In that order. This will immensely help the readability of your code, because you start first from comprehension, and then move towards performance. This establishes your pattern and semantics up front, and you are more likely to maintain good semantics this way.
  • Know your audience. If your audience isn’t used to inline lambda calculations, skip it. Even if you believe it is the “best way” to solve the problem, there are other equally good ways most likely available.
  • Follow well-established refactoring and OO patterns. Seriously. These things have been tried and tested, and they work. Look here for a good description of many of these patterns: sourcemaking.com/design_patterns
  • Don’t follow rules blindly. Take some time away from the code and revisit it – what sticks out as weird? What sticks out as confusing? When does your brain feel tired?
  • Readable code isn’t always maintainable, and vice versa. Maintainable code is established by following good practices and principles.
  • Tests are HUGELY important to codebase maintenance. Having good test coverage keeps you from having to load all of the codebases into your working memory (in your head), and reduces errors. (Note: tests won’t eliminate errors altogether; they are there to help you, not to make you obsolete.)

All-in-all, coding is a human process. Follow Hemingway’s advice when writing code. Simpler is typically better.

Top 8 Signs Your Writing Bad Unit Tests

If you have been developing software for a long time, then you can easily relate to the importance of unit testing.  Experts say that most bugs can be captured in the unit testing phase itself, which eventually gets passed on to quality teams.  Here is a list of my top 8 signs your writing bad unit tests.

1) Test passes but not testing the actual feature

Believe me, I have seen test cases in some projects that appear to be doing lots of work in the code but in reality, they were doing nothing. For example – a test that’s sending requests to the server and no matter what the server responds with, the test was passing.

Beware of these such test cases taking place in your code repository by doing strict code reviews. Once they make their way in your code base they will become a liability on you to carry them, build them, and running them every time but without adding any value.

2) Testing irrelevant things

I have seen developers checking multiple irrelevant things so that the code passes with doing [something], well not necessarily the correct thing. The best approach is to follow the single responsibility principle, which says, one unit test case should test only one thing and that’s all.

3) Testing multiple things in assertions

In the previous sign, the test was testing irrelevant things. For this sign the unit-test is testing the correct items,  however, there are too many items in one test case. This again is a violation of the single responsibility principle.

Well, please note that I am not encouraging the use of a single assertion per test case. To test a single entity, you might need to use multiple assertions, do it as needed.

For example, one API which takes some parameters in a POST body then creates the Employee object and return it in response. This Employee object can have multiple fields like first name, last name, address etc. Writing a test case to validate only first-name, then another for last-name and another for the address is duplicity of code, without any value. Don’t do it!

Instead, write a single positive test case for creating the Employee object and validate all of the fields in that one unit test. Negative test cases should be written separately doing only one thing and one assertion in this case. e.g. One test case for blank first name, one test case for invalid first name and so on. All such negative test cases can be validate using a single assertion which expect an exception in response.

4) Test accessing the testee using reflexion

This one is real bad. Trying to change the testee to suit ones need.  The test will blow up when the testee may refactor some of the code.  Do not use this or allow to be used either.

5) Tests swalloing exceptions

I have seen my fair share of such test cases. They silently swallow the exception in little catch block at the end of the test case. And worse is that they do not raise a failed alarm. Exceptions are signals that your applications are trying to communicate that something bad has happened and you must investigate it. You should not allow your test cases to ignore these signals.

Whenever you see an unexpected exception, fail the test case without failure.

6) Test which depends on some excessive setup

I do not like test cases that require a number of things to happen before they start testing the actual thing. Such a scenario could be an application which facilitates online meetings. Now to test whether a user of a particular type, can join the meeting, below are the steps test is performing:

  • Create the User
  • Set user permissions
  • Create the meeting
  • Set meeting properties
  • Publish meeting joining information
  • [Test] User join the meeting
  • Pass/Fail

Now in the above scenario, there are 5 complex steps which must be setup before actual logic is verified. For me, this is not a good test case.

A correct test system will have an existing user present in the system for this activity, at least. This will reduce at least two steps in the test case. If appropriate, you could have a few already created meetings to make this test case really focused.

Another way to make it correct is by using mock objects. They are there for this very purpose.

7) Test compatible to only developer machine

This is not widely seen but sometimes visible when written by freshers. They use system dependent file paths, environment variables or properties in place of using common properties/paths.

8) Tests filling log files

This never seems to be an issue until you need to perform a deep debug then make life hell for debugger who is trying to find something hidden in those log files bloated with wasteful messages.

Tests are not for debugging the application, so do not put debug statements in your logs. A single log statement for Pass or Fail is enough. Believe me.

 

These are my thoughts based on my experiences. I do not expect you to agree with me on all of the above points or you might have some other opinion which is perfectly cool too.