DEV Community

dankturtle
dankturtle

Posted on • Edited on

Hello World and Unit Testing with Python

Hello community. I've been in the IT field for decade or so and am now dipping my toes into the 'deep-end' of the field. Perhaps I should use 'deeper', as depth may somewhat be a matter of perspective. I expect many of you have a group you can confidently point to and declare them as the real nuts of the bunch.

I came across a thorough guide on testing code using unittest and pytest. I've condensed it into concise take-ways. In this article I focus on how to use unittest and how to understand the sometimes confusing results.

Software Testing is helpful to identify bugs and inconsistencies in code that can cause errors. The ISTQB breaks software testing into 4 levels:

  1. Unit Testing - tests specific lines of code
  2. Integration Testing - tests integration between many units
  3. Systems Testing - tests entire system
  4. Acceptance Testing - checks compliance with acceptance criteria

It's critical to note that just because a test passes, doesn't mean that the software is entirely bug-free. (If only it were that easy!)

Unit Testing verifies correct behavior of a 'unit' which may be a class, function, or method depending on the language. Unit testing is a low-level process which can identify bugs at local level before they propagate to other levels of the software system. Testing many units individually has several benefits: It eases identifying and locating integration problems. Bugs resulting from future modifications can be easier to location (provided good test history and documentation). Repeatedly testing the relationship of unit input and output provides documentation (results) prior to deployment.

Additional Qualities of a Unit Test:

  • Fast (in most cases)
  • Isolated - testing of individual units doesn't rely on external processes (like network resources, files, etc)
  • Repeatable - Many tests can confirm consistency of results
  • Naming - The name of a unit test should clarify the tests purpose

A common Organization Strategy for writing unit tests is the Arrange, Act, and Assert (AAA) pattern.

  • Arrange - set objects and variables
  • Act - call the unit being tested (function/class/method)
  • Assert - declare expected outcome

Having a uniform strategy like AAA will ensure your tests are clean and easy to read.

Unit Testing in Python is primarily performed with unittest and pytest.

unittest is included in the standard Python library and contains:

  • test case - tests a single unit
  • test suite - group of tests that run together
  • test runner - component that handles the execution and results of all test cases

unittest is written as methods of a class that subclasses unittest.TestCase and can use special assertion methods.

The guide uses the Example of a bank account that is designed to not allow withdraws greater than the current balance:

import unittest

class BankAccount:
  def __init__(self, id):
    self.id = id
    self.balance = 0

  def withdraw(self, amount):
    if self.balance >= amount:
      self.balance -= amount
      return True
    return False

  def deposit(self, amount):
    self.balance += amount
    return True
Enter fullscreen mode Exit fullscreen mode

A new test case is made in the same python file by creating class TestBankOperations that's a subclass of unittest.TestCase.

class TestBankOperations(unittest.TestCase):
    def test_insufficient_deposit(self):
      # Arrange
      a = BankAccount(1)
      a.deposit(100)
      # Act
      outcome = a.withdraw(200)
      # Assert
      self.assertFalse(outcome)
Enter fullscreen mode Exit fullscreen mode

Notice how the AAA method is used to Arrange (establish variables), Act (call unit to be tested), and Assert (verify outcome). Also note how the test method test_insufficient_deposit begins with test. This is important because every test method must start with test.

Ideally this test returns the result false. This is because we asked the test to deposit 100 and try to withdraw 200. To Assert the result, the example uses the special assertion method assertFalse(). Put more simply - we expect that depositing 100 and withdrawing 200 will fail. By using the special assertion method, we are declaring that the test will have passed if test_insufficient_deposit fails.

We Run The Test by opening the command line and running

python -m unittest example.py
Enter fullscreen mode Exit fullscreen mode

(example.py being the file containing the source code we are testing.)

Results should look like:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Enter fullscreen mode Exit fullscreen mode

This indicates that our test passed and the attempt to withdraw in excess of balance failed.

We can generate a Failed Test for this example by creating another unit test that tries to deposit a negative amount to our account:

def test_negative_deposit(self):
    # Arrange
    a = BankAccount(1)
    # Act
    outcome = a.deposit(-100)
    # Assert
    self.assertFalse(outcome)
Enter fullscreen mode Exit fullscreen mode

To get more thorough results from the failed test we add -v (verbose) to unittest in the command line. We run unit test, which will now run both the insufficient deposit test and the negative deposit test.

python -m unittest -v example.py
Enter fullscreen mode Exit fullscreen mode

And our result:

test_insufficient_deposit (example.TestBankOperations) ... ok
test_negative_deposit (example.TestBankOperations) ... FAIL

======================================================================
FAIL: test_negative_deposit (example.TestBankOperations)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "example.py", line 35, in test_negative_deposit
    self.assertFalse(outcome)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)
Enter fullscreen mode Exit fullscreen mode

We see that our second test failed in line 35. In self.assertFalse(outcome) we asserted that our test should return false, as we don't want our program accepting negative deposits. However the outcome was true (successful) and therefore our test failed.

We can use this information to determine that we may need to modify the code if refusing negative deposits is one of the design requirements for our program.

unittest has Different Assertion Methods based on your needs

  • assertEqual(x,y) - tests x==y
  • assertRaises(exception_type) - checks if exceptions are raised
  • assertIsNone(x) - tests if x is None
  • assertIn(x,y) - tests if x in y

To Expand Functionality of unit testing in Python, it is recommended to use the pytest library (non-native), which allows for more complex testing using less code, supports the native unittest suites and offers more than 800 external plugins.

If you have made it this far, thanks for reading my post. I hope I was able to concisely explain and demonstrate the value of unittest and help some beginners make more sense of somewhat confusing results (like assertfalse failed true is not false) indicate. Credit for code examples goes to Lorenzo Bonannella in his very thorough guide of testing using unittest and pytest.

Top comments (0)