Dear Testing, I was wrong about you

Dear Testing,
I’m sorry I was so wrong about you. When we first met in my Software Design class, I didn’t know what to think of you. Honestly, I think you were misconstrued. Your purpose and design were skipped over, and we were tossed into assert statements without regard. For our final project in that class, there was a requirement everyone had to write tests for the code they worked on.
I worked on the database and business logic. Tasks that you are made for. Some of my teammates however, who only worked on the front-end, struggled to figure out how they could write any tests. Before we divided the work, we foolishly forgot to think that testing front-ends programmatically isn’t a real thing.
I remember falling asleep in a table in another classroom at 1AM while they were still trying to figure it out. When I woke up and 2 hours later, still nothing had come about. In the end, I don’t even remember what we did, I think we set up assert statements for the colors of buttons or something. Its okay, you can laugh.
Since that day, I doubted you for all the wrong reasons. Like some idiot, I tested my code manually, irrespective of potential edge cases, and not validating before writing more code. At least I never put all my code in a single big try except block.
I’m sorry testing. Please have me back.
Arez
For those who have worked in a production code environment, you probably are thinking “no duh”. You likely have experienced testing all the time, and you likely have a love hate relationship with it, and some strong opinions. However, to the uninitiated testing really isn’t a topic that is covered well enough. Either you have a class that directly focuses on the topic briefly or you learn on the job. Looking through the internet to understand the methodology behind testing, you have to watch various different videos and piece it all together.
To be fair, my neglect of testing was to some extent out of laziness. Whenever I wrote code for a project, even for my senior capstone which was a large codebase. I never once tested. Instead, I just knew the entire codebase so well that I was able to debug most issues that came up.
In reality, the enlightenment was right in front of my eyes. I don’t recall the specific event, but in reaction to a software oversight, one of my favorite professors told me “You can’t call yourself an ‘engineer’ if you don’t test everything the best you can” She was right. But testing should be a skill that is incredibly valuable even for smaller scale personal projects, and developing with tests in mind helps us write better code.
It is not enough to just learn HOW to test, its important to WHEN we should test, WHAT should we test, and WHY we should test. Anyone after watching a video could understand how to write a basic test. Yet if you do not understand the methodological reasoning behind testing, knowing how to write a test is meaningless. It is that methodology that made testing feel cumbersome to me. Understanding the connections between the when, where, why and how enabled me to overcome that block.
I certainly am still developing my intuition when it comes to testing, and managing tests. However, we all start somewhere and I am writing the following article to demonstrate some testing fundamentals, and I will be doing so using a popular python module for testing known as pytest. However a lot of these concepts can be applied to other languages as well. In the following I am going to take a function that needs a makeover, and use it to demonstrate fundamental testing concepts.
Testing == Better Code
Lets use the following code sample to show what I mean. Note that the code samples that I am going to use are intentionally simple. As I want to communicate the overarching ideas behind testing, rather than having you understand some complicated functionality.
Lets say I have the following function. In short, this function reads a file for a keyword, and “scrambles” the first appearance of that keyword in the file. Then writes to a new file.
def process_text_file(keyword, shift_amount=3):
input_file = "src/input.txt"
output_file = "src/output.txt"
try:
with open(input_file, 'r') as f:
text = f.read()
except FileNotFoundError:
return "Error: File not found"
keyword_index = text.find(keyword)
if keyword_index == -1:
result = text
else:
before_keyword = text[:keyword_index]
after_keyword = text[keyword_index + len(keyword):]
shifted_keyword = ""
for char in keyword:
if char.isalpha():
shifted_keyword += chr(ord(char) + shift_amount)
else:
shifted_keyword += char
result = before_keyword + shifted_keyword + after_keyword
with open(output_file, 'w') as f:
f.write(result)
return result
If we want to break it down line by line the function:
- Reads text from a file at
src/input.txt
- Searches for a keyword inside of the text
- If the keyword is not found, it returns the text in the file
- If the keyword is found, we take the index of that keyword in the returned string and use it to split the text from text before and after the keyword
- We use this index to split the keyword into text before and after it in the file buffer
- We shift the ordinal value of each of the characters after the keyword by the shift amount
- We combine the newly shifted keyword with the text before and after it.
- We write all of this to the file
src/output.txt
There’s nothing fundamentally wrong with this function. Oftentimes we will go and write code like this, and then make it look better and be more extensible through refactoring. I have found that for me, thinking about making the code testable (even if I don’t test it) has helped me in my refactoring process.
Now, lets say that this function belongs to a larger module. One that maybe generates our text, writes it to the file, and provides the keyword to search for. Whatever it may be. Lets say we run the module and we have the keyword “hello” and a shift amount of 3, expecting the output file to contain “khoor” (each letter shifted by 3 ordinally), however instead we get a completely different result – maybe the keyword wasn’t shifted at all, or the file is empty, or we get an error.
Debugging wise, it becomes difficult to tell where we went wrong. Was it the file reading or writing , the keyword search logic, the character shifting? Was it something completely outside of the scope of the function? You might be thinking “Okay well we can just verify all of those work by just looking at the code” To which I say, you can probably do it for this simple example, but you can’t do this for everything you code. If you feel like you can, well congratulations, you have just solved the Halting Problem and Turing and Gödel are rolling in their graves.
Through creating test(s) we can verify which component is working as expected, and which isn’t. Lets go ahead and create a test for this function using pytest.
How to write a test
Before we start writing tests, lets go over some of the basics of testing in pytest. Originally, when writing this article I was intending on going into more detail about pytest, however I am going to focus on testing fundamentals today.
Foremost, the most important thing in testing (regardless of language) is the assert
. We use an assert
whenever we want to compare what we expected vs what we actually obtained.
Say I had this simple adding function:
def add(a, b):
return a + b
and I wanted to verify it was working. I could create the following test in pytest
:
import add
import pytest
def test_add():
result = add(2, 3)
assert result == 5
Then I could run it using pytest test_filename.py
in the terminal, or simply pytest
if I want to run all tests in the current directory. This seems pretty simple, but imagine all of the outputs that you can verify with assert! You can verify data, return statements and much more.
Another important concept in testing is the idea of mocking. Mocking is the practice of replacing real dependencies or external systems with fake, controlled versions during testing. This allows you to test your code in isolation without relying on things like actual files, databases, network calls, or other external resources. For example, instead of reading from a real file on disk, you could mock the file reading operation to return predetermined test data.
For instance lets say we are calling on an API, but we don’t actually want to call on the API itself for whatever reason (rate-limiting, no connection, etc.) Well what we can instead do is mock it:
import requests
from unittest.mock import patch
def get_user_data(user_id):
"""Fetches user data from an external API."""
response = requests.get(f"https://api.example.com/users/{user_id}")
data = response.json()
user_info = {
"user_id": data["id"],
"full_name": data["name"],
"contact_email": data["email"],
"is_active": data.get("active", True)
}
return user_info
# In pytest, this replaces any calls to request.get with the mock
@patch('requests.get')
def test_get_user_data(mock_get):
# Sets the return value of requests.get to be hardcoded to these values
mock_get.return_value.json.return_value = {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"active": True
}
#Call the function, with the requests.get call inside mocked
result = get_user_data(123)
assert result["full_name"] == "John Doe"
assert result["contact_email"] == "john@example.com"
assert result["is_active"] == True
mock_get.assert_called_once_with("https://api.example.com/users/123")
In this example, we never actually make a real HTTP request to the API. Instead, we mock requests.get
to return fake data. This doesn’t seem all that bad, but imagine doing this for all sorts of things. Further, ask yourself: are we really testing anything of value here? What we are really testing is how we manipulate the data, not that we can get it,.
In fact, lets take a look at a test for our original process_text_file()
to see how mocks could be horrific.