Hello World and Unit Testing with Python
Tags: beginners, webdev, python
Published: 2022-07-27 21:02:00

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](https://www.sitepoint.com/python-unit-testing-unittest-pytest/?utm_source=rss&ref=dailydevbytes.com) 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](https://www.istqb.org/) 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
```

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)
```

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
```
(`example.py` being the file containing the source code we are testing.)

**Results** should look like:

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

OK
```

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)
```

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
```

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)
```

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](https://www.sitepoint.com/python-unit-testing-unittest-pytest/?utm_source=rss&ref=dailydevbytes.com) of testing using `unittest` and `pytest`.