diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c968750c9734841b69001a5775c27f5ff7f5777
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,32 @@
+# Test Design for `main.TriangleApp.classify`
+
+## Categories
+
+*   Type of triangle: invalid, equilateral, isosceles, right, scalene
+*   Sign of lengths: all negative, mixed, all positive
+*   Smallest side: `a`, `b`, `c`, tie
+*   Largest side: `a`, `b`, `c`, tie
+
+## Initial Test Frames (by CPT)
+
+*   `a=5,` `b=3,` `c=6` (scalene, all positive, `b` smallest, `c` largest)
+*   `a=8,` `b=2,` `c=5` (invalid, all positive, `b` smallest, `a` largest)
+*   `a=-2,` `b=-2,` `c=-5` (invalid, all negative, `c` smallest, tie for largest)
+*   `a=3,` `b=5,` `c=4` (right, all positive, `a` smallest, `b` largest)
+*   `a=1,` `b=1,` `c=1` (equilateral, all positive, tie for smallest, tie for largest)
+*   `a=2,` `b=2,` `c=3` (isosceles, all positive, tie for smallest, `c` largest)
+*   `a=2,` `b=2,` `c=-3` (invalid, mixed signs, `c` smallest, tie for largest)
+
+## Supplemental Test Frames (for 100% branch coverage)
+
+*   `a=0`, `b=1`, `c=2`
+*   `a=2`, `b=3`, `c=18`
+*   `a=3`, `b=4`, `c=5`
+
+## Supplemental Test Frames (by mutation testing)
+
+*   `a=…`, `b=…`, `c=…`
+*   `a=…`, `b=…`, `c=…`
+*   `a=…`, `b=…`, `c=…`
+*   `a=…`, `b=…`, `c=…`
+*   `a=…`, `b=…`, `c=…`
diff --git a/test_main.py b/test_main.py
new file mode 100644
index 0000000000000000000000000000000000000000..e09334bfcdc80a9c53e558897d3a1663628523e7
--- /dev/null
+++ b/test_main.py
@@ -0,0 +1,54 @@
+from unittest import TestCase
+from main import Classification, TriangleApp
+
+
+class TestTriangleApp(TestCase):
+    def test_classification_of_a_scalene_triangle(self):
+        actual = TriangleApp.classify(5, 3, 6)
+        expected = Classification.SCALENE
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_triangle_with_sides_too_short_to_be_valid(self):
+        actual = TriangleApp.classify(8, 2, 5)
+        expected = Classification.INVALID
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_triangle_with_all_negative_sides(self):
+        actual = TriangleApp.classify(-2, -2, -5)
+        expected = Classification.INVALID
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_right_triangle(self):
+        actual = TriangleApp.classify(3, 5, 4)
+        expected = Classification.RIGHT
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_an_equilateral_triangle(self):
+        actual = TriangleApp.classify(1, 1, 1)
+        expected = Classification.EQUILATERAL
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_an_isosceles_triangle(self):
+        actual = TriangleApp.classify(2, 2, 3)
+        expected = Classification.ISOSCELES
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_triangle_with_one_negative_side(self):
+        actual = TriangleApp.classify(2, 2, -3)
+        expected = Classification.INVALID
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_degenerate_triangle(self):
+        actual = TriangleApp.classify(0, 1, 2)
+        expected = Classification.INVALID
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_triangle_with_short_sides(self):
+        actual = TriangleApp.classify(2, 3, 18)
+        expected = Classification.INVALID
+        self.assertEqual(actual, expected)
+
+    def test_classification_of_a_right_triangle_with_short_sides(self):
+        actual = TriangleApp.classify(3, 4, 5)
+        expected = Classification.RIGHT
+        self.assertEqual(actual, expected)