diff --git a/knoten/csm.py b/knoten/csm.py
index d951b1fa270511b5fdee051be0c01c30ad1218b6..f80753052a4b4d9c8ff55b02efd29a3e3102b348 100644
--- a/knoten/csm.py
+++ b/knoten/csm.py
@@ -157,13 +157,12 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001):
     intersection = generate_ground_point(0.0, image_pt, camera)
     iterations = 0
     semi_major, semi_minor = get_radii(camera)
-    
+  
     source_proj = f'+proj=cart +a={semi_major} +b={semi_minor}'
     dest_proj = f'+proj=lonlat +a={semi_major} +b={semi_minor}'
     transformer = utils.create_transformer(source_proj, dest_proj)
-
     while iterations != max_its:
-        lon, lat, alt = transformer.transform(intersection.x, 
+        lon, lat, _ = transformer.transform(intersection.x, 
                                               intersection.y, 
                                               intersection.z,
                                               errcheck=True)
@@ -174,9 +173,7 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001):
             raise ValueError(f'No DEM height at {lat}, {lon}')
     
         next_intersection = camera.imageToGround(image_pt, float(height))
-        dist = max(abs(intersection.x - next_intersection.x),
-            abs(intersection.y - next_intersection.y),
-            abs(intersection.z - next_intersection.z))
+        dist = _compute_intersection_distance(intersection, next_intersection)
 
         intersection = next_intersection
         iterations += 1
@@ -184,6 +181,30 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001):
             break
     return intersection
 
+def _compute_intersection_distance(intersection, next_intersection):
+    """
+    Private func that takes two csmapi Ecef objects or other objects with
+    x,y,z properties and computes the distance between them. This is the
+    maximum distance in 3D space.
+
+    Parameters
+    ----------
+    intersection : object
+                   Any object with x,y, and z properties that are numeric
+
+    next_intersection : object
+                        Any object with x,y, and z properties that are numeric              
+
+    Returns
+    -------
+    dist : float
+           The maximum distance between intersection and next_intersection
+           in one of the three planes (x,y,z)
+    """
+    return max(abs(intersection.x - next_intersection.x),
+            abs(intersection.y - next_intersection.y),
+            abs(intersection.z - next_intersection.z))
+
 def generate_boundary(isize, npoints=10):
     '''
     Generates a bounding box given a camera model
diff --git a/tests/test_csm.py b/tests/test_csm.py
new file mode 100644
index 0000000000000000000000000000000000000000..600e84a80b2ba65f783fd73ea7a0d135ca8db54f
--- /dev/null
+++ b/tests/test_csm.py
@@ -0,0 +1,76 @@
+from unittest import mock
+import pytest
+
+from plio.io.io_gdal import GeoDataset
+
+import csmapi
+from knoten import csm
+
+@pytest.fixture
+def mock_dem():
+    mock_dem = mock.MagicMock(spec_set=GeoDataset)
+    #mock_dem.read_array.return_value = 100
+    #mock_dem.latlon_to_pixel.return_value = (0.5,0.5)
+    return mock_dem
+
+@pytest.fixture
+def mock_sensor():
+    mock_sensor = mock.MagicMock(spec=csmapi.RasterGM)
+    return mock_sensor
+
+@pytest.fixture
+def pt():
+    return csmapi.ImageCoord(0.0, 0.0)
+
+def test_generate_ground_point_with_float(mock_sensor):
+    csm.generate_ground_point(0, (0.5, 0.5), mock_sensor)
+    # The internal conversion from tuple to csmapi.ImageCoord means 
+    # assert_called_once_with fails due to different addresses of
+    # different objects.
+    mock_sensor.imageToGround.assert_called_once()
+
+def test_generate_ground_point_with_imagecoord(mock_sensor, pt):
+    height = 0.0
+    csm.generate_ground_point(height, pt, mock_sensor)
+    mock_sensor.imageToGround.assert_called_once_with(pt, height)
+
+@mock.patch.object(csm, 'get_radii', return_value=(10,10))
+@mock.patch('pyproj.transformer.Transformer.transform', return_value=(0,0,0))
+@mock.patch.object(csm, '_compute_intersection_distance', return_value=0)
+def test_generate_ground_point_with_dtm(mock_dem, mock_sensor, pt):
+    # Passing the mock_dem fixture fails for some reason. The
+    # isinstance(obj, GeoDataset) check fails, causing the singldispath
+    # to never dispatch to the func under test.
+    mock_dem = mock.MagicMock(spec_set=GeoDataset)
+    mock_dem.no_data_value = 10
+    mock_dem.read_array.return_value = [[100]]
+    mock_dem.latlon_to_pixel.return_value = (0.5,0.5)
+    csm.generate_ground_point(mock_dem, pt, mock_sensor)
+    # This call is mocked so that the intitial intersection and 
+    # one iteration should occur. Therefore, the call count
+    # should always be 2.
+    assert mock_sensor.imageToGround.call_count == 2
+
+from collections import namedtuple
+
+@mock.patch.object(csm, 'get_radii', return_value=(10,10))
+@mock.patch('pyproj.transformer.Transformer.transform', return_value=(0,0,0))
+def test_generate_ground_point_with_dtm_ndv(mock_dem, mock_sensor, pt):
+    # Passing the mock_dem fixture fails for some reason. The
+    # isinstance(obj, GeoDataset) check fails, causing the singldispath
+    # to never dispatch to the func under test.
+    mock_dem = mock.MagicMock(spec_set=GeoDataset)
+
+    # If the no data value equals the height, this should raise a value error
+    mock_dem.no_data_value = 100
+    mock_dem.read_array.return_value = [[100]]
+    mock_dem.latlon_to_pixel.return_value = (0.5,0.5)
+    with pytest.raises(ValueError):
+        csm.generate_ground_point(mock_dem, pt, mock_sensor)
+
+def test__compute_intersection_distance():
+    Point = namedtuple("Point", 'x, y, z')
+    pt1 = Point(0,0,0)
+    pt2 = Point(1,1,1)
+    dist = csm._compute_intersection_distance(pt1, pt2)
+    assert dist == 1
\ No newline at end of file