Pankracy1999
Pankracy1999

Reputation: 66

C# Unit Testing parser

I need to write a parser. There are many input formats which I want to test and the output is pretty complex. How should I test different inputs and outputs with similar tests?

For example

public class Person
{
    public string Name;
    public int Age;
    public string Comment;
}

public interface IParser
{
    Person Parse(string input);
}

public class Parser : IParser
{
    public Person Parse( string input )
    {
        var fields = input.Split(',');

        var p = new Person
        {
            Name = fields[0],
            Age = int.Parse(fields[1])
        };

        p.Comment = fields.Length == 3 ? fields[2] : ""; 

        return p;
    }
}

I wrote these tests...

public class ParserTests
{
    protected string _input;
    protected Person _expected;

    private IParser _parser = new Parser();
    private Person _actual { get { return _parser.Parse(_input); } }

    [TestMethod]
    public void Parse_Name()
    {
        Assert.AreEqual(_expected.Name, _actual.Name);
    }

    [TestMethod]
    public void Parse_Age()
    {
        Assert.AreEqual(_expected.Age, _actual.Age);
    }

    [TestMethod]
    public void Parse_Comment()
    {
        Assert.AreEqual(_expected.Comment, _actual.Comment);
    }
}

[TestClass]
public class ParserTestsWithoutComment : ParserTests
{
    public ParserTestsWithoutComment()
    {
        _input = "John,29";
        _expected = new Person { Name = "John", Age = 29, Comment = "" };
    }
}

[TestClass]
public class ParserTestsWithComment : ParserTests
{
    public ParserTestsWithComment()
    {
        _input = "Brian,99,test";
        _expected = new Person { Name = "Brian", Age = 99, Comment = "test" };
    }
}

I'm new to unit testing and I'm not sure how to start with more complex stuff. My real input file is more complicated it's like

PokerStars Hand #98451585362:  Hold'em No Limit ($5/$10 USD) - 2013/05/12 9:25:04 CET [2013/05/12 3:25:04 ET]
Table 'Soyuz-Apollo II' 6-max Seat #4 is the button
Seat 1: Codrus426 ($1812.52 in chips) 
Seat 2: JMBigJoe ($2299.10 in chips) 
Seat 3: xinxin1 ($903.94 in chips) 
Seat 4: moshmachine ($1107 in chips) 
Seat 5: TopKat5757 ($1147 in chips) 
Seat 6: LukaschenkoA ($1274.96 in chips) 
TopKat5757: posts small blind $5
LukaschenkoA: posts big blind $10
*** HOLE CARDS ***
Codrus426: calls $10
JMBigJoe: raises $25 to $35
xinxin1: folds 
moshmachine: folds 
TopKat5757: folds 
LukaschenkoA: folds 
Codrus426: calls $25
*** FLOP *** [2h 3s 6h]
Codrus426: checks 
JMBigJoe: bets $41
Codrus426: calls $41
*** TURN *** [2h 3s 6h] [2d]
Codrus426: bets $40
JMBigJoe: calls $40
*** RIVER *** [2h 3s 6h 2d] [Qh]
Codrus426: checks 
JMBigJoe: checks 
*** SHOW DOWN ***
Codrus426: shows [9d Ah] (a pair of Deuces)
JMBigJoe: mucks hand 
Codrus426 collected $244 from pot
*** SUMMARY ***
Total pot $247 | Rake $3 
Board [2h 3s 6h 2d Qh]
Seat 1: Codrus426 showed [9d Ah] and won ($244) with a pair of Deuces
Seat 2: JMBigJoe mucked
Seat 3: xinxin1 folded before Flop (didn't bet)
Seat 4: moshmachine (button) folded before Flop (didn't bet)
Seat 5: TopKat5757 (small blind) folded before Flop
Seat 6: LukaschenkoA (big blind) folded before Flop

And I want parse it to a Hand class I'm working on...

public class Hand
{   
    public long ID; 
    public string Stakes; 
    public DateTime Date; 

    public IDictionary<Street, decimal> Pots;
    public decimal FinalPot; 
    public decimal Rake; 

    public Player Hero; 
    public IDictionary<Player, PlayerInfo> Players; 

    public IList<Card> Board; 

    public IList<Decision> Actions; 

    public Hand()
    { 
        this.Players = new Dictionary<Player, PlayerInfo>();
        this.Board = new List<Card>();
        this.Actions = new List<Decision>();
        this.Pots = new Dictionary<Street, decimal>();
    }
}

public class PlayerInfo
{
    public Player Player;
    public decimal Stack;
    public decimal Summary;
    public Position Position;
    public Card Holecards;
}

Upvotes: 3

Views: 2607

Answers (2)

k3b
k3b

Reputation: 14775

Your Solution is working but difficuilt to understand since you combine global variables with inheritance.

If you are using NUnit 2.5 or later you can use Parameterized Tests with TestCaseAttribute.

[TestCase("John,29","John",29,"")]
[TestCase(",13","",13,"")]
public void ParserTest(Sting stringToParse, String expextedName, int expectedAge, String expectedComment)
{
    IParser _parser = new Parser();
    Person _actual = _parser.Parse(stringToParse);

    Assert.AreEqual(expextedName, _actual.Name, stringToParse + " failed on Name");
    Assert.AreEqual(expextedAge, _actual.Age, stringToParse + " failed on Age");
    Assert.AreEqual(expextedComment, _actual.Comment, stringToParse + " failed on Comment");
}

I think this is much easier to understand.

If you need to stay with mstest you have to simulated it as decribed in how-to-rowtest-with-mstest

Upvotes: 2

J0e3gan
J0e3gan

Reputation: 8938

I think the structure you have is pretty good, bearing in mind that TMTOWTDI.

Be sure, however, to explicitly test Parse for null, empty (i.e. zero-length), and whitespace strings. More complicated/expected-path cases are key to test of course, but simple cases like this are important too.

Also, the structure you are using could very well be collapsed into ParserTests with a common %MethodUnderTest%_With%Condition(s)%_Expect%ExpectedResult% test-case naming convention and optionally the Test Data Builder pattern to make another class responsible for building test data like you are now doing in your ParserTests subclasses

Upvotes: 0

Related Questions