Site icon TechVidvan

Unit Testing in Python

unit testing with python

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:

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:

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:

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:

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.

Exit mobile version