I have talked to many engineers during job interviews and casual conversations, and I always hear things like: “yes, we lack tests in our code base… this is something we should address at some point… we just haven’t had enough time…”.

In my opinion, tests shouldn’t be seen as something you could leave for later, as some kind of complementary activity. Tests are an essential part of every engineer’s mental process while developing software, even though many are unaware of this. Tests are intrinsic to the development process and not a separate task that should be done before or after. You test at the same time as you develop.

When you pick up a piece of paper and write some use cases to understand the problem better, you are testing. When you put a breakpoint to check if some variable has some expected value, you are testing. When you print something to the console to check if some method is being called, you are testing. This mental process must be externalized, documented, and automated so we can reproduce it over and over as our software evolves.

To illustrate this idea, let’s use an example. Suppose you have to write a component to validate variable names using a regex expression. A variable name must start with a letter or underscore and be followed by any sequence of letters, digits, and underscores like: _var, amount, transaction_balance, address2. We could come up with the following implementation:

class VariableNameValidator {

static final VAR_NAME_PATTERN = /[_A-Za-z][_A-Za-z0-9]*/
boolean validate(String name) {
Matcher matcher = name =~ VAR_NAME_PATTERN
matcher.matches()
}

}

A common approach to test this would be the following:

static main(args) {

def var1 = ‘_abc’
def var2 = ‘123’
def var3 = ‘abcd_1234’
def vnv = new VariableNameValidator()
println vnv.validate(var1)
println vnv.validate(var2)
println vnv.validate(var3)

}

Looking at this code, we can see that:

  • We came up with different scenarios like a name starting with an underscore, a name containing only digits, and a name starting with letters followed by an underscore and then digits.
  • We printed the result of the validation in the standard output for each scenario to check if it is as expected.

Many developers argue that they don’t write tests because they don’t have enough time. But they do write tests like the one above all the time. Thus, time is not an issue, if the approach is:

  • It was written alongside the production code.
  • Verification is done manually, looking at the console to see if the output is as expected.
  • The scenarios are not explicitly described.
  • Probably the developer will delete this code once he verifies that the implementation is correct. Then, any time the developer has to maintain this code, he will write the tests and delete it again.

A better approach would be the following:

class VariableNameValidatorTest {

static main(args) {

// Initialization code
def validator = new VariableNameValidator()
def variableName, expectedResult, actualResult
// When a variable starts with an underscore followed by letters should validate successfully.
variableName = ‘_myVariable’
expectedResult = true
actualResult = validator.validate(variableName)
if (expectedResult != actualResult) {

throw new RuntimeException(‘Test failed.’)

}

// When a variable name contains only numbers should fail validation.

variableName = ‘123’
expectedResult = false
actualResult = validator.validate(variableName)
if (expectedResult != actualResult) {

throw new RuntimeException(‘Test failed.’)

}

// When a variable name consists of a word and a number separated by an underscore should validate successfully.

variableName = ‘abcd_1234’
expectedResult = true
actualResult = validator.validate(variableName)
if (expectedResult != actualResult) {

throw new RuntimeException(‘Test failed.’)

}
println “Tests completed successfully.”

}

}

Looking at the previous code, we can see that:

  • We are using a separate class to do all the testing. This way, we can keep it under version control apart from production code.
  • Each scenario is clearly described using the pattern: “When <something happens> should <expected result>.”
  • All the verification is done automatically.
  • A test code like this is a source of documentation. Reading the testing scenarios gives us a pretty good idea of what the VariableNameValidator class does.

You should note that I’m not using any test framework. Frameworks provide several features like automation, static code analysis, and deployment pipeline integration. But if you don’t see tests as an intrinsic part of your mental process to develop software, it will seem that these tools are just unnecessarily complex. You will end up writing worthless test code just to meet some arbitrary test coverage goal.

My point is that we do tests all the time. But, most of the time, we don’t use a systematic approach to do it. It is not necessary to expend a great deal of time and effort. Don’t think in TDD, unit testing, integration testing, test coverage, or pipeline checks. All of these are secondary. Testing is not an activity separate from writing code. It is an essential part of reasoning about it.

More Articles

Subscribe To Our Newsletter