maandag 12 november 2007

The Card Class

Let us first create the card class. The most common class of all the classes in our object space: this is what it is all about :-). I do not have a lot of time available today, so I'll just ramble down to the details. I assume you already have some experience with C# here... If you don't, please write me a comment below!

Also, if anything is plain wrong (remember, I am just starting in C#) or when something is not very clear, please let me know.

Lets get down to business... Create a new Class Library called Poker.Common and add a new class, Card.cs:
public class Card
{
}

Add a few enums in the class called SuitEnum and ValueEnum. These enums define the properties of the cards in the deck. I use enums because I like classes to be specific: other developers will have no trouble using these.
public enum SuitEnum
{
Hearts
, Diamonds
, Clubs
, Spades
}

public enum ValueEnum
{
_2
, _3
, _4
, _5
, _6
, _7
, _8
, _9
, _10
, Jack
, Queen
, King
, Ace
}

After that we can define our private members:
private SuitEnum suit;
private ValueEnum value;

Now lets do some real work. The suit of the card is accessible through a property. In the property setter we check for the value which was passed to it before applying it to the private members. This way we ensure the state of the card object is valid.
public SuitEnum Suit
{
get { return this.suit; }
set
{
if (value == SuitEnum.Clubs
value == SuitEnum.Diamonds
value == SuitEnum.Hearts
value == SuitEnum.Spades)
{
this.suit = value;
}
else
{
throw new ArgumentOutOfRangeException();
}
}
}

Next, it should be possible to access the value of the card a property:
public ValueEnum Value
{
get { return this.value; }
set
{
if ((int)value <> 12)
{
throw new ArgumentOutOfRangeException();
}

this.value = value;
}
}

The only way to construct the card class will be by specifying it's suit and value, as shown next:
public Card(SuitEnum suit, ValueEnum value)
{
this.Suit = suit;
this.Value = value;
}

Before I proceed to the unit test for the Card class, I override the ToString() function:
public override string ToString()
{
string tempString = string.Empty;

if ((int)this.value >= 0 && (int)this.value <= 7)
tempString = ((int)this.value + 2).ToString();

switch (this.value)
{
case ValueEnum._10:
tempString = "T";
break;

case ValueEnum.Jack:
tempString = "J";
break;

case ValueEnum.Queen:
tempString = "Q";
break;

case ValueEnum.King:
tempString = "K";
break;

case ValueEnum.Ace:
tempString = "A";
break;
}

switch (this.suit)
{
case SuitEnum.Clubs:
tempString += "c";
break;

case SuitEnum.Diamonds:
tempString += "d";
break;

case SuitEnum.Hearts:
tempString += "h";
break;

case SuitEnum.Spades:
tempString += "s";
break;
}

return tempString;
}

To setup the unit tests, create a new project called Poker.Common.Tests. Notice that the name is exactly like above, with .Tests appended. This ensures other developers won't be surprised about what the heck you are doing.

Add a reference to the nunit.framework assembly in the .Tests project. This assembly is part of the NUnit framework. Also add a reference to the Poker.Common project we created above.

Create a new class called CardTests in the .Tests project. Make the CardTests public and add a TestFixture attribute to it. The TestFixture marks the class as testable by NUnit. Also add some namespace imports on top to NUnit.Framework and Poker.Common.

The class now looks like this:
[TestFixture]
public class CardTests
{
}

When unit testing, there are a few testing scenarios. First of all, you have to cover the border tests. These are tests designed to ensure the minimum and maximum values are covered. Add the following to the testing class and run the tests, by right clicking on the class name and selecting Run Test(s). This command is available after installing the TestDriven.net framework.
[Test]
public void Card_ConstructorFilled2Clubs_ToString2c()
{
Card c = new Card(Card.SuitEnum.Clubs, Card.ValueEnum._2);
Assert.AreEqual("2c", c.ToString());
}

[Test]
public void Card_ConstructorFilledAceSpades_ToStringAs()
{
Card c = new Card(Card.SuitEnum.Spades, Card.ValueEnum.Ace);
Assert.AreEqual("As", c.ToString());
}

Also, we have a crossing point in the class: when the value of the card exceeds 9, suddenly the string representation will be alphanumeric:
[Test]
public void Card_ConstructorFilled9Clubs_ToString9c()
{
Card c = new Card(Card.SuitEnum.Clubs, Card.ValueEnum._9);
Assert.AreEqual("9c", c.ToString());
}

[Test]
public void Card_ConstructorFilled10Diamonds_ToStringTd()
{
Card c = new Card(Card.SuitEnum.Diamonds, Card.ValueEnum._10);
Assert.AreEqual("Td", c.ToString());
}

As you have seen before, we expect the class to throw an error when an invalid value is passed to either the Suit or Value property:
[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Card_ConstructorSuitOutOfRange_ThrowsException()
{
Card c = new Card((Card.SuitEnum) 10, Card.ValueEnum._2);
}

[Test, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Card_ConstructorValueOutOfRange_ThrowsException()
{
Card c = new Card(Card.SuitEnum.Clubs, (Card.ValueEnum) 15);
}

Wrapping up, we have to check for the other suits and alphanumeric values to be correct.
[Test]
public void Card_ConstructorFilledJackHearts_ToStringJh()
{
Card c = new Card(Card.SuitEnum.Hearts, Card.ValueEnum.Jack);
Assert.AreEqual("Jh", c.ToString());
}

[Test]
public void Card_ConstructorFilledQueenSpades_ToStringQs()
{
Card c = new Card(Card.SuitEnum.Spades, Card.ValueEnum.Queen);
Assert.AreEqual("Qs", c.ToString());
}

[Test]
public void Card_ConstructorFilledKingSpades_ToStringKs()
{
Card c = new Card(Card.SuitEnum.Spades, Card.ValueEnum.King);
Assert.AreEqual("Ks", c.ToString());
}

Tada, there we have a fully testable card class! The unit tests assure that any refactoring in the future won't break the class and provides nice documentation to the developer.

Again, if anything in the above code is wrong or you don't understand a specific aspect, don't hestitate to mention it in a comment. I happily spent another blog entry to it.

Geen opmerkingen: