From 95754058906cc165ac77815fc31d731c6a4da053 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 18 Feb 2019 10:14:22 -0800 Subject: [PATCH 1/6] changed the "cigar_party" (potentially offensive) examples to "walnut_party" --- source/examples/testing/cigar_party.py | 15 ----- source/examples/testing/test_cigar_party.py | 61 ----------------- source/examples/testing/test_walnut_party.py | 65 +++++++++++++++++++ source/examples/testing/walnut_party.py | 15 +++++ source/exercises/unit_testing.rst | 30 +++++---- source/modules/Testing.rst | 41 ++++++------ .../Lesson01/codingbat/Logic-1/cigar_party.py | 45 ------------- .../codingbat/Logic-1/walnut_party.py | 49 ++++++++++++++ source/solutions/Lesson06/cigar_party.py | 15 ----- source/solutions/Lesson06/test_cigar_party.py | 64 ------------------ .../solutions/Lesson06/test_walnut_party.py | 63 ++++++++++++++++++ source/solutions/Lesson06/walnut_party.py | 14 ++++ .../codingbat/Logic-1/cigar_party.py | 45 ------------- 13 files changed, 244 insertions(+), 278 deletions(-) delete mode 100644 source/examples/testing/cigar_party.py delete mode 100644 source/examples/testing/test_cigar_party.py create mode 100644 source/examples/testing/test_walnut_party.py create mode 100644 source/examples/testing/walnut_party.py delete mode 100755 source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py create mode 100755 source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py delete mode 100644 source/solutions/Lesson06/cigar_party.py delete mode 100644 source/solutions/Lesson06/test_cigar_party.py create mode 100644 source/solutions/Lesson06/test_walnut_party.py create mode 100644 source/solutions/Lesson06/walnut_party.py delete mode 100755 source/solutions/codingbat/Logic-1/cigar_party.py diff --git a/source/examples/testing/cigar_party.py b/source/examples/testing/cigar_party.py deleted file mode 100644 index e6863f46..00000000 --- a/source/examples/testing/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(cigars, is_weekend): - pass diff --git a/source/examples/testing/test_cigar_party.py b/source/examples/testing/test_cigar_party.py deleted file mode 100644 index 260d5f47..00000000 --- a/source/examples/testing/test_cigar_party.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - assert cigar_party(50, False) is True - - -def test_3(): - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False diff --git a/source/examples/testing/test_walnut_party.py b/source/examples/testing/test_walnut_party.py new file mode 100644 index 00000000..62f226dc --- /dev/null +++ b/source/examples/testing/test_walnut_party.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +""" +test code for the walnut party example + +Adapted from the "coding bat" site: https://codingbat.com/python + +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + assert walnut_party(50, False) is True + + +def test_3(): + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/examples/testing/walnut_party.py b/source/examples/testing/walnut_party.py new file mode 100644 index 00000000..9b195b28 --- /dev/null +++ b/source/examples/testing/walnut_party.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(walnuts, is_weekend): + pass diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index 72ba4057..0398156e 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -19,13 +19,13 @@ Test Driven Development Download this module: -:download:`cigar_party.py ` +:download:`walnut_party.py ` -(This is the `"cigar party" `_ problem from the codingbat site) +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) and this test file: -:download:`test_cigar_party.py ` +:download:`test_walnut_party.py ` Put them in the same directory, and make that directory your working directory. @@ -33,7 +33,7 @@ Then try running the test file with pytest: .. code-block:: bash - $ pytest test_cigar_party + $ pytest test_walnut_party What you've done here is the first step in what is called: @@ -50,15 +50,16 @@ Test Driven development Open: -``test_cigar_party.py`` +``test_walnut_party.py`` and: -``cigar_party.py`` +``walnut_party.py`` In your editor. -Now edit ``cigar_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. +Now edit ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. + Doing your own: --------------- @@ -69,16 +70,23 @@ Pick another example from codingbat: Do a bit of test-driven development on it: - * run something on the web site. - * write a few tests using the examples from the site. +* run something on the web site. +* write a few tests using the examples from the site. +* then write the function, and fix it 'till it passes the tests. -These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests, +These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, with ``test_`` prepended. - * then write the function, and fix it 'till it passes the tests. +.. note:: + Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions. + One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use `naming conventions `_ to find your test files. + If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested: + ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules. + Do at least two of these to get the hang of the process. Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? + Give it a shot -- you'll know if it still works if the tests still pass! diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index 7a37ae3b..6f9b0cfa 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -38,11 +38,9 @@ The original testing system in Python. ``import unittest`` -More or less a port of ``JUnit`` from Java +More or less a port of `JUnit `_ from Java -A bit verbose: you have to write classes & methods - -(And we haven't covered that yet!) +A bit verbose: you have to write classes & methods (And we haven't covered that yet!) But here's a bit of an introduction, as you will see this in others' code. @@ -108,32 +106,31 @@ in ``test_my_mod.py``: Advantages of ``unittest`` -------------------------- +The ``unittest`` module is pretty full featured - The ``unittest`` module is pretty full featured - - It comes with the standard Python distribution, no installation required. +It comes with the standard Python distribution, no installation required. - It provides a wide variety of assertions for testing all sorts of situations. +It provides a wide variety of assertions for testing all sorts of situations. - It allows for a setup and tear down workflow both before and after all tests and before and after each test. +It allows for a setup and tear down workflow both before and after all tests and before and after each test. - It's well known and well understood. +It's well known and well understood. Disadvantages of ``unittest`` ----------------------------- - It's Object Oriented, and quite "heavyweight". +It's Object Oriented, and quite "heavyweight". - - modeled after Java's ``JUnit`` and it shows... + - modeled after Java's ``JUnit`` and it shows... - It uses the framework design pattern, so knowing how to use the features means learning what to override. +It uses the framework design pattern, so knowing how to use the features means learning what to override. - Needing to override means you have to be cautious. +Needing to override means you have to be cautious. - Test discovery is both inflexible and brittle. +Test discovery is both inflexible and brittle. - And there is no built-in parameterized testing. +And there is no built-in parameterized testing. Other Options @@ -252,13 +249,13 @@ Test Driven Development Download these files, and save them in your own students directory in the class repo: -:download:`test_cigar_party.py <../examples/testing/test_cigar_party.py>` +:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>` and: -:download:`cigar_party.py <../examples/testing/cigar_party.py>` +:download:`walnut_party.py <../examples/testing/walnut_party.py>` then, in dir where you put the files, run:: - $ pytest test_cigar_party.py + $ pytest test_walnut_party.py You will get a LOT of test failures! @@ -272,16 +269,16 @@ A bunch of tests exist, but the code to make them pass does not yet exist. The red you see in the terminal when we run the tests is a goad to you to write the code that fixes these tests. -The tests all failed because ``cigar_party()`` looks like: +The tests all failed because ``walnut_party()`` looks like: .. code-block:: python - def cigar_party(cigars, is_weekend): + def walnut_party(walnuts, is_weekend): pass A totally do nothing function! -Put real code in ``cigar_party.py`` until all the tests pass. +Put real code in ``walnut_party.py`` until all the tests pass. When the tests pass -- you are done! That's the beauty of test-driven development. diff --git a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py b/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py deleted file mode 100755 index ad7df22c..00000000 --- a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - - -def cigar_party(cigars, is_weekend): - """ - basic solution - """ - if is_weekend and cigars >= 40: - return True - elif 40 <= cigars <= 60: - return True - return False - - -def cigar_party2(cigars, is_weekend): - """ - some direct return of bool result - """ - if is_weekend: - return (cigars >= 40) - return (cigars >= 40 and cigars <= 60) - - -def cigar_party3(cigars, is_weekend): - """ - conditional expression - """ - return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60) - -if __name__ == "__main__": - # some tests - - assert cigar_party(30, False) is False - assert cigar_party(50, False) is True - assert cigar_party(70, True) is True - assert cigar_party(30, True) is False - assert cigar_party(50, True) is True - assert cigar_party(60, False) is True - assert cigar_party(61, False) is False - assert cigar_party(40, False) is True - assert cigar_party(39, False) is False - assert cigar_party(40, True) is True - assert cigar_party(39, True) is False - - print("All tests passed") diff --git a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py new file mode 100755 index 00000000..92d1ff28 --- /dev/null +++ b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +""" +adapted from coding bat: https://codingbat.com/python +""" + + +def walnut_party(walnuts, is_weekend): + """ + basic solution + """ + if is_weekend and walnuts >= 40: + return True + elif 40 <= walnuts <= 60: + return True + return False + + +def walnut_party2(walnuts, is_weekend): + """ + Direct return of bool result + """ + if is_weekend: + return (walnuts >= 40) + return (walnuts >= 40 and walnuts <= 60) + + +def walnut_party3(walnuts, is_weekend): + """ + Conditional expression + """ + return (walnuts >= 40) if is_weekend else (walnuts >= 40 and walnuts <= 60) + +if __name__ == "__main__": + # some tests + + assert walnut_party(30, False) is False + assert walnut_party(50, False) is True + assert walnut_party(70, True) is True + assert walnut_party(30, True) is False + assert walnut_party(50, True) is True + assert walnut_party(60, False) is True + assert walnut_party(61, False) is False + assert walnut_party(40, False) is True + assert walnut_party(39, False) is False + assert walnut_party(40, True) is True + assert walnut_party(39, True) is False + + print("All tests passed") diff --git a/source/solutions/Lesson06/cigar_party.py b/source/solutions/Lesson06/cigar_party.py deleted file mode 100644 index 992d99bd..00000000 --- a/source/solutions/Lesson06/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(num, weekend): - return num >= 40 and (num <= 60 or weekend) - diff --git a/source/solutions/Lesson06/test_cigar_party.py b/source/solutions/Lesson06/test_cigar_party.py deleted file mode 100644 index 80bc0920..00000000 --- a/source/solutions/Lesson06/test_cigar_party.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - - assert cigar_party(50, False) is True - - -def test_3(): - - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False - diff --git a/source/solutions/Lesson06/test_walnut_party.py b/source/solutions/Lesson06/test_walnut_party.py new file mode 100644 index 00000000..5bd7ae45 --- /dev/null +++ b/source/solutions/Lesson06/test_walnut_party.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + + assert walnut_party(50, False) is True + + +def test_3(): + + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/solutions/Lesson06/walnut_party.py b/source/solutions/Lesson06/walnut_party.py new file mode 100644 index 00000000..781f71be --- /dev/null +++ b/source/solutions/Lesson06/walnut_party.py @@ -0,0 +1,14 @@ + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(num, weekend): + return num >= 40 and (num <= 60 or weekend) diff --git a/source/solutions/codingbat/Logic-1/cigar_party.py b/source/solutions/codingbat/Logic-1/cigar_party.py deleted file mode 100755 index ad7df22c..00000000 --- a/source/solutions/codingbat/Logic-1/cigar_party.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - - -def cigar_party(cigars, is_weekend): - """ - basic solution - """ - if is_weekend and cigars >= 40: - return True - elif 40 <= cigars <= 60: - return True - return False - - -def cigar_party2(cigars, is_weekend): - """ - some direct return of bool result - """ - if is_weekend: - return (cigars >= 40) - return (cigars >= 40 and cigars <= 60) - - -def cigar_party3(cigars, is_weekend): - """ - conditional expression - """ - return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60) - -if __name__ == "__main__": - # some tests - - assert cigar_party(30, False) is False - assert cigar_party(50, False) is True - assert cigar_party(70, True) is True - assert cigar_party(30, True) is False - assert cigar_party(50, True) is True - assert cigar_party(60, False) is True - assert cigar_party(61, False) is False - assert cigar_party(40, False) is True - assert cigar_party(39, False) is False - assert cigar_party(40, True) is True - assert cigar_party(39, True) is False - - print("All tests passed") From f491b17e76607942c63e3d9e24dae7a89797e26b Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 18 Feb 2019 10:16:32 -0800 Subject: [PATCH 2/6] added a .gitignore so output files would not be tracked --- source/solutions/Lesson06/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 source/solutions/Lesson06/.gitignore diff --git a/source/solutions/Lesson06/.gitignore b/source/solutions/Lesson06/.gitignore new file mode 100644 index 00000000..8035ab50 --- /dev/null +++ b/source/solutions/Lesson06/.gitignore @@ -0,0 +1,3 @@ +*.txt + + From 100dcd6a0139f98e10047311e559ac20a21e8a1b Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 18 Feb 2019 11:39:09 -0800 Subject: [PATCH 3/6] adding a bit more to the testing module --- source/examples/testing/test_random_pytest.py | 31 +++- .../examples/testing/test_random_unitest.py | 4 +- source/exercises/unit_testing.rst | 26 +++- source/modules/Testing.rst | 144 +++++++++++++----- 4 files changed, 152 insertions(+), 53 deletions(-) diff --git a/source/examples/testing/test_random_pytest.py b/source/examples/testing/test_random_pytest.py index 6250d1b4..160c180e 100644 --- a/source/examples/testing/test_random_pytest.py +++ b/source/examples/testing/test_random_pytest.py @@ -8,32 +8,47 @@ import pytest -seq = list(range(10)) +example_seq = list(range(10)) def test_shuffle(): - # make sure the shuffled sequence does not lose any elements + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) random.shuffle(seq) - seq.sort() # IFyou comment this out, it will fail, so you can see output + seq.sort() # If you comment this out, it will fail, so you can see output print("seq:", seq) # only see output if it fails assert seq == list(range(10)) def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ with pytest.raises(TypeError): random.shuffle((1, 2, 3)) def test_choice(): - element = random.choice(seq) - assert (element in seq) + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) def test_sample(): - for element in random.sample(seq, 5): - assert element in seq + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ with pytest.raises(ValueError): - random.sample(seq, 20) + random.sample(example_seq, 20) diff --git a/source/examples/testing/test_random_unitest.py b/source/examples/testing/test_random_unitest.py index f825be5b..b8a1b712 100644 --- a/source/examples/testing/test_random_unitest.py +++ b/source/examples/testing/test_random_unitest.py @@ -8,7 +8,9 @@ def setUp(self): self.seq = list(range(10)) def test_shuffle(self): - # make sure the shuffled sequence does not lose any elements + """ + make sure the shuffled sequence does not lose any elements + """ random.shuffle(self.seq) self.seq.sort() self.assertEqual(self.seq, list(range(10))) diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index 0398156e..23255070 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -11,7 +11,29 @@ In order to do unit testing, you need a framework in which to write and run your Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact a kind of unit test. But as you build larger systems, you'll want a more structured way to write and run your tests. +We will use the pytest testing system for this class. +If you have not already done so -- install pytest like so: + +.. code-block:: bash + + $ python3 -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/DrMartins, inifile: + plugins: cov-2.6.0 + collected 0 items + + ========================= no tests ran in 0.01 seconds ========================= + +If you already HAVE some tests -- you may see somethign different! Test Driven Development @@ -33,7 +55,7 @@ Then try running the test file with pytest: .. code-block:: bash - $ pytest test_walnut_party + $ pytest test_walnut_party.py What you've done here is the first step in what is called: @@ -43,7 +65,7 @@ A bunch of tests exist, but the code to make them pass does not yet exist. The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. -Let's do that next! +Do that next! Test Driven development ----------------------- diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index 6f9b0cfa..c04029ca 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -138,22 +138,24 @@ Other Options There are several other options for running tests in Python. -* `Nose`: https://nose.readthedocs.org/ +* **Nose2**: https://github.com/nose-devs/nose2 -* `pytest`: http://pytest.org/latest/ +* **pytest**: http://pytest.org/latest/ * ... (many frameworks supply their own test runners: e.g. django) -Nose was the most common test runner when I first started learning testing, but it has been in maintaince mode for a while. +Nose was the most common test runner when I first started learning testing, but it has been in maintenance mode for a while. Even the nose2 site recommends that you consider pytest. pytest has become the defacto standard test runner for those that want a more "pythonic" test framework. -It is very capable and widely used. +pytest is very capable and widely used. For a great description of the strengths of pytest, see: `The Cleaning Hand of Pytest `_ +So we will use pytest for the rest of this class. + Installing ``pytest`` --------------------- @@ -170,7 +172,7 @@ at the command line: $ pytest -If you have any tests in your repository, that will find and run them. +If you have any tests in your repository, that command will find and run them (If you have followed the proper naming conventions). **Do you?** @@ -179,12 +181,18 @@ Pre-existing Tests Let's take a look at some examples. -in ``/examples/testing`` +Create a directory to try this out, and download: + +:download:`test_random_unitest.py <../examples/testing/test_random_unitest.py>` + +In the directory you created for that file, run: .. code-block:: bash $ pytest +It should find that test file and run it. + You can also run pytest on a particular test file: .. code-block:: bash @@ -202,17 +210,20 @@ Take a few minutes to look these files over. What is Happening Here? ----------------------- -You should have gotten results that look something like this:: +You should have gotten results that look something like this: - MacBook-Pro:Session06 Chris$ pytest test_random_unitest.py +.. code-block:: bash + + $ pytest ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 - rootdir: /Users/Chris/PythonStuff/UWPCE/IntroPython-2017/examples/Session06, inifile: + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 collected 3 items - test_random_unitest.py ... + test_random_unitest.py ... [100%] - =========================== 3 passed in 0.02 seconds =========================== + =========================== 3 passed in 0.06 seconds =========================== When you run the ``pytest`` command, ``pytest`` starts in your current @@ -243,60 +254,109 @@ It will run ``unittest`` tests for you, so can be used as a test runner. But in addition to finding and running tests, it makes writing tests simple, and provides a bunch of nifty utilities to support more complex testing. +Now download this file: -Test Driven Development ------------------------ +:download:`test_random_pytest.py <../examples/testing/test_random_pytest.py>` -Download these files, and save them in your own students directory in the class repo: +And run pytest again: -:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>` -and: -:download:`walnut_party.py <../examples/testing/walnut_party.py>` +.. code-block:: bash -then, in dir where you put the files, run:: + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 + collected 8 items - $ pytest test_walnut_party.py + test_random_pytest.py ..... [ 62%] + test_random_unitest.py ... [100%] -You will get a LOT of test failures! + =========================== 8 passed in 0.07 seconds =========================== -What we've just done here is the first step in what is called: +Note that it ran the tests in both the test files. - **Test Driven Development**. +Take a look at ``test_random_pytest.py`` -- It is essentially the same tests -- but written in native pytest style -- simple test functions. -The idea is that you write the tests first, and then write the code that passes the tests. In this case, the writing the tests part has been done for you: +pytest tests +------------ -A bunch of tests exist, but the code to make them pass does not yet exist. +The beauty of pytest is that it takes advantage of Python's dynamic nature -- you don't need to use any particular structure to write tests. -The red you see in the terminal when we run the tests is a goad to you to write the code that fixes these tests. +Any function named appropriately is a test. -The tests all failed because ``walnut_party()`` looks like: +If the function doesn't raise an error or an assertion, the test passes. It's that simple. + +Let's take a look at ``test_random_pytest.py`` to see how this works. .. code-block:: python - def walnut_party(walnuts, is_weekend): - pass + import random + import pytest -A totally do nothing function! +The ``random module is imported becasue that's what we are testing``. +``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment. -Put real code in ``walnut_party.py`` until all the tests pass. +.. code-block:: python -When the tests pass -- you are done! That's the beauty of test-driven development. + seq = list(range(10)) -Trying it yourself ------------------- +Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it. + +Now the first test -- simply by naming it ``test_something``, pytest will run it as a test: + +.. code-block:: python -Try it a bit more, writing the tests yourself: + def test_shuffle(): + """ + Make sure the shuffled sequence does not lose any elements + """ + seq2 = seq[:] # make a copy so the main one won't get changed + seq2.sort() + random.shuffle(seq2) + seq2.sort() # If you comment this out, it will fail, so you can see output + print("seq2:", seq2) # only see output if it fails + assert seq2 == list(range(10)) -Pick an example from codingbat: +This test is designed to make sure that random.shuffle only re-arranges the items, but doesn't add or lose any. +First a copy of the global sequence is made -- you want to make sure that tests don't change the status of anything global. - `codingbat `_ -Do a bit of test-driven development on it: - * run something on the web site. - * write a few tests using the examples from the site. - * then write the function, and fix it 'till it passes the tests. -Do at least two of these... + def test_shuffle_immutable(): + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + + + def test_choice(): + element = random.choice(seq) + assert (element in seq) + + + def test_sample(): + for element in random.sample(seq, 5): + assert element in seq + + + def test_sample_too_large(): + with pytest.raises(ValueError): + random.sample(seq, 20) + + + + + + + + +Test Driven Development +----------------------- + +Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code. + +This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future will assurance that you haven't broken it. +Give this exercise a try to get the idea: +:ref:`exercise_unit_testing` From 62b93a29b4dcbc5aa105485f2bf7be97662b7ab0 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 18 Feb 2019 13:59:38 -0800 Subject: [PATCH 4/6] fleshing out the unit testing module a bit --- source/examples/testing/test_random_pytest.py | 35 ++--- source/exercises/unit_testing.rst | 45 +++--- source/modules/Testing.rst | 147 ++++++++++++++---- 3 files changed, 165 insertions(+), 62 deletions(-) diff --git a/source/examples/testing/test_random_pytest.py b/source/examples/testing/test_random_pytest.py index 160c180e..b9a65afd 100644 --- a/source/examples/testing/test_random_pytest.py +++ b/source/examples/testing/test_random_pytest.py @@ -11,13 +11,29 @@ example_seq = list(range(10)) +def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + + +def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + + def test_shuffle(): """ Make sure a shuffled sequence does not lose any elements """ seq = list(range(10)) random.shuffle(seq) - seq.sort() # If you comment this out, it will fail, so you can see output + # seq.sort() # If you comment this out, it will fail, so you can see output print("seq:", seq) # only see output if it fails assert seq == list(range(10)) @@ -29,23 +45,6 @@ def test_shuffle_immutable(): with pytest.raises(TypeError): random.shuffle((1, 2, 3)) - -def test_choice(): - """ - A choice selected should be in the sequence - """ - element = random.choice(example_seq) - assert (element in example_seq) - - -def test_sample(): - """ - All the items in a sample should be in the sequence - """ - for element in random.sample(example_seq, 5): - assert element in example_seq - - def test_sample_too_large(): """ Trying to sample more than exist should raise an error diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index 23255070..b71653dc 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -8,7 +8,7 @@ Preparation ----------- In order to do unit testing, you need a framework in which to write and run your tests. -Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact a kind of unit test. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact, a kind of unit test. But as you build larger systems, you'll want a more structured way to write and run your tests. We will use the pytest testing system for this class. @@ -33,42 +33,50 @@ at the command line: ========================= no tests ran in 0.01 seconds ========================= -If you already HAVE some tests -- you may see somethign different! +If you already HAVE some tests -- you may see something different! Test Driven Development ----------------------- -Download this module: +Download these files, and save them in your own students directory in the class repo: -:download:`walnut_party.py ` +:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>` -(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) - -and this test file: +and: -:download:`test_walnut_party.py ` +:download:`walnut_party.py <../examples/testing/walnut_party.py>` -Put them in the same directory, and make that directory your working directory. +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) -Then try running the test file with pytest: +In the directory where you put the files, run: .. code-block:: bash $ pytest test_walnut_party.py +You will get a LOT of test failures! + What you've done here is the first step in what is called: - **Test Driven Development**. + **Test Driven Development** A bunch of tests exist, but the code to make them pass does not yet exist. The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. -Do that next! +The tests all failed because currently ``walnut_party()`` looks like: -Test Driven development ------------------------ +.. code-block:: python + + def walnut_party(walnuts, is_weekend): + pass + +A totally do nothing function. + + +Making tests pass +----------------- Open: @@ -80,8 +88,9 @@ and: In your editor. -Now edit ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. +Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. +When the tests pass -- you are done! That's the beauty of test-driven development. Doing your own: --------------- @@ -92,9 +101,9 @@ Pick another example from codingbat: Do a bit of test-driven development on it: -* run something on the web site. -* write a few tests using the examples from the site. -* then write the function, and fix it 'till it passes the tests. +* Run something on the web site. +* Write a few tests using the examples from the site. +* Then write the function, and fix it 'till it passes the tests. These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, with ``test_`` prepended. diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index c04029ca..f9baa7e6 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -26,9 +26,9 @@ block. * You can't do anything else when the file is executed without running tests. - This is not optimal. +This is not optimal. - Python provides testing systems to help. +Python provides testing systems to help. Standard Library: ``unittest`` @@ -294,7 +294,7 @@ Let's take a look at ``test_random_pytest.py`` to see how this works. import random import pytest -The ``random module is imported becasue that's what we are testing``. +The ``random`` module is imported becasue that's what we are testing. ``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment. .. code-block:: python @@ -303,51 +303,146 @@ The ``random module is imported becasue that's what we are testing``. Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it. -Now the first test -- simply by naming it ``test_something``, pytest will run it as a test: +Now the first tests -- simply by naming it ``test_something``, pytest will run it as a test: + +.. code-block:: python + + def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + +This is pretty straightforward. We make a random choice from the sequence, +and then assert that the selected element is, indeed, in the original sequence. + +.. code-block:: python + + def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + +And this is pretty much the same thing, except that it loops to make sure that every item returned by ``.sample`` is in the original sequence. + +Note that this will result in 5 separate assertions -- that is fine, you can have as many assertions as you like in one test function. But the test will fail on the first failed assertion -- so you only want to have closely related assertions in each test function. .. code-block:: python def test_shuffle(): """ - Make sure the shuffled sequence does not lose any elements + Make sure a shuffled sequence does not lose any elements """ - seq2 = seq[:] # make a copy so the main one won't get changed - seq2.sort() - random.shuffle(seq2) - seq2.sort() # If you comment this out, it will fail, so you can see output - print("seq2:", seq2) # only see output if it fails - assert seq2 == list(range(10)) + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) -This test is designed to make sure that random.shuffle only re-arranges the items, but doesn't add or lose any. -First a copy of the global sequence is made -- you want to make sure that tests don't change the status of anything global. +This test is designed to make sure that ``random.shuffle`` only re-arranges the items, but doesn't add or lose any. +In this case, the global ``example_seq`` isn't used, because ``shuffle()`` will change the sequence -- tests should never rely on or alter global state. So a new sequence is created for the test. This also allows the test to know exactly what the results should be at the end. +Then the "real work" -- calling ``random.shuffle`` on the sequence -- this should re-arrange the elements without adding or losing any. +Calling ``.sort()`` again should put the elements back in the order they started - def test_shuffle_immutable(): - with pytest.raises(TypeError): - random.shuffle((1, 2, 3)) +So we can then test that after shuffling and re-sorting, we have the same sequence back: +.. code-block:: python - def test_choice(): - element = random.choice(seq) - assert (element in seq) + assert seq == list(range(10)) +If that assertion passes, the test will pass. - def test_sample(): - for element in random.sample(seq, 5): - assert element in seq +``print()`` and test failures +............................. +Try commenting out the sort line: - def test_sample_too_large(): - with pytest.raises(ValueError): - random.sample(seq, 20) +.. code-block:: python + + # seq.sort() # If you comment this out, it will fail, so you can see output + +And run again to see what happens. This is what I got: + +.. code-block:: bash + $ pytest test_random_pytest.py + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/PythonStuff/UWPCE/PythonCertDevel/source/examples/testing, inifile: + plugins: cov-2.6.0 + collected 5 items + + test_random_pytest.py F.... [100%] + + =================================== FAILURES =================================== + _________________________________ test_shuffle _________________________________ + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + # seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + > assert seq == list(range(10)) + E assert [4, 8, 9, 3, 2, 0, ...] == [0, 1, 2, 3, 4, 5, ...] + E At index 0 diff: 4 != 0 + E Use -v to get the full diff + + test_random_pytest.py:22: AssertionError + ----------------------------- Captured stdout call ----------------------------- + seq: [4, 8, 9, 3, 2, 0, 7, 5, 6, 1] + ====================== 1 failed, 4 passed in 0.40 seconds ====================== + +You get a lot of information when test fails. It's usually enough to tell you what went wrong. + +Note that pytest didn't print out the results of the print statement when the test passed, but when it failed, it printed it (under "Captured stdout call"). This means you can put diagnostic print calls in your tests, and they will not clutter up the output when they are not needed. + +Testing for Exceptions +...................... + +One of the things you might want to test about your code is that it raises an exception when it should -- and that the exception it raises is the correct one. + +In this example, if you try to call ``random.shuffle`` with an immutable sequence, such as a tuple, it should raise a ``TypeError``. Since raising an exception will generally stop the code (and cause a test to fail), we can't use an assertion to test for this. + +pytest provides a "context manager", ``pytest.raises``, that can be used to test for exceptions. The test will pass if and only if the specified Exception is raised by the enclosed code. You use it like so: + +.. code-block:: python + + def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) +The ``with`` block is how you use a context manager -- it will run the code enclosed, and perform various actions at the end of the code, or when an exception is raised. +This is the same ``with`` as used to open files. In that case, it is used to assure that the file is properly closed when you are done with it. In this case, the ``pytest.raises`` context manager captures any exceptions, and raises an ``AssertionError`` if no exception is raised, or if the wrong exception is raised. +In this case, the test will only pass if a ``TypeError`` is raised by the call to ``random.shuffle`` with a tuple as an argument. +The next test: + +.. code-block:: python + + def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) +is very similar, except that this time, a ValueError has to be raised for the test to pass. +pytest provides a number of other features for fixtures, parameterized tests, test classes, configuration, shared resources, etc. +But simple test functions like this will get you very far. Test Driven Development @@ -355,7 +450,7 @@ Test Driven Development Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code. -This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future will assurance that you haven't broken it. +This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future with assurance that you haven't broken it. Give this exercise a try to get the idea: From a749b4228c74583aa5372c9d676ba2fc7a87b82e Mon Sep 17 00:00:00 2001 From: "Christopher H.Barker, PhD" Date: Tue, 19 Feb 2019 18:35:06 -0800 Subject: [PATCH 5/6] fix indentation issue --- source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py index 92d1ff28..3bdae1d8 100755 --- a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py +++ b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py @@ -10,7 +10,7 @@ def walnut_party(walnuts, is_weekend): basic solution """ if is_weekend and walnuts >= 40: - return True + return True elif 40 <= walnuts <= 60: return True return False From 7ac2595ba6a5256eaf75ca3cd8c6acb835cdac69 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Tue, 2 Apr 2019 22:18:01 -0700 Subject: [PATCH 6/6] added a reference and a bit more on comprehensions --- source/modules/Comprehensions.rst | 45 ++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index a594c669..19408c2c 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -88,7 +88,40 @@ Comprehensions and map() Comprehensions are another way of expressing the "map" pattern from functional programming. -Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. +Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. IF youare not familiar with ``map()``, you can saftly skip this, but if you are: + +.. code-block:: python + + map(a_function, an_iterable) + +is the same as: + +.. code-block:: python + + [a_function(item), for item in an_iterable] + +In this case, the comprehension is a tad wordier than ``map()``. BUt comprehensions really shine when you do'nt already have a handy function to pass to map: + +.. code-block:: python + + [x**2 for x in an_iterable] + +To use ``map()``, you need a function: + +.. code-block:: python + + def square(x): + return x**2 + + map(square, an_iterable) + +There are shortcuts of course, including ``lambda`` (stay tuned for more about that): + +.. code-block:: python + + map(lambda x: x**2, an_iterable) + +But is that easier to read or write? What about filter? @@ -130,6 +163,8 @@ This is expressing the "filter" pattern and the "map" pattern at the same time - Get creative.... +How do I see all the built in Exceptions? + .. code-block:: python [name for name in dir(__builtin__) if "Error" in name] @@ -431,3 +466,11 @@ If you are going to immediately loop through the items created by the comprehens The "official" term is "generator expression" -- that is what you will see in the Python docs, and a lot of online discussions. I've used the term "generator comprehension" here to better make clear the association with list comprehensions. +References +---------- + +This is a nice intro to comprehensions from Trey Hunner: + +https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ + +Trey writes a lot of good stuff -- I recommned browsing his site.