diff --git a/README.md b/README.md index 5990e18..e88ed8a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This tutorial helps you to learn automated testing in Python 3 using the `pytest *Captain Ahab was vicious because Moby Dick, the white whale, had bitten off his leg. So the captain set sail for a hunt. For months he was searching the sea for the white whale. The captain finally attacked the whale with a harpoon. Unimpressed, the whale devoured captain, crew and ship. The whale won.* -![tick marks while counting words](../images/counting470.png "Counting words") +![tick marks while counting words](images/counting470.png "Counting words") Herman Melville's book *“Moby Dick”* describes the epic fight between the captain of a whaling ship and a whale. In the book, the whale wins by eating most of the other characters. **But does he also win by being mentioned more often?** @@ -19,13 +19,19 @@ Herman Melville's book *“Moby Dick”* describes the epic fight between the ca clone the repository: - :::bash - git clone https://github.com/krother/python_testing_tutorial.git +```bash +git clone https://github.com/krother/python_testing_tutorial.git +``` install **pytest**: - :::bash - pip install pytest +```bash +pip install pytest +``` + +## Getting Started + +Start with the first exercises in the chapter [Unit Tests](articles/unit_tests.md)! ## Chapters @@ -65,4 +71,4 @@ Attribution License 4.0. ## Contributors -Kristian Rother, Magdalena Rother, Daniel Szoska +Kristian Rother, Magdalena Rother, Daniel Szoska, Malte Bonart diff --git a/articles/challenges.md b/articles/challenges.md deleted file mode 100644 index 24a0a40..0000000 --- a/articles/challenges.md +++ /dev/null @@ -1,53 +0,0 @@ - -# Challenges - -## 1. Unit Tests - -### 1.1 Test a Python function -The function **main()** in the module **word_counter.py** calculates the number of words in a text body. - -For instance, the following sentence contains **three** words: - - Call me Ishmael - -Your task is to prove that the **main()** function calculates the number of words in the sentence correctly with **three**. - -Use the example test in **test_1_1_unit_test.py**. - -### 1.2 Test proves if code is broken -The test in the module **test_failing_code.py** fails, because there is a bug in the function **word_counter.average_word_length()**. In the sentence - - Call me Ishmael - -The words are **four, two,** and **seven** characters long. This gives an average of: - - >>> (4 + 2 + 7) / 3.0 - 4.333333333333333 - -Your task is to fix the code, so that the test passes. - -Use the example in **test_1_2_broken_code.py**. - -### 1.3 Code proves if tests are broken -The test in the module **test_failing_test.py** fails, because there is a bug in the test file. - -Your task is to fix the test, so that the test passes. Use the example in **test_1_3_broken_test.py**. - - -### 1.4 Test border cases -High quality tests cover many different situations. The most common situations for the program **word_counter.py** include: - -| test case | description | example input | expected output -|-----------|-------------|---------------|----------------- -| empty | input is valid, but empty | "" | 0 -| minimal | smallest reasonable input | "whale" | 1 -| typical | representative input | "whale eats captain" | 3 -| invalid | input is supposed to fail | 777 | *Exception raised* -| maximum | largest reasonable input | *Melville's entire book* | *more than 200000* -| sanity | program recycles its own output | *TextBody A created from another TextBody B* | *A equals B* -| nasty | difficult example | "That #~&%* program still doesn't work!" | 6 - -Your task is to make all tests in **test_1_4_border_cases.py** pass. - - ----- diff --git a/articles/fixtures.md b/articles/fixtures.md index 8b51b35..cd4441c 100644 --- a/articles/fixtures.md +++ b/articles/fixtures.md @@ -13,12 +13,13 @@ There, add a function that loads the file `data/mobydick_summary.txt`: Place the decorator `@pytest.fixture` on top of it: - :::python3 - import pytest +```python +import pytest - @pytest.fixture - def text_summary(): - return open(...).read() +@pytest.fixture +def text_summary(): + return open(...).read() +``` ---- @@ -26,9 +27,10 @@ Place the decorator `@pytest.fixture` on top of it: Now create a module `test_corpus.py` with a function that uses the fixture: - :::python3 - def test_short_sample(text_summary): - assert count_words(text_summary) == 77 +```python +def test_short_sample(text_summary): + assert count_words(text_summary) == 77 +``` Execute the module with `pytest`. Note that you **do not** need to import `conftest`. Pytest does that automatically. @@ -44,11 +46,12 @@ Create a fixture for the full text of the book `mobydick_full.txt` as well. Create a fixture in `conftest.py` that prepares a dictionary with word counts using the `word_counter.count_words_dict()` function. - :::python3 - from word_counter import count_words_dict +```python +from word_counter import count_words_dict - @pytest.fixture - def count_dict(text_summary): - return ... +@pytest.fixture +def count_dict(text_summary): + return ... +``` Write a simple test that makes sure the dictionary is not empty. diff --git a/articles/mock_objects.md b/articles/mock_objects.md deleted file mode 100644 index 428b9b2..0000000 --- a/articles/mock_objects.md +++ /dev/null @@ -1,8 +0,0 @@ - -# Mock Objects - -### Exercise 1: Using a Mock Object - -The function **word_report.get_top_words()** requires an instance of the class **TextBody**. You need to test the function, excluding the possibility that the **TextBody** class is buggy. To do so, you need to replace the class by a **Mock Object**, a simple placeholder. - -Your task is to write a test for the function **word_counter.get_top_words()** that does not use the class **TextBody**. diff --git a/articles/multiple_packages.md b/articles/multiple_packages.md deleted file mode 100644 index 479ea20..0000000 --- a/articles/multiple_packages.md +++ /dev/null @@ -1,20 +0,0 @@ -### Exercise 5: Import test data in multiple test packages -In a big software project, your tests are distributed to two packages. Both **test_first.py** and **test_second.py** require the variable **MOBYDICK_SUMMARY** from the module **test data.py**. The package structure is like this: - - testss/ - test_a/ - __init__.py - test_first.py - test_b/ - __init__.py - test_second.py - __init__.py - test_data.py - test_all.py - -Your task is to make sure that the variable **MOBYDICK_SUMMARY** is correctly imported to both test modules, so that the tests pass for all of: - - tests/test_a/test_first.py - tests/test_b/test_second.py - tests/test_all.py - diff --git a/articles/organizing_tests.md b/articles/organizing_tests.md index fd79c5b..7f3402b 100644 --- a/articles/organizing_tests.md +++ b/articles/organizing_tests.md @@ -9,11 +9,12 @@ Make sure the name of the class starts with the word `Test`. Indent your test functions so that they belong to the class. Add `self` as the first parameter of each function: - :::python3 - class TestDummy: +```python +class TestDummy: - def test_dummy(self): - assert ... + def test_dummy(self): + assert ... +``` ---- @@ -21,8 +22,9 @@ Add `self` as the first parameter of each function: Run all tests written so far by simply typing - :::bash - pytest +```bash +python -m pytest +``` ---- @@ -30,18 +32,21 @@ Run all tests written so far by simply typing Run only one test file: - :::bash - pytest FILE_NAME +```bash +python -m pytest FILE_NAME +``` Run only one test class: - :::bash - pytest FILE_NAME::CLASS_NAME +```bash +pytest -m pytest FILE_NAME::CLASS_NAME +``` Finally, run a single test: - :::bash - pytest FILE_NAME::CLASS_NAME::TEST_NAME +```bash +pytest -m pytest FILE_NAME::CLASS_NAME::TEST_NAME +``` ---- @@ -51,7 +56,8 @@ Find out which options of pytest do the following: *more verbose output | re-run failing tests | stop on first test that fails* - :::bash - pytest -lf - pytest -v - pytest -x +```bash +pytest --lf +pytest -v +pytest -x +``` diff --git a/articles/parameterized.md b/articles/parameterized.md index 72077fd..e0397cf 100644 --- a/articles/parameterized.md +++ b/articles/parameterized.md @@ -5,15 +5,16 @@ The tests in `test_parameterized.py` check a list of pairs (word, count) that apply to the text file `mobydick_summary.txt`: - :::python3 - PAIRS = [ - ('whale', 5), - ('goldfish', 0), - ('captain', 4), - ('white', 2), - ('jellyfish', 99), - ('harpoon', 1), - ] +```python +PAIRS = [ + ('whale', 5), + ('goldfish', 0), + ('captain', 4), + ('white', 2), + ('jellyfish', 99), + ('harpoon', 1), +] +``` Run the tests and see what happens. @@ -34,12 +35,13 @@ We will create six tests from the example data. Use the **test parametrization in pytest**. Change the test function by adding the following decorator: - :::python3 - import pytest +```python +import pytest - @pytest.mark.parametrize('word, number', PAIRS) - def test_count_words_dict(word, number): - ... +@pytest.mark.parametrize('word, number', PAIRS) +def test_count_words_dict(word, number): + ... +``` The two arguments will be filled in automatically. Now remove the `for` loop. diff --git a/articles/test_coverage.md b/articles/test_coverage.md index 0fc6f34..8016558 100644 --- a/articles/test_coverage.md +++ b/articles/test_coverage.md @@ -3,8 +3,9 @@ For the next exercises, you need to install a small plugin: - :::bash - pip install pytest-cov +```bash +pip install pytest-cov +``` ---- @@ -12,8 +13,9 @@ For the next exercises, you need to install a small plugin: Calculate the percentage of code covered by automatic tests: - :::bash - pytest --cov=. +```bash +python -m pytest --cov=. +``` Instead of the `.` you can insert the path you would like to see in the coverage report. @@ -24,8 +26,9 @@ Check whether any hidden files have appeared. ### Exercise 2: Identify uncovered lines Find out which lines are not covered by tests. Execute - :::bash - coverage html +```bash +python -m pytest --cov=. --cov-report term-missing +``` Open the resulting file `htmlcov/index.html` in a web browser. diff --git a/articles/unit_tests.md b/articles/unit_tests.md index 1fcf1f7..d2d9797 100644 --- a/articles/unit_tests.md +++ b/articles/unit_tests.md @@ -5,17 +5,19 @@ #### How many words are in the following sentence? - :::bash - Call me Ishmael. +```bash +Call me Ishmael. +``` ---- #### How many words are in the next sentence? - :::bash - "you haint no objections to sharing a harpooneer's blanket, - have ye? I s'pose you are goin' a-whalin', - so you'd better get used to that sort of thing." +```bash +"you haint no objections to sharing a harpooneer's blanket, +have ye? I s'pose you are goin' a-whalin', +so you'd better get used to that sort of thing." +``` ---- @@ -26,15 +28,17 @@ The function `count_words()` in the module **word_counter.py** calculates the nu For instance, we would expect the following input to result in a word count of `3`: - :::bash - Call me Ishmael +```bash +Call me Ishmael +``` Your task is to prove that the `count_words()` function in fact returns `3`. Run the example test in `test_unit_test.py` with - :::bash - pytest test_unit_test.py +```bash +python -m pytest test/test_unit_test.py +``` ---- diff --git a/cover.png b/cover.png deleted file mode 100644 index b5f213e..0000000 Binary files a/cover.png and /dev/null differ diff --git a/cover.jpg b/images/cover.jpg similarity index 100% rename from cover.jpg rename to images/cover.jpg diff --git a/images/cover.png b/images/cover.png index 30b0d4d..b5f213e 100644 Binary files a/images/cover.png and b/images/cover.png differ diff --git a/cover_small.jpg b/images/cover_small.jpg similarity index 100% rename from cover_small.jpg rename to images/cover_small.jpg diff --git a/mobydick/word_counter.py b/mobydick/word_counter.py new file mode 100644 index 0000000..7621529 --- /dev/null +++ b/mobydick/word_counter.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +""" +Code that is being tested +""" + +def split_to_words(text): + '''split a string into tokens''' + if type(text) != str: + raise TypeError('accepts only string input.') + words = text.split(' ') + return words + +def count_words(text): + '''count number of words in a text''' + words = split_to_words(text) + return len(words) + +def count_word(text, word): + '''count the frequency of a word in a text''' + words = split_to_words(text) + return words.count(word) + +def count_words_dict(text): + '''Returns a dictionary of word counts''' + words = split_to_words(text) + d = {} + + for word in words: + d.setdefault(word, 0) + d[word] += 1 + + return d diff --git a/test/test_broken.py b/test/test_broken.py index e16013b..7a53c7b 100644 --- a/test/test_broken.py +++ b/test/test_broken.py @@ -9,7 +9,7 @@ Find out which is which and fix both. """ -from word_counter import count_words +from mobydick.word_counter import count_words def test_count_words_tabs(): diff --git a/test/test_parameterized.py b/test/test_parameterized.py index f65cd88..e925eac 100644 --- a/test/test_parameterized.py +++ b/test/test_parameterized.py @@ -1,7 +1,6 @@ +from mobydick.word_counter import count_words_dict -from word_counter import count_words_dict - -MOBYDICK_SUMMARY = open('../data/mobydick_summary.txt').read() +MOBYDICK_SUMMARY = open('./data/mobydick_summary.txt').read() PAIRS = [ @@ -15,6 +14,6 @@ def test_count_words_dict(): - counts = count_words_dict(text) + counts = count_words_dict(MOBYDICK_SUMMARY) for word, number in PAIRS: assert counts[word] == number diff --git a/test/test_unit_test.py b/test/test_unit_test.py index 4cad767..b7638c4 100644 --- a/test/test_unit_test.py +++ b/test/test_unit_test.py @@ -2,9 +2,10 @@ Example of a Unit Test """ -from word_counter import count_words +from mobydick.word_counter import count_words + def test_count_words(): """Count words in a short sentence""" - n = count_words("Call me Ishmael") - assert n == 3 + text = "Call me Ishmael" + assert count_words(text) == 3 diff --git a/test/word_counter.py b/test/word_counter.py deleted file mode 100644 index af7a7d7..0000000 --- a/test/word_counter.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Code that is being tested -""" - -def count_words(text): - if type(text) != str: - raise TypeError('word counter accepts only string input.') - words = text.split(' ') - return len(words) - - -def count_words_dict(text, n): - '''Returns the n most frequent words.''' - d = {'dummy': 1} - ... - return d