Search code examples
c#doublemstest

Unit Test - sometimes works, sometimes not


I have a unit test below running against the code that follows. This test sometimes passes, sometimes fails. Not sure why and hesitate to change things radically since well, it is a formula and sometimes passes...I'm thinking it may have something to do with the precision of the type double? Not sure. Thoughts?

Good test

Bad test

[TestMethod]
public void CircleFromCircumference()
{
    var random = new Random();
    var circumference = random.NextDouble();
    var circle = new Circle("My circle", circumference, Circle.CircleDimensions.Circumference);

    var var1 = circumference - circle.Circumference;
    var var2 = circumference - 2 * Math.PI * circle.Radius;
    var var3 = circumference - Math.PI * circle.Diameter;
    var var4 = Math.Pow(circumference / (2 * Math.PI), 2) * Math.PI - circle.Area;

    Assert.IsTrue(
        circumference - circle.Circumference <= 0 //circumference
        && circumference - 2 * Math.PI * circle.Radius <= 0 //radius
        && circumference - Math.PI * circle.Diameter <= 0 //diameter
        && Math.Pow(circumference / (2 * Math.PI), 2) * Math.PI - circle.Area <= 0 //area
        && string.IsNullOrEmpty(circle.ShapeException));
}



using System;
using System.Runtime.Serialization;

namespace Shapes
{
    [DataContract]
    public class Circle : Shape
    {
        [DataMember] public double Radius { get; set; }
        [DataMember] public double Diameter { get; set; }
        [DataMember] public double Circumference { get; set; }

        /// <summary>
        /// The name of the value you are sending. Radius is the default
        /// </summary>
        public enum CircleDimensions
        {
            Circumference = 1,
            Area = 2,
            Diameter = 3
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="circleName">The name of your circle</param>
        /// <param name="dimension">The value of the dimension you are providing</param>
        /// <param name="circleDimensions">The name of the value you are providing. Radius is default</param>
        public Circle(string circleName, double dimension = 0, CircleDimensions circleDimensions = 0)
        {
            this.ShapeName = circleName;
            if (dimension <= 0)
            {
                this.ShapeException = "Parameters must be greater than zero";
                return;
            }

            switch (circleDimensions)
            {
                case CircleDimensions.Circumference:
                    //radius from Circumference
                    this.Circumference = dimension;
                    this.Radius = this.RadiusFromCircumference(dimension);
                    this.Area = this.CalculateArea(this.Radius);
                    this.Diameter = this.CalculateDiameter(this.Radius);
                    break;
                case CircleDimensions.Area:
                    //radius from Area
                    break;
                case CircleDimensions.Diameter:
                    //radius from diameter
                    break;
                default: //calculate from radius
                    this.Radius = dimension;
                    this.Diameter = this.CalculateDiameter(dimension);
                    this.Circumference = this.CalculateCircumference(dimension);
                    this.Area = this.CalculateArea(dimension);
                    break;
            }
        }

        private double RadiusFromCircumference(double dimension) => dimension / (2 * Math.PI);

        private double CalculateCircumference(double dimension) => 2 * Math.PI * dimension;
        private double CalculateDiameter(double dimension) => 2 * dimension;
        private double CalculateArea(double dimension) =>
            Math.PI * (Math.Pow(dimension, 2));
    }
}

Solution

  • The inconsistency has nothing to do with precision per se, it has more to do with how floating point representation works. For example, if you write this:

    for (float f = 0.0f; f != 1.0f; f+=0.1f)
    {
       Console.WriteLine(f);
    }
    

    It will never exit. Because 0.1 does not have an exact representation in binary form. (https://www.exploringbinary.com/why-0-point-1-does-not-exist-in-floating-point/). I also recommend reading (https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)

    Back to the problem at hand, in your code, you are getting the Radius using this :

    dimension / (2 * Math.PI); //passed in dimension is the Circumference, returns radius
    

    And then in your test you are asserting that:

    circumference - 2 * Math.PI * circle.Radius <= 0
    

    Dividing and then multiplying by the same floating point number is not guaranteed to give you the original floating point number as a result.

    Thus, it is a bad idea in general to assert this. The most common way to test "almost equality" is to test equality "within limits". In your case, all you have to do is define a small enough epsilon that you deem "acceptable", greater or equal to double.Epsilon in your tests.

    var epsilon = double.Epsilon;
    Assert.IsTrue(
        Math.Abs(circumference - circle.Circumference) <= epsilon //circumference
        && Math.Abs(circumference - 2 * Math.PI * circle.Radius) <= epsilon //radius
        && Math.Abs(circumference - Math.PI * circle.Diameter) <= epsilon //diameter
        && Math.Abs(Math.Pow(circumference / (2 * Math.PI), 2) * Math.PI - circle.Area) <= epsilon //area
        && string.IsNullOrEmpty(circle.ShapeException));
    

    If instead you have to guarantee exactness, one option is to switch to a non-floating point type like decimal, but expect a performance hit.