MbUnit CsvDataAttribute
MbUnit has several cool features which distinguish it from some of the other unit testing frameworks on the .NET platform. Among them are the RollbackAttribute, PrincipalAttribute, ThreadedRepeatAttribute, and the Csv/XmlDataAttribute.
I hadn’t noticed the CsvDataAttribute previously when I’ve worked with MbUnit, but it’s definitely one that I think that most teams can make the most use of. While the RowAttribute allows developers to externalize and parameterize their unit tests, the CsvDataAttribute takes it to another level by allowing developers to put test parameters in a simple text file. This is extremely handy since it becomes easier to add more test conditions as you come up with new scenarios without recompiling code. Theoretically, you could even involve your QA team in getting the right set of test data since they could modify the external CSV file. I find this extremely handy 🙂
The documentation on how to use it was lacking a bit; while it explained that you can add metadata (custom attributes) via the CSV file, it didn’t give an example for the ExpectedExceptionAttribute, one of the most common ones, I’d imagine.
Consider the following property which normalizes and validates a phone number (note: this was meant as a simple example):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span style="font-family: Lucida Console;"><span style="color: #808080;">/// <summary></span> <span style="color: #808080;">/// Gets or sets the number.</span> <span style="color: #808080;">/// </summary></span> <span style="color: #808080;">/// <value>The number.</value></span> <span style="color: #0000ff;">public string </span><span style="color: #008000;">Number</span> <span style="color: #5e82fd;">{</span> <span style="color: #0000ff;">get </span><span style="color: #5e82fd;">{ </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_number</span><span style="color: #5e82fd;">; }</span> <span style="color: #0000ff;">set</span> <span style="color: #5e82fd;">{</span> <span style="color: #0000ff;">if </span><span style="color: #5e82fd;">(</span><span style="color: #0000ff;">string</span><span style="color: #5e82fd;">.</span><span style="color: #000000;">IsNullOrEmpty</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">value</span><span style="color: #5e82fd;">))</span> <span style="color: #5e82fd;">{</span> <span style="color: #0000ff;">throw new </span><span style="color: #008000;">ArgumentException</span><span style="color: #5e82fd;">(</span> <span style="color: #ff00ff;">"The phone number cannot be null or empty."</span><span style="color: #5e82fd;">);</span> <span style="color: #5e82fd;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #808080;">// Grab all the digits.</span> <span style="color: #0000ff;">char</span><span style="color: #5e82fd;">[] </span><span style="color: #000000;">digits </span><span style="color: #5e82fd;">= </span><span style="color: #000000;">value</span><span style="color: #5e82fd;">.</span><span style="color: #ff8040;">ToCharArray</span><span style="color: #5e82fd;">()</span> <span style="color: #5e82fd;">.</span><span style="color: #000000;">Where</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">c </span><span style="color: #5e82fd;">=> </span><span style="color: #0000ff;">char</span><span style="color: #5e82fd;">.</span><span style="color: #ff8040;">IsDigit</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">c</span><span style="color: #5e82fd;">)).</span><span style="color: #ff8040;">ToArray</span><span style="color: #5e82fd;">();</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">if </span><span style="color: #5e82fd;">(</span><span style="color: #000000;">digits</span><span style="color: #5e82fd;">.</span><span style="color: #800080;">Length </span><span style="color: #5e82fd;">!= </span><span style="color: #000000;">10</span><span style="color: #5e82fd;">)</span> <span style="color: #5e82fd;">{</span> <span style="color: #0000ff;">throw new </span><span style="color: #008000;">FormatException</span><span style="color: #5e82fd;">(</span> <span style="color: #ff00ff;">"A phone number must contain 10 digits."</span><span style="color: #5e82fd;">);</span> <span style="color: #5e82fd;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">_number </span><span style="color: #5e82fd;">= </span><span style="color: #0000ff;">new string</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">digits</span><span style="color: #5e82fd;">);</span> <span style="color: #5e82fd;">}</span> <span style="color: #5e82fd;">}</span></span> |
The test method might look like this:
1 2 3 4 5 6 7 8 9 10 |
<span style="font-family: Lucida Console;"><span style="color: #5e82fd;">[</span><span style="color: #008080;">Test</span><span style="color: #5e82fd;">]</span> <span style="color: #5e82fd;">[</span><span style="color: #000000;">CsvData</span><span style="color: #5e82fd;">(</span><span style="color: #800080;"><strong>FilePath</strong> </span><span style="color: #5e82fd;">= </span><span style="color: #ff00ff;">"<strong>CsvData\\PhoneNumbers.txt</strong>"</span><span style="color: #5e82fd;">, </span><span style="color: #800080;"><strong>HasHeader</strong> </span><span style="color: #5e82fd;">= </span><span style="color: #0000ff;"><strong>true</strong></span><span style="color: #5e82fd;">)]</span> <span style="color: #0000ff;">public void </span><span style="color: #000000;">TestPhoneNumberNormalizationWithCsv</span><span style="color: #5e82fd;">(</span> <span style="color: #0000ff;">string </span><span style="color: #000000;">type</span><span style="color: #5e82fd;">, </span><span style="color: #0000ff;">string </span><span style="color: #000000;">number</span><span style="color: #5e82fd;">, </span><span style="color: #0000ff;">string </span><span style="color: #000000;">expected</span><span style="color: #5e82fd;">)</span> <span style="color: #5e82fd;">{</span> <span style="color: #800080;">PhoneNumber </span><span style="color: #000000;">phoneNumber </span><span style="color: #5e82fd;">= </span><span style="color: #0000ff;">new </span><span style="color: #800080;">PhoneNumber</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">0</span><span style="color: #5e82fd;">, </span><span style="color: #000000;">0</span><span style="color: #5e82fd;">, </span><span style="color: #000000;">number</span><span style="color: #5e82fd;">, </span><span style="color: #000000;">type</span><span style="color: #5e82fd;">);</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">Assert</span><span style="color: #5e82fd;">.</span><span style="color: #ff8040;">AreEqual</span><span style="color: #5e82fd;">(</span><span style="color: #000000;">expected</span><span style="color: #5e82fd;">, </span><span style="color: #000000;">phoneNumber</span><span style="color: #5e82fd;">.</span><span style="color: #008000;">Number</span><span style="color: #5e82fd;">);</span> <span style="color: #5e82fd;">}</span></span> |
Now we’d like to test our validation logic to gaurd against future refactorings to make sure that anyone refactoring this code throws the appropriate exceptions that our downstream callers expect.
You can see that I’ve used the FilePath property and the HasHeader property (you have to use this if there is a header, otherwise, it detects the header as a row; it’s not true by default it seems). The text file to go with this test would then look like:
1 2 3 4 |
<span style="font-family: Lucida Console;">Type, Number, Expected, [ExpectedException] Home, (732) 555-1012 begin_of_the_skype_highlighting (732) 555-1012 end_of_the_skype_highlighting, 7325551012 Home, , , ArgumentException </span> |
There are a few things to note here:
- If no exceptions are associated with the row, don’t include a trailing comma and empty value (see the first line).
- The headers are not case sensitive.
- Null values can be specified using an empty value.
- When specifying exceptions, you do not need to use typeof(ArgumentException), just the type is enough.
Happy (unit) testing!