4
\$\begingroup\$

I am starting to explore automated testing of code, so I decided to write some trivial code about the cartesian plane and test it. I am particularly interested in automated testing conventions and tips.

cartesian.py

def pytagoras(a,b):
    return (a**2 + b**2)**0.5

def distance(point_1,point_2):
    return pytagoras((point_1[0]-point_2[0]),(point_1[1]-point_2[1]))

def point_in_line(point,line):
    def function(x):
        return line[0] * x + line[1]
    return function(point[0]) == point[1]

def average(lst):
    return sum(lst) / len(lst)

def middle_point(point_1,point_2):
    return [average([point_1[0],point_2[0]]),average([point_1[1],point_2[1]])]

def is_valid_triangle(sides):
    return sum(sides) - max(sides) > max(sides)

def heron(AB,BC,AC):
    if not is_valid_triangle((AB,BC,AC)):
        raise ValueError("The sides length given can't be a triangle.")
    sp = sum([AB,BC,AC]) / 2
    return (sp * (sp-AB) * (sp-BC) * (sp-AC) )**0.5

def area_of_tringle(points):
    AB = distance(points[0],points[1])
    BC = distance(points[1],points[2])
    AC = distance(points[0],points[2])
    return heron(AB,BC,AC)

test_cartesian.py

import unittest
import cartesian

class test_cartesian(unittest.TestCase):
    def test_pytagoras(self):
        self.assertEqual(cartesian.pytagoras(3,4),5)
        self.assertEqual(cartesian.pytagoras(6,8),10)

    def test_distance(self):
        self.assertEqual(cartesian.distance( (0,0) , (3,4) ),5)
        self.assertEqual(cartesian.distance( (1,2) , (4,6) ), 5)
        self.assertEqual(cartesian.distance( (1,1) , (1,1) ),0)

    def test_point_in_line(self):
        self.assertTrue(cartesian.point_in_line([4,6],[1,2]))
        self.assertTrue(cartesian.point_in_line([2,8],[3,2]))

    def test_average(self):
        self.assertEqual(cartesian.average([8,4,8,0]), 5)

    def test_middle_point(self):
        self.assertEqual(cartesian.middle_point([8,2],[0,4]),[4,3])
        self.assertEqual(cartesian.middle_point([5,5],[7,13]),[6,9])

    def is_valid_triangle(self):
        self.assertFalse(cartesian.is_valid_triangle(2,4,100))
        self.assertTrue(cartesian.is_valid_triangle(6,5,20))

    def test_heron(self):
        self.assertEqual(cartesian.heron(3,4,5),6)
        self.assertAlmostEqual(cartesian.heron(3,6,8),7.64,places=2)

    def test_area_of_triangle(self):
        self.assertAlmostEqual(cartesian.area_of_tringle(([4,2],[6,2],[7,0])),2)
        self.assertAlmostEqual(cartesian.area_of_tringle(([4,1],[3,4],[9,1])),7.5)

if __name__ == '__main__':
    unittest.main()
\$\endgroup\$
4
\$\begingroup\$
  1. There are no docstrings. What do these functions do? What do they return?

  2. The function pytagoras is already built into Python under the name math.hypot.

  3. In point_in_line it appears that a line is being represented by a slope \$ m \$ and a y-intercept \$ b \$. But not all lines can be represented in this form! In particular, vertical lines have no slope or y-intercept.

    It is more robust to represent a line in one of these three forms:

    1. Two different points \$ p_0 \$ and \$ p_1 \$ on the line.
    2. A point \$ p \$ on the line and a vector \$ v \$ along the line.
    3. A vector \$ n \$ normal to the line, and its perpendicular distance from the origin \$ d \$.
  4. The function average is built into Python under the name statistics.mean.

  5. Much of this code is awkward because points are represented by Python tuples or lists. A simple operation like subtracting two points (in distance) requires disassembling the points into their elements, subtracting the elements, and then reassembling the result. If the code represented points using some kind of vector data structure, then a lot of it could be simplified.

    For example, if the implementations were written very simply like this:

    from math import hypot
    
    def distance(p, q):
        """Return the distance from p to q."""
        return hypot(*(p - q))
    
    def middle_point(p, q):
        """Return a point halfway between p and q."""
        return 0.5 * (p + q)
    

    Then using the built-in turtle.Vec2D:

    >>> from turtle import Vec2D
    >>> distance(Vec2D(0, 0), Vec2D(3, 4))
    5.0
    >>> middle_point(Vec2D(1, 2), Vec2D(3, 4))
    (2.00,3.00)
    

    (Note that I don't actually recommend that you use turtle.Vec2D. This vector class is very basic: it doesn't have dot products or cross products or even division. But it's built into Python, so you can experiment with it without having to download or install anything. For actual use, NumPy arrays would be good, or pygame.math.Vector2, or something pure-Python like my vector.py.)

  6. The unit test cases are always the same. There's nothing wrong with this, but it seems a bit wasteful, because every time you run the tests they go through the same steps. If a case passed last time you ran it, and you haven't changed the function under test, then it's certain to pass this time. All the test is protecting you against is a regression: it doesn't help you find new bugs.

    A technique that sometimes does find new bugs is randomizing the test case. For example, we could test the distance function by generating random points that are a known distance apart:

    import random
    
    def test_distance(self):
        z = 1000000
        u = random.uniform
        p = Vec2D(u(-z, z), u(-z, z))
        self.assertEqual(cartesian.distance(p, p), 0)
        d = u(0, z)
        q = p + Vec2D(0, d).rotate(u(0, 360))
        self.assertAlmostEqual(cartesian.distance(p, q), d)
        self.assertAlmostEqual(cartesian.distance(q, p), d)
    

    Then each time the test case is run a different set of points will be tested.

    If you actually do get an error from a randomized test case, you'll want to reproduce it for debugging. How do you do that when it's random? You need to seed the random number generator when the test suite starts, and record the seed, for example like this:

    import binascii
    import os
    seed = os.urandom(16)
    print("seed:", binascii.hexlify(seed))
    random.seed(seed)
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.