My solution for UncleBob's Mark IV CoffeeMaker - 2

Part 2: Watch those sensors
From my little design exercise, it looks like we need someone who will poll the sensors periodically and alert us with events.
I have already created an implementation of the CoffeeMakerAPI interface called FakeCoffeeMakerDevice that we'll use for our task.
Test first.. here we go

[TestFixture]
public class TestSensorObserver
{

private int m_iNotificationCount;
[SetUp]
public void Setup()
{
m_bEventRaised = 0;
}

[Test]
public void Test_BrewButtonPressedEvent_IsRaised()
{
SensorObserver obObserver = new SensorObserver();
FakeCoffeeMakerDevice obCoffeeMaker = new FakeCoffeeMakerDevice();
obObserver.Observe(obCoffeeMaker);
obObserver.BrewButtonPressed += new EventHandler(On_Event_Raised);

obCoffeeMaker.PressBrewButton();
Thread.Sleep(300);

obObserver.Dispose();
Assert.IsTrue(m_bEventRaised, "BrewButton Pressed But SensorObserver didnt notify
us"
);
}

private void On_Event_Raised(object sender, EventArgs e)
{
m_bEventRaised = true;
}


Now the code to make this pass turned out to be a bit longwinded. TDD with multithreading right off the bat.


public class SensorObserver
{
private CoffeeMakerAPI m_obDevice;

public event EventHandler BrewButtonPressed;

private bool m_bStopWatching = false;
private Thread obListenerThread;

private bool m_bIsDisposed = false;

public void Observe(CoffeeMakerAPI obDevice)
{
m_obDevice = obDevice;
obListenerThread = new Thread(new ThreadStart(ListenForChanges));
obListenerThread.Start();
}

private void ListenForChanges()
{
while ( !m_bStopWatching )
{
if (BrewButtonStatus.PUSHED == m_obDevice.GetBrewButtonStatus())
{
if (null != this.BrewButtonPressed)
{
this.BrewButtonPressed(this, EventArgs.Empty);
}
}

Thread.Sleep(100);
}
}

public void Dispose()
{
if (m_bIsDisposed)
{ return; }
m_bStopWatching = true;
obListenerThread.Join(2000);
m_bIsDisposed = true;
}
}


  • Add a similar test for BoilerEmpty event. Done.
  • Refactor common test code into setup and teardown.
  • Remove the magic numbers related to delays for threads


[TestFixture]
public class TestSensorObserver
{
SensorObserver obObserver;
FakeCoffeeMakerDevice obCoffeeMaker;
private int m_iNotificationCount;

const int I_WAIT_FOR_EVENTS_TO_FIRE = 300;

[SetUp]
public void Setup()
{
m_iNotificationCount = 0;

obObserver = new SensorObserver();
obCoffeeMaker = new FakeCoffeeMakerDevice();
obObserver.Observe(obCoffeeMaker);
}

[Test]
public void Test_BrewButtonPressedEvent_IsRaised()
{
obObserver.BrewButtonPressed += new EventHandler(On_Event_Raised);

obCoffeeMaker.PressBrewButton();
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);

obObserver.Dispose();
Assert.IsTrue(m_iNotificationCount != 0, "BrewButton Pressed But SensorObserver didnt notify us");
}
...

But now it seems that the Sensor Observer would nag us with events till we put some water in the boiler. We want only one notification when the boiler goes from NOT_EMPTY to EMPTY. (Not a problem with Brew Button since the spec says that the CoffeeMaker auto-resets it to NOT_PUSHED.)
Looks like we need a test to verify that only one event is raised. We could write a new test but that would be the same as the previous one except for the end assertion. How about if we use an integer value instead of a boolean to check for event notifications.. we could then check m_iNotification == 1 to verify that the event was raised.. and only once.
Turns out I need to add another delay before setting BoilerState to EMPTY in the test else the change is too fast for SensorObserver to see.

[Test]
public void Test_BoilerEmptyEvent_IsRaised_ExactlyOnce()
{
obCoffeeMaker.SetBoilerStatus(BoilerStatus.NOT_EMTPY);
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);
obObserver.BoilerEmpty += new EventHandler(On_Event_Raised);

obCoffeeMaker.SetBoilerStatus(BoilerStatus.EMPTY);
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);

obObserver.Dispose();
Assert.IsTrue(m_iNotificationCount == 1, "Boiler has run out of water but SensorObserver has notified us {0} times", m_iNotificationCount);
}

Code to make that green:

private void ListenForChanges()
{
BoilerStatus eLastBoilerStatus = m_obDevice.GetBoilerStatus();
while ( !m_bStopWatching )
{
...

BoilerStatus eCurrentBoilerStatus = m_obDevice.GetBoilerStatus();
if ((BoilerStatus.EMPTY == eCurrentBoilerStatus) && (eCurrentBoilerStatus != eLastBoilerStatus))
{
if (null != this.BoilerEmpty)
{
this.BoilerEmpty(this, EventArgs.Empty);
}
}
eLastBoilerStatus = eCurrentBoilerStatus;
Thread.Sleep(FOR_100_MILLISEC);
}

Great. Next we need a notification which lets us know of changes in the WarmerPlateStatus… and I‘m beginning to feel like a BDUFish (building up a component without a user story).. but this is the last event .. I’ll trudge on.
The WP can be in 3 states.. let me draw up a quick state diagram.
Shows that there are 5 valid transitions.. (Invalid Transitions: PotNotEmpty ->PotEmpty. PotEmpty->WarmerEmpty is suspect but I’ll let it slide.. (Wally just checked if there’s any… and snuck it back.. The puppet master shall lie in wait).
Hmm as I think about it.. I just need a changed notification.. the new value can be queried.. Let see if we can do with a simple ‘changed’ event..

[Test]
public void Test_WarmerPlateStatusChanged()
{
obCoffeeMaker.WarmerPlateStatus = WarmerPlateStatus.POT_EMPTY;
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);
obObserver.WarmerPlateStatusChanged += new EventHandler(On_Event_Raised);

obCoffeeMaker.WarmerPlateStatus = WarmerPlateStatus.POT_NOT_EMPTY;
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);
Assert.IsTrue(m_iNotificationCount == 1, "Sensor Observer missed the Warmer Plate PE->PNE transition");

obCoffeeMaker.WarmerPlateStatus = WarmerPlateStatus.WARMER_EMPTY;
Thread.Sleep(I_WAIT_FOR_EVENTS_TO_FIRE);
Assert.IsTrue(m_iNotificationCount == 2, "Sensor Observer missed the Warmer Plate PNE->WE transition");

// and so on for all 6 transitions

Code is similar to that of BoilerEmpty event except that you raise an event whenever there is a change between last and current values
The test and the ListenForChanges method both are nearly exceeding a screenful.. but I’ll let it be for now. I don’t see more additions coming.. if they do I‘ll slice-n-extract methods.

Ok so now we have everything ready for the real deal.. CoffeeMaker. Lets begin with something simple.
Turn the page...

No comments:

Post a Comment