Introduction to ``unittest`` ============================ - Python standard library module. - Sometimes referred to as PyUnit. - Based on the XUnit framework designed by Kent Beck and Erich Gamma. - Pattern repeated in many other languages including C, perl, Java, and Smalltalk. Writing Your First Test Case ---------------------------- Create a file named ``myfirsttest.py`` inside your virtual environment directory. It should contain the following: .. code-block:: python import unittest class MyFirstTestCase(unittest.TestCase): def test_true_is_really_true(self): self.assertTrue(True) def not_a_test(self): self.assertTrue(False) if __name__ == '__main__': unittest.main() What Is The Test Testing? ------------------------- - We've defined a :term:`test case`, with a single :term:`test` named ``test_true_is_really_true``. - *All methods of a TestCase must begin with ``test`` to be considered runnable.*. The ``not_a_test`` method will therefore not be executed by the test runner when we go to execute it. Running The Test Suite ----------------------- Here's how you run the test suite: .. code-block:: python $VENV/bin/python myfirsttest.py Here's the expected output of the execution of the above: .. code-block:: text . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK What Just Happened? ------------------- - The ``if __name__ == '__main__':`` stanza was true. - It calls ``unittest.main()``. - ``unittest.main()`` discovers all of the test *cases* it can find in the ``__main__`` module and creates a :term:`test suite` from them. - ``unittest.main()`` is shorthand for ``unittest.main('__main__')``. - The test suite is run. Interpreting Output ------------------- - Stray speck of dust is a dot. - 1 test run successfully. The ``test_true_is_really_true`` test passed. - Note that the ``not_a_test`` method was not executed. Let's Break Things ------------------ Copy the file you just created to one named ``badfirsttest.py``. Change the method named ``not_a_test`` to ``test_false_is_really_true``. .. code-block:: python import unittest class MyFirstTestCase(unittest.TestCase): def test_true_is_really_true(self): self.assertTrue(False) def test_false_is_really_true(self): self.assertTrue(False) if __name__ == '__main__': unittest.main() Run this test suite: .. code-block:: python $VENV/bin/python badfirsttest.py This should be the output: .. code-block:: text F. ====================================================================== FAIL: test_false_is_really_true (__main__.MyFirstTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "badfirsttest.py", line 9, in test_false_is_really_true self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=1) What Happened? -------------- - We changed the name of the ``not_a_test`` method of the test case to ``test_false_is_really_true``, therefore the test was found by the test runner. - False is not true, therefore the test failed. - The output has an ``F``, then a dot as the first line. This means that the first test executed and failed, and the second test executed and passed. (Note that tests in a test case are executed in alphabetical order by default, that's why the "second" test ``test_false_is_really_true`` executed before the "first" test ``test_true_is_really_true``). - There is output describing the failure and the line the offending code is on related to the failed test. It is in the form of a Python traceback. Let's Break Things Again ------------------------ Copy the ``badfirsttest.py`` file to a file named ``excfirsttest.py``. Add a method to it named ``test_divide_by_zero``. .. code-block:: python import unittest class MyFirstTestCase(unittest.TestCase): def test_true_is_really_true(self): self.assertTrue(True) def test_false_is_really_true(self): self.assertTrue(False) def test_divide_by_zero(self): self.assertEqual(1/0, 0) if __name__ == '__main__': unittest.main() Run this test suite: .. code-block:: python $VENV/bin/python excfirsttest.py Here's the output: .. code-block:: text EF. ====================================================================== ERROR: test_divide_by_zero (__main__.MyFirstTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "excfirsttest.py", line 12, in test_divide_by_zero self.assertEqual(1/0, 0) ZeroDivisionError: integer division or modulo by zero ====================================================================== FAIL: test_false_is_really_true (__main__.MyFirstTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "excfirsttest.py", line 9, in test_false_is_really_true self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1, errors=1) What Just Happened ------------------ - Our original ``test_false_is_really_true`` test continues to fail. - A new failure for ``test_divide_by_zero`` pops up. - The new failure is a different kind of failure. It's an "error". - An error means that the test code could not make an assertion before the code that is under test raised an exception. Alternate Command-Line Features of ``unittest`` ----------------------------------------------- ``unittest`` can be run as a ``-m`` target too. Copy ``excfirsttest.py`` to ``cmdlinefirsttest.py`` and change the latter's contents to look like this: .. code-block:: python import unittest class MyFirstTestCase(unittest.TestCase): def test_true_is_really_true(self): self.assertTrue(True) def test_false_is_really_true(self): self.assertTrue(False) def test_divide_by_zero(self): self.assertEqual(1/0, 0) In other words, just remove the ``if __name__ == "__main__"`` stanza. When we try to run this file, we get no output: .. code-block:: bash $ python cmdlinefirsttest.py Produces nothing. We can however run it using the Python ``-m`` command line feature: .. code-block:: bash $ python -m unittest cmdlinefirsttest We see the same output as if we had run: .. code-block:: bash $ python excfirsttest.py Note that we dropped the ``.py`` on ``cmdlinefirsttest.py`` because ``unittest`` when invoked via ``-m`` wants a *module name* rather than a *file name*. Running A Single Test --------------------- We can run a *single* test using the ``-m`` feature by naming its module, class, and method name on the command line instead of just the module: .. code-block:: bash $ python -m unittest cmdlinefirsttest.MyFirstTestCase.test_divide_by_zero Here's that output: .. code-block:: text E ====================================================================== ERROR: test_divide_by_zero (cmdlinefirsttest.MyFirstTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "cmdlinefirsttest.py", line 12, in test_divide_by_zero self.assertEqual(1/0, 0) ZeroDivisionError: integer division or modulo by zero ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (errors=1) Note that only a single test ran. Conclusions ----------- - Unit tests can be run via ``unittest.main()``. - Unit test methods must be attached to a TestCase. - Unit test methods must be begin with ``test``. - Unit test output consists of either a dot, a "F", or a "E" for each test succeeded, failed, or errored. - Output that includes failed or errored tests contains output about the reason for the failure or error in the form of a traceback. - We can run unit tests by running a file they're contained in or via ``python -m unittest``. - We can run a single unit test.