2 Tests, one at a time

Read the tutorial. Build it yourself is the other way to read this part.

just setup

We cannot tell whether count still works after a change, unless we try it by hand each time. Tests try it for us. Each test gives count a text, and compares what it gives back with what we expect. We add the tests one file at a time, and run them after each. One test asks for something that count cannot do yet. It asks count to count "The" and "the" as one word.

The code before this part. The code after it.

2.1 A test of counting

Nothing checks yet that count gives the right counts. A test of unittest, the test library of Python, calls count on a short text and compares the result with the dictionary that it expects. The tests go in a new folder, tests.

The first file of tests, tests/test_count.py, has two tests. test_words counts a b a. test_case counts A a, and expects the one word a, twice. The code cannot do this yet.

A patch to tests/test_count.py.

In the change of tests/test_count.py, look at test_case. It is the test that fails below.

uv run python -m unittest discover -s tests -q

One test fails. test_case expects {'a': 2}, and gets {'A': 1, 'a': 1}. The failure is a question that the code has not answered yet.

What changed

  • tests/test_count.py: two tests. The second asks for something the code cannot do yet. New, 11 lines. The file.
    • look at self.assertEqual(count("A a"), {"a": 2})
The diff of tests/test_count.py
@@ -0,0 +1,11 @@
+import unittest
+
+from tally import count
+
+
+class Count(unittest.TestCase):
+    def test_words(self):
+        self.assertEqual(count("a b a"), {"a": 2, "b": 1})
+
+    def test_case(self):
+        self.assertEqual(count("A a"), {"a": 2})

This change on GitHub. The code after it.

2.2 A test of an empty text

An empty file is a text with no words. count must give an empty dictionary for it, and not fail. A test in a file of its own, tests/test_empty.py, says so, and keeps it true after later changes.

A patch to tests/test_empty.py.

The new file tests/test_empty.py holds one test, test_empty. It gives count the empty text "", and expects the empty dictionary {}.

uv run python -m unittest discover -s tests -q

The new test passes at once, because split() finds no words in an empty text. test_case still fails.

What changed

  • tests/test_empty.py: an empty text counts nothing. New, 8 lines. The file.
    • look at self.assertEqual(count(""), {})
The diff of tests/test_empty.py
@@ -0,0 +1,8 @@
+import unittest
+
+from tally import count
+
+
+class Empty(unittest.TestCase):
+    def test_empty(self):
+        self.assertEqual(count(""), {})

This change on GitHub. The code after it.

2.3 Case does not matter

test_case fails, because count counts A and a as two words. When count puts the text in lower case before it splits it, every word is counted in lower case. One word in the loop of count, lower(), does it.

A patch to src/tally/__init__.py.

In count, in src/tally/__init__.py, the loop now splits text.lower(), and not text. So A and a are the same word.

uv run python -m unittest discover -s tests -q
echo "The the" | uv run tally

All three tests pass, and the command prints 2 the for "The the".

What changed

  • src/tally/__init__.py: one word, lower(), answers the failing test. 1 line added, 1 removed. The file.
    • look at for word in text.lower().split():
The diff of src/tally/__init__.py
@@ -5,7 +5,7 @@ import sys
 
 def count(text):
     counts = {}
-    for word in text.split():
+    for word in text.lower().split():
         counts[word] = counts.get(word, 0) + 1
     return counts
 

This change on GitHub. The code after it.