When testing software, a technique known as unit testing examines the tiniest testable bits of code and checks them for proper operation. We can ensure that every component code, including auxiliary functions that may not be visible to the user, functions correctly and as intended by doing unit tests.
The concept is that we independently test every component of our program to make sure it functions. Regression and integration testing, in contrast, verifies that the program’s various components function properly and according to plan.
In this post, learn how to use the built-in PyUnit framework and the PyTest framework, two well-known unit testing frameworks, to build unit testing in Python.
Following this training, you will be aware of the following:
- Python unit testing packages like PyUnit and PyTest
- Using unit tests to examine expected function behavior
What is Unit Testing?
Do you recall performing various arithmetic operations to complete math problems in school before combining them to produce the correct answer? Then, consider how you would verify that each step’s calculations were accurate and that nothing was written down incorrectly or carelessly.
Now apply that concept to code! For example, how would you write a test to check that the following line of code returns the area of the rectangle? We wouldn’t want to constantly review our code to validate that it is accurate statically.
def calculate_area_rectangle(width, height):
return width * height
We might execute the code with a few test instances to see if it produces the desired results.
A unit test should accomplish that! A unit test is a test that verifies the functionality of a single line of code, typically modularised as a function.
Why do we need Unit Testing?
Regression testing relies heavily on unit tests to guarantee that the code is stable and continues to operate as expected after modifications are made. After making changes to our code, we may run the unit tests we previously wrote to make sure that our modifications did not affect the functionality of other areas of the codebase.
Unit tests provide the essential additional advantage of making error isolation simple. Imagine completing the project and getting a long list of errors. What steps would we take to debug our code?
Unit tests can help in this situation. If any part of our code has been producing errors, we may examine the results of our unit tests to determine where to begin the debugging process. That’s not to suggest that unit testing can’t sometimes assist us in locating the bug. Still, it provides a much more practical starting point before we examine how components are integrated into integration testing.
By testing the functions in this Rectangle class, we will demonstrate how to perform unit testing for the remainder of the article:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
Let’s investigate how to use motivated unit tests in Python and how to include them in our development workflow now that we have them!
Designing a Test Strategy
Now let’s examine the process of creating a testing strategy.
Definition of test scope
A crucial question must be addressed before formulating a test strategy. What components of your computer program do you wish to test?
Due to the impossibility of complete testing, this is a key question. Because of this, you cannot test every input and output; instead, you should prioritize your tests according to the associated risks.
When defining your test scope, many considerations must be made:
- Risk: What would be the business repercussions if a defect were to harm this component?
- How quickly do you want your software to be completed? Have you set a due date?
- Budget: What kind of financial commitment are you willing to make to the testing activity?
Once you define the testing scope that determines what you should and shouldn’t test, you’re ready to talk about the characteristics a good unit test should have.
Qualities of a unit test
1. Fast – Unit tests must be quick because they are typically run automatically. Unfortunately, developers are more prone to bypass slow unit tests since they don’t offer quick feedback.
2. Isolated – Unit tests are, by definition, independent. They only test each piece of code and don’t rely on other resources (like a file or a network resource).
3. Repeatable – Unit tests are run often, and the outcome needs to remain constant.
4. Reliable – Only if a flaw in the system being tested will unit tests fail. It shouldn’t matter how the tests are run or in what environment.
5. Appropriately named – The exam’s name should contain pertinent information about the test itself.
Before getting into Python unit testing, one more step needs to be completed. How can we set up our tests, so they are neat and simple to read? We employ a strategy known as Arrange, Act, and Assert (AAA).
The AAA pattern
The Arrange, Act, and Assert a paradigm is a typical approach used to write and organize unit tests. The way it operates is as follows:
- All the elements and variables required for the test are set during the Arrange step.
- During the Act phase, the function/method/class under test is called.
- We finally validate the test result during the Assert phase.
By separating the setup, execution, and verification phases of a test, this technique offers a tidy way to arrange unit tests. Additionally, unit tests are simpler to read because they all use the same format.
Automated and Manual Testing
Manual testing takes another form which is known as exploratory testing. It is testing that is done without any plan. For manual testing, we need to prepare a list of the application; we enter various inputs and wait for the expected output.
Every time we make inputs or change code, we have to go through each list function and check it.
It is the most common way of testing and it is also a time-consuming process.
Automated testing, on the other hand, executes the code according to our code plan, which means it runs the part of the code we want to test in the order we want to test them with a script instead of a human.
Python provides a set of tools and libraries that help us create automated tests for an application.
Description of Tests and Basic Concepts used
Basic concepts used in the code:
- assertEqual() – This statement is used to check if the obtained result is equal to the expected result.
- assertTrue()/assertFalse() – This statement is used to verify whether a given statement is true or false.
- assertRaises() – This command is used to raise a specific exception.
Test description:
1. test_strings_a:
This test is used to test the property of a string in which a character says “a” multiplied by a number say “x” and gives the output as x times “a”. If the result matches the given output, then claimEqual() returns true in this case.
2. test_upper:
This test is used to check whether a given string is converted to uppercase or not. assetEqual() returns true if the returned string is in uppercase.
3. test_isupper:
This test is used to test a string property that returns TRUE if the string is uppercase, otherwise, it returns False. AssTrue() /assertFalse() command is used for this verification.
4. test_strip:
This test is used to check that all characters passed in the function have been removed from the string. claimEqual() returns true if the string is removed and matches the given output.
5. test_split:
This test is used to check the string split function, which splits the string using the argument passed in the function and returns the result as a list. If the result matches the given output, then claimEqual() returns true in this case.
6. unittest.main()
It provides a command line interface to the test script.
Using the PyUnit Framework included in Python
You might be asking why we need unit testing frameworks in Python and other languages that have the assert keyword. With the aid of unit testing frameworks, we can automate the testing process, run numerous tests with various parameters on the same function, look for expected exceptions, and do many other tasks.
Python’s built-in unit testing framework is called PyUnit, and it is the language’s equivalent of the Java-based JUnit testing framework. To get started writing a test file, we need to import the unittest library to use PyUnit: import unittest
The first unit test can then begin to be written. PyUnit’s unit tests are organized as subclasses of the unittest class. By overriding the runTest() method in the TestCase class, we can create our unit tests that use various assert functions from unittest to verify conditions. TestCase:
class TestGetAreaRectangle(unittest.TestCase):
def runTest(self):
rectangle = Rectangle(2, 3)
self.assertEqual(rectangle.get_area(), 6, "incorrect area")
Our initial unit test is that. It looks to see if the rectangle. The right area is returned by the get area() method for a rectangle with dimensions of 2 and 3. We apply self. Instead of just using assert, the unittest library now supports assertEqual, allowing the runner to gather all test cases and generate a report.
Use several assert methods from the unittest.
TestCase also allows us to test diverse behaviors such as self.
assertRaises(exception) – This allows us to check if a given code block causes an anticipated exception.
In our program, we call unittest.main() to execute the unit test. unittest.main()
The output indicates that the tests passed properly because the code returned the expected result in this instance:
Ran 1 test in 0.003s OK
Here is the full code:
import unittest
# Our code to be tested
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
# The test based on unittest module
class TestGetAreaRectangle(unittest.TestCase):
def runTest(self):
rectangle = Rectangle(2, 3)
self.assertEqual(rectangle.get_area(), 6, "incorrect area")
# run the test
unittest.main()
We can also nest many unit tests together in one subclass of unittest. Using the “test” prefix to name methods in the new subclass, for instance
class TestGetAreaRectangle(unittest.TestCase):
def test_normal_case(self):
rectangle = Rectangle(2, 3)
self.assertEqual(rectangle.get_area(), 6, "incorrect area")
def test_negative_case(self):
"""expect -1 as output to denote error when looking at negative area"""
rectangle = Rectangle(-1, 2)
self.assertEqual(rectangle.get_area(), -1, "incorrect negative output")
We are now focusing on expanding our tests. What if we had some setup code that we were required to perform before each test? In unittest, we can override the setUp method. TestCase.
class TestGetAreaRectangleWithSetUp(unittest.TestCase):
def setUp(self):
self.rectangle = Rectangle(0, 0)
def test_normal_case(self):
self.rectangle.set_width(2)
self.rectangle.set_height(3)
self.assertEqual(self.rectangle.get_area(), 6, "incorrect area")
def test_negative_case(self):
"""expect -1 as output to denote error when looking at negative area"""
self.rectangle.set_width(-1)
self.rectangle.set_height(2)
self.assertEqual(self.rectangle.get_area(), -1, "incorrect negative output")
We have modified the setUp() method from unittest in the code example above. Using our own setUp() method, we create a Rectangle object in the TestCase. When several tests rely on the same code to set up the test, the setUp() method, executed before each unit test, helps prevent code duplication. This is analogous to the @Before decorator in JUnit. In the same way, we can override the tearDown() method to have code run after each test.
The possibilities for PyUnit are merely the tip of the iceberg. In addition, we could write tests that look for exception messages that match a regex expression or only call setUp/tearDown methods once or for exception messages that match setUp/tearDown methods.
Unit Testing in Action
We’ll examine unit testing in practice next. In our example, we’ll be using PyUnit to test a method that uses pandas datareader to collect stock data from Yahoo Finance:
import pandas_datareader.data as web
def get_stock_data(ticker):
"""pull data from stooq"""
df = web.DataReader(ticker, "yahoo")
return df
This function crawls the Yahoo Finance website for stock data on a certain stock ticker and returns a pandas DataFrame. This has many potential failure modes. For instance, if Yahoo Finance is down, the data reader might not return anything or provide a DataFrame with blank columns or blank data in the columns (if the source restructured its website). Therefore, we should offer a variety of test functions to check for a variety of failure modes:
import datetime
import unittest
import pandas as pd
import pandas_datareader.data as web
def get_stock_data(ticker):
"""pull data from stooq"""
df = web.DataReader(ticker, 'yahoo')
return df
class TestGetStockData(unittest.TestCase):
@classmethod
def setUpClass(self):
"""We only want to pull this data once for each TestCase since it is an expensive operation"""
self.df = get_stock_data('^DJI')
def test_columns_present(self):
"""ensures that the expected columns are all present"""
self.assertIn("Open", self.df.columns)
self.assertIn("High", self.df.columns)
self.assertIn("Low", self.df.columns)
self.assertIn("Close", self.df.columns)
self.assertIn("Volume", self.df.columns)
def test_non_empty(self):
"""ensures that there is more than one row of data"""
self.assertNotEqual(len(self.df.index), 0)
def test_high_low(self):
"""ensure high and low are the highest and lowest in the same row"""
ohlc = self.df[["Open","High","Low","Close"]]
highest = ohlc.max(axis=1)
lowest = ohlc.min(axis=1)
self.assertTrue(ohlc.le(highest, axis=0).all(axis=None))
self.assertTrue(ohlc.ge(lowest, axis=0).all(axis=None))
def test_most_recent_within_week(self):
"""most recent data was collected within the last week"""
most_recent_date = pd.to_datetime(self.df.index[-1])
self.assertLessEqual((datetime.datetime.today() - most_recent_date).days, 7)
unittest.main()
The unit above tests examine the presence of specific columns (test columns present), the data frame’s non-emptiness (test non-empty), whether the “high” and “low” columns truly represent the high and low of the same row (test high low), and whether the most recent data in the DataFrame was entered within the previous week (test most recent within the week).
Consider working on a machine learning project using stock market data. A unit test framework will help you detect if your data preprocessing is performing as planned.
We can determine whether there was a material change in the output of our function using these unit tests, which may be done as part of a Continuous Integration (CI) procedure. Then, depending on the functionality we rely on from that function, we can apply additional unit tests as necessary.
Here is PyTest’s equivalent version for completeness:
import pytest
# scope="class" tears down the fixture only at the end of the last test in the class, so we avoid rerunning this step.
@pytest.fixture(scope="class")
def stock_df():
# We only want to pull this data once for each TestCase since it is an expensive operation
df = get_stock_data('^DJI')
return df
class TestGetStockData:
def test_columns_present(self, stock_df):
# ensures that the expected columns are all present
assert "Open" in stock_df.columns
assert "High" in stock_df.columns
assert "Low" in stock_df.columns
assert "Close" in stock_df.columns
assert "Volume" in stock_df.columns
def test_non_empty(self, stock_df):
# ensures that there is more than one row of data
assert len(stock_df.index) != 0
def test_most_recent_within_week(self, stock_df):
# most recent data was collected within the last week
most_recent_date = pd.to_datetime(stock_df.index[0])
assert (datetime.datetime.today() - most_recent_date).days <= 7
Building unit tests might seem laborious and time-consuming. Still, they can be an essential component of any CI pipeline and are great tools for finding errors early on before they go further down the pipeline and become more challenging to fix.
Conclusion:
To sum up, we discussed the fundamentals of unit testing in this essay. We learned the importance of unit testing and the need for everyone to test their programs. Finally, we discussed unit testing and how to create and use basic Python unit tests.

