1
\$\begingroup\$

I'm writing a vector class for 2D geometry applications, and below is a rough draft for a unit test using Cpputest. I'm familiar with unit tests, but this is the first I've done for a purely mathematical class, and I'd really appreciate any insight or feedback no matter how small; even personal preference and knit-picking is welcome.

#include "CppUTest/TestHarness.h"

#include "../Mathos.hpp"
#include <stdio.h>

// Todo:    Put padding (2 newlines) between tests.

double const TOLERANCE = 0.000001;


static void VECTORS_EQUAL(double x, double y, mat::Vector2D const &V, double tolerance)
{
    DOUBLES_EQUAL(x, V.x, tolerance);
    DOUBLES_EQUAL(y, V.y, tolerance);
}

static void VECTORS_EQUAL(mat::Vector2D const &expect,
                            mat::Vector2D const &actual,
                            double tolerance)
{
    VECTORS_EQUAL(expect.x, expect.y, actual, tolerance);
}


TEST_GROUP(VectorTestGroup)
{

};

TEST(VectorTestGroup, VectorSize)
{
    CHECK_EQUAL(16, sizeof(mat::Vector2D));
}

TEST(VectorTestGroup, VectorIsZeroByDefault)
{
    mat::Vector2D U;
    VECTORS_EQUAL(0, 0, U, TOLERANCE);
}

TEST(VectorTestGroup, VectorCanBeCreatedFromValues)
{
    // x is provided, no y
    mat::Vector2D U(1);
    VECTORS_EQUAL(1, 0, U, TOLERANCE);

    mat::Vector2D V(1, 2);
    VECTORS_EQUAL(1, 2, V, TOLERANCE);
}

TEST(VectorTestGroup, VectorCanBeCreatedFromVector)
{
    mat::Vector2D U(mat::Vector2D(1, 2));
    VECTORS_EQUAL(1, 2, U, TOLERANCE);

    mat::Vector2D V(U);
    VECTORS_EQUAL(1, 2, V, TOLERANCE);

    // Calls the mat::Vector(double, double)
    // constructor
    mat::Vector2D W = mat::Vector2D(4, 3);
    VECTORS_EQUAL(4, 3, W, TOLERANCE);

    // Calls the copy constructor
    mat::Vector2D Z = W;
    VECTORS_EQUAL(4, 3, Z, TOLERANCE);
}

TEST(VectorTestGroup, VectorAssignment)
{
    mat::Vector2D U(1, 2);
    mat::Vector2D V(3, 4);
    U = V;
    VECTORS_EQUAL(3, 4, U, TOLERANCE);
}


TEST(VectorTestGroup, Vector_addition)
{
    mat::Vector2D U(1, 2);
    mat::Vector2D V(3, 4);
    mat::Vector2D W = U + V;
    VECTORS_EQUAL(4, 6, W, TOLERANCE);
}


TEST(VectorTestGroup, Vector_addition_self_assignment)
{
          mat::Vector2D A;
    const mat::Vector2D B( 1,  2);
    const mat::Vector2D C(-3,  4);
    const mat::Vector2D D(-5, -6);
    const mat::Vector2D E( 7, -8);

    A += B;
    VECTORS_EQUAL(1, 2, A, TOLERANCE);

    A += C;
    VECTORS_EQUAL(-2, 6, A, TOLERANCE);

    A += D;
    VECTORS_EQUAL(-7, 0, A, TOLERANCE);

    A += E;
    VECTORS_EQUAL(0, -8, A, TOLERANCE);

    mat::Vector2D F;
    mat::Vector2D G(1, 1);

    F = G += A;
    VECTORS_EQUAL(1, -7, F, TOLERANCE);
}


TEST(VectorTestGroup, Vector_subtraction)
{
    mat::Vector2D U(1, 2);
    mat::Vector2D V(3, 4);
    mat::Vector2D W = U - V;
    VECTORS_EQUAL(-2, -2, W, TOLERANCE);
}


TEST(VectorTestGroup, Vector_subtraction_self_assignment)
{
          mat::Vector2D A;
    const mat::Vector2D B( 1,  2);
    const mat::Vector2D C(-3,  4);
    const mat::Vector2D D(-5, -6);
    const mat::Vector2D E( 7, -8);

    A -= B;
    VECTORS_EQUAL(-1, -2, A, TOLERANCE);

    A -= C;
    VECTORS_EQUAL(2, -6, A, TOLERANCE);

    A -= D;
    VECTORS_EQUAL(7, 0, A, TOLERANCE);

    A -= E;
    VECTORS_EQUAL(0, 8, A, TOLERANCE);

    mat::Vector2D F;
    mat::Vector2D G(1, 1);

    F = G -= A;
    VECTORS_EQUAL(1, -7, F, TOLERANCE);
}


TEST(VectorTestGroup, Scalar_multiplication)
{
    mat::Vector2D U(1, 2);
    mat::Vector2D V = 10 * U;   // Number comes first
    VECTORS_EQUAL(10, 20, V, TOLERANCE);
    V = V * 3;                  // Vector comes first
    VECTORS_EQUAL(30, 60, V, TOLERANCE);
}


TEST(VectorTestGroup, Scalar_multiplication_self_assignment)
{
    mat::Vector2D A(1, 2);

    A *= 2;
    VECTORS_EQUAL(2, 4, A, TOLERANCE);

    A *= -4.5;
    VECTORS_EQUAL(-9, -18, A, TOLERANCE);

    mat::Vector2D B = A *= 0.25;
    VECTORS_EQUAL(-2.25, -4.5, B, TOLERANCE);
}


TEST(VectorTestGroup, Scalar_division)
{
    mat::Vector2D U(1, 2);
    mat::Vector2D V = U / 4;
    VECTORS_EQUAL(0.25, 0.50, V, TOLERANCE);
}


TEST(VectorTestGroup, Scalar_division_self_assignment)
{
    mat::Vector2D A(1, 2);

    A /= 2;
    VECTORS_EQUAL(0.5, 1, A, TOLERANCE);

    A /= -4;
    VECTORS_EQUAL(-0.125, -0.25, A, TOLERANCE);

    mat::Vector2D B = A /= 0.1;
    VECTORS_EQUAL(-1.25, -2.5, B, TOLERANCE);
}


void printVectorTest(const mat::Vector2D& expected, const mat::Vector2D& actual)
{
    // Print vector as minimum one digit with 24 digits
    // trailing after the decimal point.
    printf("\n");
    printf("Expected: <%1.24f, %1.24f>\n", expected.x, expected.y);
    printf("     Got: <%1.24f, %1.24f>\n", actual.x, actual.y);
}


TEST(VectorTestGroup, Vector_equality)
{
    mat::Vector2D const A(1, 2);
    mat::Vector2D const B(3, 4);
    mat::Vector2D const C(1, 2);

    CHECK(A == A);
    CHECK(A == C);
    CHECK_FALSE(A == B);
}


TEST(VectorTestGroup, Vectors_that_are_about_the_same_are_equal)
{
    // This is not exactly 0.3
    mat::Vector2D const x(0.3);

    // Floating-point rounding errors will build up here
    mat::Vector2D const myPointNine = x + x + x;

    mat::Vector2D const expectedPointNine(0.9);

    printVectorTest(expectedPointNine, myPointNine);

    // Even though the result is not exactly 0.9, it's close
    // enough to be considered equal to 0.9
    CHECK(myPointNine == expectedPointNine);
}


TEST(VectorTestGroup, Subtracting_two_equal_vectors_gives_the_zero_vector)
{
    mat::Vector2D const b(3 * mat::Vector2D(0.3) - mat::Vector2D(0.9));

    mat::Vector2D const ZERO_VECTOR;

    printVectorTest(ZERO_VECTOR, b);

    CHECK(b == ZERO_VECTOR);
}


TEST(VectorTestGroup, Very_small_vectors_are_not_equal_to_the_zero_vector)
{
    CHECK_FALSE(mat::Vector2D(0.000001) == mat::Vector2D(0));
    CHECK_FALSE(mat::Vector2D(0.000000001) == mat::Vector2D(0));
    CHECK_FALSE(mat::Vector2D(0.000000000001) == mat::Vector2D(0));
}


TEST(VectorTestGroup, Vector_inequality)
{
    mat::Vector2D const A(1, 2);
    mat::Vector2D const B(3, 4);
    mat::Vector2D const C(1, 2);

    CHECK_FALSE(A != A);
    CHECK_FALSE(A != C);
    CHECK(A != B);
}

TEST_GROUP(VectorOperationsGroup)
{
    void setup()
    {
        mat::Vector2D U;
    }

    void teardown()
    {

    }
};

IGNORE_TEST(VectorOperationsGroup, LENGTH)
{
    // DOUBLES_EQUAL(5.000000, mat::length( mat::Vector2D( 4,  3) ), TOLERANCE);
    // DOUBLES_EQUAL(9.055385, mat::length( mat::Vector2D(-1,  9) ), TOLERANCE);
    // DOUBLES_EQUAL(8.246211, mat::length( mat::Vector2D(-8, -2) ), TOLERANCE);
    // DOUBLES_EQUAL(7.810250, mat::length( mat::Vector2D( 6, -5) ), TOLERANCE);

    CHECK(true);
}

IGNORE_TEST(VectorOperationsGroup, UNIT)
{
    // mat::Vector2D const A( 4,  3);
    // mat::Vector2D const B(-1,  9);
    // mat::Vector2D const C(-8, -2);
    // mat::Vector2D const D( 6, -5);

    // mat::Vector2D a( mat::unit( A ) );
    // mat::Vector2D b( mat::unit( B ) );
    // mat::Vector2D c( mat::unit( C ) );
    // mat::Vector2D d( mat::unit( D ) );

    // DOUBLES_EQUAL(1.000000, mat::length(a), TOLERANCE);
    // DOUBLES_EQUAL(1.000000, mat::length(b), TOLERANCE);
    // DOUBLES_EQUAL(1.000000, mat::length(c), TOLERANCE);
    // DOUBLES_EQUAL(1.000000, mat::length(d), TOLERANCE);

    // VECTORS_EQUAL(A, a * mat::length(A), TOLERANCE);
    // VECTORS_EQUAL(B, b * mat::length(B), TOLERANCE);
    // VECTORS_EQUAL(C, c * mat::length(C), TOLERANCE);
    // VECTORS_EQUAL(D, d * mat::length(D), TOLERANCE);

    CHECK(true);
}

IGNORE_TEST(VectorOperationsGroup, DOT)
{
    // mat::Vector2D const A( 4,  3);
    // mat::Vector2D const B(-1,  9);
    // mat::Vector2D const C(-8, -2);
    // mat::Vector2D const D( 6, -5);

    // DOUBLES_EQUAL( 23.00000, mat::dot(A, B), TOLERANCE);
    // DOUBLES_EQUAL(-38.00000, mat::dot(A, C), TOLERANCE);
    // DOUBLES_EQUAL( 9.000000, mat::dot(A, D), TOLERANCE);

    CHECK(true);
}

Vector2D.hpp

#ifndef VECTOR_2D_HPP
#define VECTOR_2D_HPP



#include <ostream>



namespace mat {

class Vector2D {
public:
    double x, y;

    Vector2D();
    Vector2D(double x, double y = 0);
    Vector2D(const Vector2D& A);

    const Vector2D& operator =(const Vector2D& rhs);

    Vector2D operator +(const Vector2D& rhs) const;
    Vector2D operator -(const Vector2D& rhs) const;
    Vector2D operator /(double k) const;

    const Vector2D& operator +=(const Vector2D& rhs);
    const Vector2D& operator -=(const Vector2D& rhs);
    const Vector2D& operator *=(double k);
    const Vector2D& operator /=(double k);

    bool operator ==(const Vector2D& rhs) const;
    bool operator !=(const Vector2D& rhs) const;
};

Vector2D operator*(double k, const Vector2D& A);
Vector2D operator*(const Vector2D& A, double k);

std::ostream& operator<<(std::ostream& os, const Vector2D& A);

double length(Vector2D const &U);
Vector2D unit(Vector2D const &U);
double dot(Vector2D const &U, Vector2D const &V);

}

#endif
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Nitpick: It's nit-picking (or nitpicking), not knit-picking. ;) \$\endgroup\$
    – DLosc
    Apr 11, 2022 at 19:31

1 Answer 1

2
\$\begingroup\$

Small things first:

  • Prefer to include the implementation file first, before the unit-test framework files (just in case we accidentally rely on its transitive includes).
  • Include <osfwd> rather than <ostream> where we don't need full definitions.
  • I don't like (non-member) * operator being separated from / so much - either make both functions non-member, or move the vector-first version of * within the class.
  • The IGNORE_TEST functions at the end suggest that the code is still unfinished - code should be finished to be ready for review.
  • We're missing tests of <<, length(), unit() and dot().

Main review:

  • Please don't use all-caps names for things that are not preprocessor macros. That draws reader attention to all the wrong places.

  • The whole "tolerance" complication seems unnecessary here. We're working with exact binary numbers here (apart from a division by 0.1 - that can be changed), so we should be able to use ordinary equality (even when we come to test length(), we can choose a Pythagorean pair to ensure exact results).

  • Testing the vector's size is an over-test. The size shouldn't matter to user programs, and there's no good reason for it to be a fixed size (after all, it depends directly on the the platform's choice of double and char types).

  • It's not clear why the arithmetic tests include more than one invocation of the method under test (particularly the assignment versions). A test normally has three phases - initialise, execute, verify - but these ones break that expectation. Prefer more, smaller tests:

      TEST(VectorTestGroup, Scalar_multiply_NxV)
      {
          VECTORS_EQUAL(mat::Vector2D{10, 20}, 10 * mat::Vector2D{1, 2});
      }
    
    
      TEST(VectorTestGroup, Scalar_multiply_VxN)
      {
          VECTORS_EQUAL(mat::Vector2D{10, 20}, mat::Vector2D{1, 2} * 10);
      }
    
  • The tests of == and != imply that those functions have a built-in fuzziness. I would recommend against that, and write a specific "approximately-equals" function instead when fuzzy comparison is required (allowing the user to pass in the relative and/or absolute tolerance to use - a baked-in choice is unlikely to suit all users).

    One significant aspect of such an unexpected overload is that it violates the contract of ==: users expect that if a == b and b == c, then a == c.

\$\endgroup\$
5
  • \$\begingroup\$ The tolerance thing is actually apart of the testing framework itself (see DOUBLES_EQUAL()). Should I just pass in 0 instead? That being said, my class does have an implied fuzziness of its own, and it further complicates things. \$\endgroup\$
    – Mode77
    Apr 2, 2022 at 11:05
  • 1
    \$\begingroup\$ I've never used that specific test framework, but yes, if you can't compare double values exactly, you should be able to pass zero as the tolerance. The page you linked suggests that CHECK_EQUAL() will work, though (since double has a perfectly fine operator==()). \$\endgroup\$ Apr 2, 2022 at 13:21
  • 1
    \$\begingroup\$ I still recommend not having implied fuzziness in your class's ==, as in my last bullet point. \$\endgroup\$ Apr 2, 2022 at 13:22
  • \$\begingroup\$ So floating point arithmetic works as expected if the numbers are whole? For example, double(7.0) - double(5.0) == double(2.0)? The result is exactly 2? \$\endgroup\$
    – Mode77
    Apr 2, 2022 at 13:43
  • 1
    \$\begingroup\$ Yes, simple arithmetic with whole numbers (or exact binary fractions such as 0.5, 0.125, 0.375 - assuming your platform uses binary floating-point) always gives exact results, as long as there are enough mantissa bits in the floating point representation. What doesn't work well is division (except by powers of two) or infinite fractions (such as 0.1 on a binary platform). \$\endgroup\$ Apr 2, 2022 at 17:00

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.