Beer IoT: Making cooling rate calculation testable

My previous beer IoT post introduced how to measure cooling rate of beer. As I introduced the first calculation there I implemented it in code the way it just works and gets calculations done. Now it’s time to focus on the implementation and make some small improvements that clean up code and improve technical design.

Update. Source code of my TemperatureStation solution is available at Github. The solution contains IoT background service and web application you can use right away. Readings are reported to MSSQL or Azure IoT Hub. Documentation is available at TemperatureStation wiki. Feel free to try out the solution.

Alhtough we can calculate cooling rate the calculation is not implemented very well. Here’s the current method.


private void MeasureCoolingRate(IEnumerable<Measurement<double>> temps)
{
    if (!_initialMeasurementsDone)
    {
        var ambient = temps.First(t => t.DeviceId == AmbientSensorId);
        _Ta = ambient.Value;
        _T0 = temps.First(t => t.DeviceId == BeerSensorId).Value;
 
        _firstMeasurementTime = ambient.Time;
        _initialMeasurementsDone = true;
        return;
    }
 
    var timeDelta = (temps.First().Time - _firstMeasurementTime).TotalMinutes;
    if (timeDelta >= 5)
    {
        var beer = temps.First(t => t.DeviceId == BeerSensorId);
        var d1 = _T0 - _Ta;
        var d2 = beer.Value - _Ta;
        _k = Math.Log(d1 / d2) / timeDelta;
 
        _kMeasurementDone = true;
 
        Debug.WriteLine("k = " + _k);
    }
}


We have the following problems here:

  • measuring method contains external logic (measuring using two steps),
  • if we want to test the calculation we have to use some fake client,
  • writing unit tests for methods like this is complicated.

To solve these problems we have to separate the methodics of measuring from calculations. As a result we have one method that deals with cooling rate measuring and storing logic and second method that calculates the cooling rate based on given data.

Separating calculation of cooling rate

Let’s focus first on cooling rate calculation. If we want to test just the calculation we have to move calculation to separate method. Let’s call this method as GetCoolingRate and let’s move it to some other class.


public static class Calc
{
    public static double GetCoolingRate(double T0, double Ta, double Tt, 
double t)
    {
        var d1 = T0 - Ta;
        var d2 = Tt - Ta;
 
        return Math.Log(d1 / d2) / t;
    }
}


We can make this method as static as it doesn’t have any dependencies with other classes in solution. Now we can use some test framework and write tests for this method.


[Fact]
public void TestCoolingRate()
{
    double Ta = 8;
    double T0 = 22;
    double Tt = 19;
    double t = 120;
    double k = 0.002;
 
    double kToTest = Math.Round(Calc.GetCoolingRate(T0, Ta, Tt, t),3);
 
    Assert.Equal(0.002, kToTest);
}


This code is just illustration and doesn’t handle rounding problems the way it should be in real code. But you get the point.

Measuring logic

As calculation of cooling rate is now in separate method we have to modify our old method to use the new one.


private void MeasureCoolingRate(IEnumerable<Measurement<double>> temps)
{
    if (!_initialMeasurementsDone)
    {
        var ambient = temps.First(t => t.DeviceId == AmbientSensorId);
        _ta = ambient.Value;
        _t0 = temps.First(t => t.DeviceId == BeerSensorId).Value;
 
        _firstMeasurementTime = ambient.Time;
        _initialMeasurementsDone = true;
        return;
    }
 
    var timeDelta = (temps.First().Time - _firstMeasurementTime).TotalMinutes;
    if (timeDelta >= 5)
    {
        var beer = temps.First(t => t.DeviceId == BeerSensorId);
 
        _k = Calc.GetCoolingRate(_t0, _ta, beer.Value, timeDelta);
        _kMeasurementDone = true;
 
        Debug.WriteLine("k = " + _k);
    }
}


Now our old method carries out only one task – controlling the process of gathering data for cooling rate calculation. It’s now just a helper method and if we need to test it then it’s more like target for integration tests.

Wrapping up

It’s only good idea to find a moment of time and make some clean-up to your code if you have introduced something new to code. Cooling rate calculation we added to our beer cooling solution in previous post worked but there was still room for refactoring to achieve better testability and architecture. In this post we took calculation method with complex dependencies and separated calculations part from old method to new one. As a result we can easily test cooling rate calculation using some unit test framework we like.


2 thoughts on “Beer IoT: Making cooling rate calculation testable

Leave a Reply

Your email address will not be published. Required fields are marked *