Showing posts with label TDD. Show all posts
Showing posts with label TDD. Show all posts

Friday, December 25, 2009

Implementing a Diameter Peer State Machine using Test Driven Development (Part 3).

Just a brief riepilogue of what are the missing points :

- When a peer is in "Closed" state and receives a START signal it must
  1. take a I-Snd-Conn-Req action : this is currently a missing point;
  2. change its state to Wait-Conn-Ack : we did that in the last episode :)
- When a peer is in "Closed" state and receives a R-Conn-CER signal it must :
  1. take the following 3 actions : R-Accept, Process-CER and R-Snd-CEA.
  2. change its state to R-Open.
So, concerning this episode, we are going to add the behaviour requested by the R-Conn-CER signal, leaving out for the moment all what is concerning actions. That means we will concentrate only on state transistion.

Following the same approach we used for the "Start" signal, we can write another test method :

public class PeerStateMachineTestCase
{
....@Test
....public void rConnCerSignal()
...{
......Peer peer = new Peer();
......peer.signal(PeerEventType.R_CONN_CER);

......assertTrue(peer.currentStateIs(peer.R_OPEN);
...}
}

PeerEventType.R_CONN_CER is not giving any compiler warning because we already added it in the first part.
What is really missing(from a compiler perspective) is a R_OPEN member instance on Peer class:

public class Peer
{
....final State CLOSED = new State(){};
....final State WAIT_CONN_ACK = new State(){};
....final State R_OPEN = new State(){};
...
...
}

Now our test compiles fine but it gives us a red bar: after PeerEventType.R_CONN_CER signal has been received peer is not (as expected) in R_OPEN state but in WAIT_CONN_ACK :(
This is reasonable if we remember the Peer.signal(...) implementation we did last time :

public void signal(PeerEventType type)
{
....currentState = WAIT_CONN_ACK;
}

With the new R_CONN_CER signal this implementation is not working anymore so we need to rewrite the method in order to let the peer properly react to a given signal. The most obvious solution could be :

public void signal(PeerEventType type)
{
....if (currentState == CLOSED)
....{........if (type == START)
........{............currentState = WAIT_CONN_ACK;
........}
........else if (type == R_CONN_CER)
........{............currentState = R_OPEN;
........}
....}
}

Without any doubt it's working but
  1. If you have a look at the state machine table defined on RFC 3588 you'll quickly realize that this approach will result in a looot of "if / else if" statements;
  2. personally I believe that the transition state is something related with the state itself, while in the implementation above we made that part of the Peer logic.
In addition, each time a new state needs to be added you must :
  1. Add a new state member instance;
  2. Modify the signal method.
What I'm trying to suggest is the following : if
  1. we already have something that tell us what is the current state;
  2. that thing is an object (an instance of State)
  3. obviously that object could have its own state and methods;
Why don't we put the state transition logic within the each State? With this approach we need to modify a little bit the State interface introduced in part one adding a new method and a new exception :

(Interface State)
void signal(PeerEventType eventType) throws WrongEventException;

Now each instance of State must implements this method which is supposed to
  1. Change the state of the hosting Peer instance (remember that our State instances are inner classes);
  2. Raise an exception if a wrong (not allowed) signal is received;
So for example, the inner CLOSED State instance will do something like that :

....final State CLOSED = new State()
....{
........public void signal(PeerEventType eventType) throws WrongEventException
........{............switch (eventType)
............{................case START :
................{....................currentState = WAIT_CONN_ACK;
....................break;
................}
................case R_CONN_CER :
................{....................currentState = R_OPEN;
....................break;
................}
................default :
................{....................throw new WrongEventException("Some useful message");
................}
............}
........}....};

Of course, in order to get things working as expected, we need to modify the Peer.signal method :

public void signal(PeerEventType type) throws WrongEventTypeException
{
....currentState.signal(type);
}

Now our 2 test methods should correctly work.

See you soon for the last part :)

Ciao
Andrea

Friday, October 23, 2009

Implementing a Diameter Peer State Machine using Test Driven Development (Part 2).

So, next step is to test what happens if (following the state machine picture on the previous post) a START signal is sent to our peer.
According with RFC 3588 specification, if a peer, that is in CLOSED state, successfully processes the START signal two things happen:
  • the peer must take a "I-Snd-Conn-Req" action;
  • a state transition occurs : the peer passes from CLOSED to WAIT-CONN_ACK state;
In this article we will take care about the second point.
Well, returning to our test case, we have to add another @Test method :

public class PeerStateMachineTestCase
{

....@Test
....public void startSignal()
....{
.......Peer peer = new Peer();
.......peer.signal(PeerEventType.START);

.......assertTrue(peer.currentStateIs(peer.WAIT_CONN_ACK);
....}
}

Obviously, this test method doesn't compile : we don't have a signal() method and a WAIT_CONN_ACK member variable on our Peer class.
So first of all we will insert the missing method :

public void signal(PeerEventType eventType)
{
}

after that, Peer class needs a new State instance member :

final State WAIT_CONN_ACK = new State(){};

This is how our Peer class behaves :

public class Peer
{
....final State CLOSED = new State(){};
....final State WAIT_CONN_ACK = new State(){};

....public void signal(PeerEventType eventType){}

....boolean currentStateIs(State state)
....{
........return true;
....}
}
Now the test method (and the enclosing testcase) compiles fine. But after running, we get a red bar. Why? There is one explicit problem : transition state doesn't really happen on peer instance so the assertion fails. I mean, the peer is not on WAIT_CONN_ACK State. Actually is not in any state because the currentStateIs() method is returning true everytime :)
In order to see that all is properly working we need to introduce another peer member instance that represents the current state of the peer.
Well, considering that we already have a type for this new member (State class), we can insert (in Peer class):

private State currentState;

With this new variable, I can rewrite the currentStateIs() method in the following way :

boolean currentStateIs(State state)
{
....return currentState == state;
}

Introducing that change causes a failure in the first test we saw in part I. Well, in order to fix that we need to initialize the "currentState" member instance :

private State currentState = CLOSED;

Now the @Test initialState() is working while the @Test startSignal() is failing :(.
Why? because nothing on the peer instance is causing the required state transition and the assertion on the currentStateIs() fails because the peer is still in CLOSED state.
Again, what is the minimum change / adjustment we would do in order to get things working? This is the failing test :

public class PeerStateMachineTestCase
{
....@Test
....public void startSignal()
...{
......Peer peer = new Peer();
......peer.signal(PeerEventType.START);

......assertTrue(peer.currentStateIs(peer.WAIT_CONN_ACK);
...}
}

Yes, we can do the state transition in the signal() method :

public void signal(PeerEventType type)
{
....currentState = WAIT_CONN_ACK;
}

In this way, the test will succeed because after calling the signal() method the peer state is WAIT_CONN_ACK as required.

See you soon for third episode :)
Bye
Andrea

Saturday, May 30, 2009

Implementing a Diameter Peer State Machine using Test Driven Development (Part 1).

RFC3588 (Diameter Base Protocol), specifically par. 5.6, contains specification about a finite state machine that MUST be observed by all Diameter implementations.

In this article we will see how to implement a peer state machine using Test Driven Development. Note that this is only the first part of the whole article.
The following is an extract (just the first entry) of the RFC table (you can found that on the previously mentioned paragraph) that illustrates the peer state machine :

I choose that entry because "Closed" is the peer initial state.
As you can see when the peer is in that state it can receive only two kind of events:

1) Start : A diameter application has signaled that a connection should be initiated with the peer;
2) R-Conn-CER : An acknowledgement is received stating that the transport connection has been established, and the associated Capability Exchange Request (CER) has arrived.

The first event must be associated with the following action on the peer :

1) I-Snd-Conn-Req (Initiator Sends Connection Request) : A transport connection is initiated with the peer;

After the peer successfully processed the action its new state MUST be "Wait-Conn-Ack".

The second event, R-Conn-CER, is firing the following actions on the peer :

1) R-Accept : The incoming connection associated with the R-Conn-CER is accepted as the responder connection;
2) Process-CER : The CER associated with the R-Conn-CER is processed.
3) R-Snd-CEA (Receiver Sends CEA)

After those three actions, the peer must be in "R-Open" state.

Well, of course if you have a look at the whole table there are a lot of states / actions / events but for simplicity we can assume that we are handling only with the "Closed" state. The whole article can be applied to other table entries.

First implicit requirement is that we should have a Peer class, it must have an internal state and the initial value of that state should be "Closed".
So, for a first implementation of our test case I'd like to write something like this :

package com.gazzax.diameter.peer;

public class PeerStateMachineTestCase
{
@Test
public void initialState()
{
Peer peer = new Peer();
assertTrue(peer.currentStateIs(peer.CLOSED));
}
}

Obviously, the test doesn't compile for several reasons; First of all, we need a Peer class :) So let's create it!

package com.gazzax.diameter.peer;

public class Peer
{
}

One little step ahead! But it is not sufficient because the TestCase still doesn't compile :( There's no a currentStateIs(...) method on the Peer class!
Note that that method is accepting a parameter that represents a peer state. Peer's states are typed instance members of peer class and they have a default visibility. Why? Because in that way we can see / use them on our test case without breaking encapsulation (PeerStateMachineTestCase and Peer are in the same package even if they usually reside in different source folders)

NOTE : the current state of the peer IS NOT exposed (it is a private member). You can only have an indirect / read-only access through the currentStateIs(...) method.
This is done because we don't want break enapsulation and therefore peer state invariants.

Anyway, the state machine table suggests that

- a peer, regardless its state, should be able to receive a signal;
- the concrete handling of that signal (fired actions and next state) depends on the current state of the peer;

So, this suggests me that a peer state itself shuold be able to receive a signal. Here is the peer state interface:

package com.gazzax.diameter.peer;

public interface State
{
void signal(PeerEventType event) throws WrongEventException;
}

Where

- PeerEventType is an enumeration of all defined event types; for the current example it will look like this :

package com.gazzax.diameter.peer;

public enum PeerEventType
{
START,
R_CONN_CER
}

- WrongEventException : that will be thrown when the received event type is violating the rules defined on the peer state machine table. I mean, when the peer will receive a wrong event according with its current state. I mean, when the current state of the peer doesn't recognize the received event type as valid.

We left the TestCase with compilation errors. We need a currentStateIs(State peerState) method. Let's write it with a dummy implementation!

public class Peer
{
private final State CLOSED;

boolean currentStateIs(State state)
{
return false;
}
}

Now code should compile :)
Run again the test case and you will see the red bar! Bad? Absolutely not, that means "Progress"! Another little step ahead!

What is the minimal thing / adjustment that we could do in order to get a green bar?

public class Peer
{
private final State CLOSED;

boolean currentStateIs(State state)
{
return true;
}
}

Run again the test and...et voilĂ ! We got a green bar! Another little step ahead!