Racing-Car-Kata Part I

Veröffentlicht von

Today I tried the first part of Emily Bache’s Racing Car Katas. My solution can be found under this Github Repository. Here is the kata description in detail:

  1. TirePressureMonitoringSystem exercise: write the unit tests for the Alarm class. The Alarm class is designed to monitor tire pressure and set an alarm if the pressure falls outside of the expected range. The Sensor class provided for the exercise fakes the behaviour of a real tire sensor, providing random but realistic values….

The description also states that the API of the Alarm class does not have to be changed. First of all, I decided to move the code into a separate GitHub repository to have a nice clean history of the refactoring process. In addition, I moved the projects to netcore 3.1 to be able to use dotnet watch test to run the unit tests continuously during the refactoring process.

Implementing the unit tests

Since the responsibility of the system is to fire up an alarm if tire pressure exceeds a certain range, I decided to come up with three parameterized test cases:

  1. Tire pressure is below the required range.
  2. Tire pressure is above the required range.
  3. Tire pressure is inside the required range.

When starting to write the test I recognized that pressure can’t be changed from outside in the Alarm class:

namespace TDDMicroExercises.TirePressureMonitoringSystem
{
    public class Alarm
    {
        private const double LowPressureThreshold = 17;
        private const double HighPressureThreshold = 21;

        Sensor _sensor = new Sensor();

        bool _alarmOn = false;
        private long _alarmCount = 0;


        public void Check()
        {
            double psiPressureValue = _sensor.PopNextPressurePsiValue();

            if (psiPressureValue < LowPressureThreshold || HighPressureThreshold < psiPressureValue)
            {
                _alarmOn = true;
                _alarmCount += 1;
            }
        }

        public bool AlarmOn
        {
            get { return _alarmOn; }
        }

    }
}

 

The dependency on the Sensor class is a hardcoded field initializer, which makes it hard to write unit tests for the Alarm class. In the case of SOLID design principles, this class breaks the dependency inversion principle, since Sensor is hardcoded. So we’ve to add an option to inject new pressure values while keeping the actual interface unchanged. Since the description doesn’t mention that we are not allowed to extend the interface of the Alarm class it is very simple to inject the Sensor class via an additional constructor:

public Alarm() : this(new Sensor())
{
}

public Alarm(ISensor sensor) => _sensor = sensor;

With this change Sensor now also implements ISensor. Based on that we’re able to inject a test-stub serving the requested pressure values for our unit tests.

The unit tests:

using NUnit.Framework;

namespace TDDMicroExercises.TirePressureMonitoringSystem
{
    [TestFixture]
    public sealed class AlarmTest
    {
        [TestCase(0.0)]
        [TestCase(-1.0)]
        [TestCase(16.99)]
        public void Check_SensorStatesLowPressure_ReturnTrue(double pressure)
            => Assert(pressure, true);

        [TestCase(21.01)]
        [TestCase(1000.0)]
        public void Check_SensorStatesHighPressure_ReturnTrue(double pressure)
            => Assert(pressure, true);

        [TestCase(17.0)]
        [TestCase(17.5)]
        [TestCase(20.0)]
        [TestCase(21.0)]
        public void Check_SensorStatesValidPressure_ReturnFalse(double pressure)
            => Assert(pressure, false);

        private void Assert(double pressure, bool alarmState)
        {
            // Arrange
            var sensorObject = SetupSensorStub(pressure);
            var alarm = new Alarm(sensorObject);

            // Act
            alarm.Check();

            // Assert
            NUnit.Framework.Assert.AreEqual(alarmState, alarm.AlarmOn);
        }

        private static ISensor SetupSensorStub(double pressure)
        {
            var sensorStub = new Moq.Mock<ISensor>();
            sensorStub.Setup(sensor => sensor.PopNextPressurePsiValue()).Returns(pressure);
            var sensorObject = sensorStub.Object;
            return sensorObject;
        }
    }
}

As can be seen, each unit test receives the actual pressure as a parameter and delegates the assertion logic to the common Assert method. Since the hardcoded pressure limits are 17 and 21 the test cases try to catch the edge cases.

After the unit tests are committed I switch to a cyclic refactor, test, commit-flow as described in Martin Fowler’s Refactoring book. To do so I use the dotnet watch test command:

└[~\source\repos\racing-car-kata-TirePressureMonitoringSystem]> dotnet watch  --project  .\src\TirePressureMonitorSystem.Tests\TirePressureMonitorSystem.Tests.csproj test

 

Here is a screenshot showing Visual Studio and my cmd-line setup:

Every time I make a small refactoring step I save the file. This triggers the tests to run. If the test result is ok I commit the change ( on a separate refactor branch). After refactoring is done I merge the branch back to the master.

The refactoring

The refactoring phase is pretty straightforward.  At first, I removed the _alarmCount field because it was not used at all. After that, I replaced _alarmOn with the already available AlarmOn property. Finally, I moved the range pressure check to a private function to make the Check method more readable for other developers. Between these steps, I performed the refactoring and took a look at the test output. If tests were green I committed a small change.

Here is my final result of the refactoring:

using System;

namespace TDDMicroExercises.TirePressureMonitoringSystem
{
    public class Alarm
    {
        private const double LowPressureThreshold = 17.00;
        private const double HighPressureThreshold = 21.00;

        private readonly ISensor _sensor;

        public bool AlarmOn { get; private set; }

        public Alarm() : this(new Sensor())
        {
        }

        public Alarm(ISensor sensor) => _sensor = sensor;

        public void Check() => PressureIsNotInRange(() => AlarmOn = true);

        private void PressureIsNotInRange(Action notInRange)
        {
            var pressure = _sensor.PopNextPressurePsiValue();
            if (pressure < LowPressureThreshold || HighPressureThreshold < pressure)
            {
                notInRange?.Invoke();
            }
        }

    }
}

 

 

 

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*

code