Why do I feel lazy to write tests?

Javier Berga García
Trucksters
Published in
6 min readMay 25, 2023

Introduction

All software developers have been lazy at some point when it comes to writing tests for our code. How many times have we forgotten about that broken test that we’ll fix later? You are not alone. This is a very common situation for a developer. I’m not going to convince anyone of the importance of testing, even the simplest test can provide a lot of information, especially the simplest test.

In this post, we’re going to discuss about the elements that make us feel lazy and share solutions so that these elements don’t become a hindrance when it comes to writing tests. This analysis takes into consideration a set of factors that were found in a deep analysis of our repository.

“Testing is a skill. While this may come as a surprise to some people it is a simple fact.” — Fewster and Graham

The test is not dessert, it’s the first course

Logically, a function can only be tested after it’s been written, right? We often build tests once we’ve finished writing our code, however, building tests first can serve as a tool for the subsequent development of the code. This development methodology is known as TDD (Test Driven Development) and can be summarized in three steps:

  1. Write an automated test for a new functionality.
  2. Run the test and see it fail (since the functionality hasn’t been implemented yet).
  3. Write the code that passes the test and run it again to ensure that the code works correctly.

This cycle would be repeated several times until the functionality is fully developed.

TDD forces you to think about the requirements of your functionality before developing it, which is especially useful for saving time detecting and correcting errors.

Additionally, the satisfaction that comes from finishing the development of a function and seeing that the test has already been built is unparalleled.

Juggling to build input data

This is the first red flag that tests can indicate. The input data should be easy to reproduce and building them should not take much time. There are several reasons why this may happen:

External dependencies

If your function depends on external resources such as databases, web services, or file systems, it can be complicated to simulate this environment. If you are constantly replicating the database for your tests, beware! It might be a sign that you have the database tightly coupled to your code and creating input data can be slow and tedious as you will have to create them based on the logic stored in the database. Removing the database from the core of your repository is a clear relief when it comes to generating the data that will serve as input for your functions.

Creating an interface to manage database reading is a good measure to have the database decoupled from the code. By using an interface, we can define a set of methods that allow us to access the data without worrying about the underlying database implementation.

Coupling

Coupling and cohesion are two software quality metrics regarding structured design. These two metrics are usually contrasted. A high level of coupling explains how dependent our modules are in the code.

The following example will illustrate how different modules can be highly coupled:

class Order:
def __init__(self, order_id, customer, items):
self.order_id = order_id
self.customer = customer
self.items = items

def get_total(self):
total = 0
for item in self.items:
total += item.price * item.quantity
return total

def get_order_id(self):
return self.order_id

class Customer:
def __init__(self, name, email, address, phone):
self.name = name
self.email = email
self.address = address
self.phone = phone

class Item:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity

In this example, the object Order contains an attribute Customer , this object contains a lot of information that is not probably needed by Order . Same happens between Item and Order. Thus, the classes in this code are tightly coupled.

Why is this important when testing? Well, testing Order class would imply defining different types of objects. This procedure might be slower than expected and initializing an Order class might take too much time.

Decoupling highly coupled code may be a challenge. Sometimes it is not an easy task, however, a good exercise is to analyze the input data and wonder if all that data is needed. Coming back to the example, the class Order does not need more information about a Customer apart from its id.

Our advice is simplifying as much as possible the input information needed in order to make the code readable and repeatable.

Never forget KISS

We often wonder if we can reuse our code to reduce our efforts and standardize the way we work. Tests are a perfect scenario for code reuse. Some data that is created is often the same for different tests, and with different inputs, we test the same code. We can create a superstructure that generates the base data we need for each test.

Error! These developments are smart, sophisticated, and… deceiving. As we add more tests, new specific needs arise for each of them, and changes in those generic developments become a reality. Eventually, we end up with a complex script that generates data for all tests to work.

Keep It Simple, Stupid! These generic and complex developments end up requiring time to be understood and modified. If you don’t want to get bored reading legacy code, it’s preferable to generate data in the simplest and most intuitive possible way.

On the other hand, a test should have as input data the minimum to test that functionality; otherwise, we end up having cross-data from another test that could affect the expected result.

“Simplicity is the key of brilliance.” — Bruce Lee

Divide and conquer

A good collection of tests deserves good organization. As more and more tests are created, it becomes increasingly difficult to keep everything located. During different developments, tests are likely to undergo modifications. In this sense, we must feel at home and keep the folder structure organized. Everything must be at hand. A good folder structure could be the following:

project/
├── test/
│ ├── unit/
│ │ ├── test1/
│ │ │ ├── datasets/
│ │ │ ├── test1.py
│ ├── integration/

A good folder structure can help you keep everything under control. Use descriptive file and folder names to make it easy to find what you’re looking for.

In addition, some tests might have something to test in common. For instance, testing different methods in the same class. These tests can share some elements since those methods belong to the same class and have the same attributes. Here we have a clear use case, which is testing a class. It can be a good idea to set all the tests under the same context within a class TestClass.

Let’s see it in an example. Imagine we want to test the previous class Order:

class TestOrder:
def test_get_total():
items = [Item("item1", 10, 2), Item("item2", 5, 3)]
order = Order(1, "John", items)
assert order.get_total() == 35

def test_get_order_id():
order = Order(1, "John", [])
assert order.get_order_id() == 1

In this case, alphabetic order might add some noise and not be the best idea.

Conclusion

Why do I feel lazy about writing tests?

In my case, two words are needed in order to answer this question: time and practicality.

If building a test involves a considerable investment of time, it is worth stopping to think about what elements are preventing development from being agile. Time should be spent on designing a dataset that can properly test the functionality and edge cases we look for.

In addition, the test folder is a folder in which we will constantly work, so it must be a comfortable, organized, and very intuitive workspace.

This article serves as an example of an analysis process in which we have been immersed. The final result is more than satisfactory and the testing process has been integrated very naturally into each of our developments.

Did you find this interesting?

At Trucksters, we are always facing new problems and challenges. Visit our Careers’ homepage to check the latest open positions! 👇

--

--