Getting faster with testing

Getting faster with testing

Small intro

Perfectly skippable

I know that this title sounds a bit contradicting when you are new or generally not used to writing your own tests. In certain moments, it is, so you are partially right.

Personally, I as well experienced the path from not understanding the difference between a unit or an integration test (really understanding it) to getting a bit TDD addicted. I know I know, sounds like I’m intelligent as a chair, but when you work with frameworks such as Spring, it’s perhaps not that intuitive for a beginner.

To generally address the “contradictions” of the title, if you are bumping into testing it may seem like a waste of time, extra unnecessary code to maintain or simply boring. The funny part is, you are going to do it anyway. And most likely in such a manner that you are going to spend more time doing it.

How are you actually wasting more time not writing tests

I’m going to write my examples in Go since that is my main orientation for now, but I generally like to think it’s in the principles, not the tech stack. So it’s applicable to any environment as well.

Let’s say it’s another day at your job and you are writing some service code for your web backend. Your ticket requires you to add a simple method of adding two numbers, so your Excel-like web frontend can call the endpoint and get the result. I know, it’s quite a useless example, and I’m quite confident you won’t encounter it in your daily job.

Somewhere inside your service, you have:

package calc

func Add(a, b int) int {
 return a + b + 1
}

This code is quite simple, and even a TDD fanatic would be courageous to push it to production directly. But let’s not treat it that way.

Now, you as a conscious developer prior to opening your PR and asking for a review want to test your code and see if it’s actually behaving as it should. What are you going to usually do? Fire up Postman, click on “New request”, fill up the URL, add the endpoint, required parameters, fire up the server and click “Send”. That does include specifying the “Content-Type” header and the JSON creation. Of course, you can also curl it, it’s a longer one:

curl --location --request POST 'http://localhost:8080/calculations/addition' \
--header 'Content-Type: application/json' \
--data-raw '{
    "a": 5,
    "b": 5
}'

For some unknown reason, the response is:

{
  "result": 11
}

Well, that’s off, let’s check the code. It seems that for some quite unknown reason, we added + 1 at the end of our function. Maybe it’s a copy+paste error, maybe just talking with a colleague and not looking at what I’m typing error, or maybe it was the GitHub copilot. Either way, it’s bad. We go quickly fix it, we go to Postman, click “Send” — phew it’s all good now.

This was indeed a simple process. But what if it had more variations? What if we have a bit of complex branching within our code? What if we have to pass several layers of code just to get to invoke this method, and it has to pass several layers back? We have to do a lot of work just to test the Add() function. Even in this scenario, we could have been faster.

The alternative would be to have:

package calc

import (
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestAdd(t *testing.T) {
 actual := Add(5, 5)
 assert.Equal(t, 10, actual)
}

Or if you want to make the test really good and fancy:

package calc

import (
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestAdd(t *testing.T) {
 tests := []struct {
  name     string
  a        int
  b        int
  expected int
 }{
  {
   name:     "When 1 + 1 should be 2",
   a:        1,
   b:        1,
   expected: 2,
  },
  {
   name:     "When 2 + 2 should be 4",
   a:        2,
   b:        2,
   expected: 4,
  },
  {
   name:     "When 3 + 3 should be 6",
   a:        3,
   b:        3,
   expected: 6,
  },
 }

 for _, test := range tests {
  t.Run(test.name, func(t *testing.T) {
   actual := Add(test.a, test.b)
   assert.Equal(t, test.expected, actual)
  })
 }
}

So, besides actually being faster, and not worrying about the surrounding code we added several other things to our code and us.

We added:

  1. self-confidence in our code

  2. A code constraint — if we or someone adds the + 1 the tests will break and protect us

  3. documentation of the method.

The beauty of it it’s always there. You don’t have to bug around with Postman or other methods. You are gaining on two fronts, momentarily testing, but also expanding and guarding your code with those tests. If you are testing the endpoint itself, write an integration test that does an HTTP POST to your server and verifies the result. After a few times almost always you are going to be faster than the manual method and it’s easy to modify — and as mentioned before, it guards you.

Confidence

One of the greatest benefits that I think gets you hooked up on TDD or testing in general, is the confidence it gives you when you push your code. For some items, you didn’t even start up your app and you have the confidence it works. It gives you fast verifications of your code and it gives you sort of written proof that it’s good. Other than that, I can’t even count the number of times I found my own bugs before pushing the code actually thanks to those tests. It could be small things, it could be you learning that you actually don’t understand either the requirement or the tools you are using within the code.

Speed

Testing is one of those items that gain speed over time. When you are new to it, the testing environment hasn’t been set up fully perhaps, or the culture doesn’t nurture it yet — it’s probably not the fastest thing around. It’s one of the investments you have to give and wait for to come back. I promise you it will come back. As you get more proficient with it and the team is starting to gain its culture, your development speed will increase dramatically. It might even get to you to the point of being faster than you originally were.

We also have to think about more than just the time we invest to write those tests. When you add the time and effort saved from discovering bugs early, not introducing new ones, and organising your code efficiently and well (in order to be able to test something good you have to define your classes/interfaces/objects well. Otherwise it’s not testable properly.), and the documentation you provide. The math is quite good. It’s also a really great learning mechanism for technology and tools.

Code constraint and guarding

Each time you write a test you add another guard to your code. What do I mean by that? If you or someone else comes and modifies the code, there could be unintended consequences. You attempt to improve the code, but yet the test breaks. That can be a wonderful pointer that you made a mistake and have to watch out, otherwise you could end up breaking your production environment.

Refactoring is one of the most wonderful examples. One of the top priorities prior to heavy refactoring is to have tests in place. Otherwise, how are you going to know you didn’t break the code? It’s quite normal to want to come back to your old code, wanting to improve it. IDEs help a lot of course, but without tests and doing some deep refactors, you are risking breaking functioning code. Of course, it doesn’t have to mean that those tests are valid as well, but if you are in a good environment with a good team it’s abnormally helpful!

Documentation

One of the greatest benefits I could put forward is documenting your code with tests. When you look at a method or an interface/class/object you are trying to figure out what is it doing for and what is its purpose. Well, don’t the tests seem tempting to look at? If you scroll back to the “fancy” example you can see how it does that. From the behaviour naming of tests “When we get this it should do this” to simply looking at the code execution. When tests are written well, with behaviour descriptions they can provide wonderful documentation and insights into our code so when a new engineer comes, he/she can go through the tests and understand our code much, much faster.

I hope I managed to persuade you to at least give it a shot and see how it suits you. Remember to give it time and you have to be persistent. I would also recommend reading the “Test Driven Development: By Example” — by Kent Beck. Not to be forced to do TDD, just to get more insights into testing and what it brings you. In addition, it’s always cool to check out your testing framework in more detail. Every technology has it.

Also, be careful not to overdo it. It can be tempting but also lead to bad tests. For example mocking absolutely everything that in the end creates pointless tests, etc.

Have in mind that the example provided is a simple one. There is a good challenge in organising your code, providing good mocks and verifications. But it also brings lots of fun (and frustration (: ) with it!

Have a pleasant day!