Introduction

In this blog post, we’re going to look at visualizing the unit tests that are automatically generated by Diffblue cover. For this, we’re going to look at a common task in the world of computer graphics: triangle intersections. This is used when producing photorealistic images using ray-tracing. We’re going to plot the tests that Diffblue Cover writes and visually show how they test our triangle intersection logic.

Ray-tracing simulates the behavior of light in a scene. The light rays are traced from the camera into the scene, one ray per pixel (there are techniques to improve the quality of the image by using more than one, but for this discussion we’ll stick with a single ray). If the ray hits an object, then the color of the resultant pixel in the image is determined by the material properties of the intersection point: if the material is a blue cloth, then the resulting pixel will be a shade of blue. The exact color will depend on not only the material properties but also the lighting, shading, occlusion, textures and so on.

Traditionally, 3D objects in computer graphics are built from a mesh of triangles, so the first step is to determine if the simulated light ray intersects with a triangle or not. If it hasn’t, it’ll continue on its way.

For the rest of this post, we’re going to assume that the ray is shooting into the page perpendicular to the surface (i.e. straight into the screen). This makes it easier to visualize and discuss. In a real application, the math would need to take into account the direction of the ray in 3-dimensional space.

Triangles

Suppose we have a triangle sitting on a page. It has three points at the following coordinates:

a = (x_a, y_a)
b = (x_b, y_b)
c = (x_c, y_c)

These really define three lines, AB, BC and CA. Let’s look at an image of one of these lines.

Triangle connecting points (x_a, y_a), (x_b, y_b) and (x_c, y_c), creating the lines AB, BC and CA.

Remember that the light ray is being fired into the page, and so corresponds to a point on the plane. To determine if the point intersects the triangle, we need to determine if it’s inside or outside the triangle. We will get to a proper definition of “inside” or “outside” after we’ve discussed a little geometry.

Lines from Points

Line AB connection points (a) and (b)

Let’s start with defining the line (y = mx + c, where m is the slope of the line, and c is the intersection point) going from A to B. Suppose that the point A is at (1, 0) and B is at (3,3). We have the following two equations:

0 = m(1) + c
3 = m(3) + c

We need to find the slope and intersection points. To do this we can then subtract one equation from the other, simplify the result and obtain the following:

m = (3 - 0)/(3 - 1) = 1.5

From that, we can obtain the value for c by working backwards from one of the equations above with the new value of m:

0 = 1 * 1.5 + c
0 = 1.5 + c
c = -1.5

Now that we have the m and c values, we can define a line in two dimensions:

Graph to show the line AB, connecting points a and b, across two dimensions, thanks to obtaining the line's slope and intercept.

y = 1.5x - 1.5

Left or Right of a Line?

We now have our equation and can check to see if a point is to the left or the right of it. To check, we need to introduce an inequality. In this case, we say it’s to the right if y is less than the equation.

y < 1.5x - 1.5
0 < 1.5x - 1.5 - y

Graph to show the region shaded to the right of line AB, connection points a and b.

From the figure, we can see that if a point is in the shaded region, then the inequality holds. Suppose we had a test point p at (2, 1); what happens when we put that into the inequality above?

0 < 1.5 * 2 - 1.5 - 1
0 < 3 - 1.5 - 1
0 < 0.5

The inequality holds, therefore it must be on the right of the line. Let’s see what happens for point q at (2,3).

0 < 1.5 * 2 - 1.5 - 3
0 < 3 - 1.5 - 3
0 < -1.5

This inequality doesn’t hold, therefore the point is to the left of the line.

Graph to show point q falling to the left of line AB, given that the inequality doesn't hold

In this figure, you can see the two test points that we introduced and tested. Point p is in the shaded region and to the right of the line, while point q is not.

Inside or Outside

From here, you can see that if you perform this three times on the three different pairs of points in the triangle, you can determine if the point is inside (when it’s to the right of all the lines) or outside (when it’s to the left of any one of them).

This only works when the lines are defined by a clockwise traversal of the points. If you wanted to support a counter-clockwise traversal, the left and rights in the previous condition would be reversed. In reality, we don’t know (nor does it really matter) which direction we visit the points in, as long as the test point is on the same side for all the lines it’ll be inside.

Here’s an example of the inside/outside test implemented in Java. The sign method is implemented to check the point against the line equation. The derivation of this is left as an exercise for the reader.

public class Triangle {

  private final Point2D a;
  private final Point2D b;
  private final Point2D c;

  public Triangle(final Point2D a, final Point2D b, final Point2D c) {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  public boolean inside(final Point2D p){
    final int k1 = sign(p, b, a);
    final int k2 = sign(p, c, b);
    final int k3 = sign(p, a, c);
    return k1 > 0 && k2 > 0 && k3 > 0 || k1 < 0 && k2 < 0 && k3 < 0;
  }

  private int sign(Point2D p1, Point2D p2, Point2D p3) {
    return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
  }
}

Testing

This is all very well and good, but what tests cases are there? Well, when the point is inside or outside of a triangle, right?

  @Test
  public void testInside() {
    // Arrange
    Point2D a = new Point2D(0, 0);
    Point2D b = new Point2D(0, 3);
    Point2D c = new Point2D(3, 0);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(1, 1);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertTrue(actualInsideResult);
  }

  @Test
  public void testOutside() {
    // Arrange
    Point2D a = new Point2D(0, 0);
    Point2D b = new Point2D(0, 3);
    Point2D c = new Point2D(3, 0);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(4, 4);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

These test cases correspond to the following diagrams.

Diagram used to visualise the first test case, with point 'p' falling within the triangle

Diagram used to visualise the second test case, with point 'p' falling outside of the triangle

Automatically Generated Tests

Running dcover on that snippet above, I got the following set of tests:

Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p' Graph to show the outcome of an automatically generated test case, including points 'a', 'b', 'c' and 'p'

Or if you prefer the code:

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

public class TriangleTest {

  // human written test
  @Test
  public void testInside() {
    // Arrange
    Point2D a = new Point2D(0, 0);
    Point2D b = new Point2D(0, 3);
    Point2D c = new Point2D(3, 0);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(1, 1);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertTrue(actualInsideResult);
  }

  // human written test
  @Test
  public void testOutside() {
    // Arrange
    Point2D a = new Point2D(0, 0);
    Point2D b = new Point2D(0, 3);
    Point2D c = new Point2D(3, 0);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(4, 4);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

  // tests below here were generated by `dcover`

  @Test
  public void testInside1() {
    // Arrange
    Point2D a = new Point2D(1, 0);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 10);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

  @Test
  public void testInside2() {
    // Arrange
    Point2D a = new Point2D(1, 10);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 3);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

  @Test
  public void testInside3() {
    // Arrange
    Point2D a = new Point2D(10, 0);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 10);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertTrue(actualInsideResult);
  }

  @Test
  public void testInside4() {
    // Arrange
    Point2D a = new Point2D(1, 10);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 0);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

  @Test
  public void testInside5() {
    // Arrange
    Point2D a = new Point2D(1, 3);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 3);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }

  @Test
  public void testInside6() {
    // Arrange
    Point2D a = new Point2D(1, 0);
    Point2D b = new Point2D(10, 3);
    Point2D c = new Point2D(2, 10);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertTrue(actualInsideResult);
  }

  @Test
  public void testInside7() {
    // Arrange
    Point2D a = new Point2D(1, 0);
    Point2D b = new Point2D(-10, 3);
    Point2D c = new Point2D(2, 3);
    Triangle triangle = new Triangle(a, b, c);
    Point2D p = new Point2D(2, 3);

    // Act
    boolean actualInsideResult = triangle.inside(p);

    // Assert
    assertFalse(actualInsideResult);
  }
}

The interesting thing about these tests is that the point stays in the same position, but the triangle changes shape. The Triangle class also allows some interesting constructions—look at the 5th example, a case where the two of the triangles points are equal.

From these tests above, we can see that there are some interesting edge cases tested—what if the test point and one of the triangle points share the same location?

Conclusions

This article visualizes some automatically generated test cases for a simple triangle intersection test. As we’ve seen, there are some interesting test cases produced that wouldn’t necessarily have been considered by a human writing the same code. You can try it yourself using Diffblue Cover.