Test Driven Development Example - CUSIP Validation






4.20/5 (4 votes)
A worked example of test driven development using Microsoft.VisualStudio.TestTools.UnitTesting
Introduction
The pseudo algorithm for validating a CUSIP number is documented on Wikipedia and is a fairly straight forward piece of code.
What I am showing here is not how to do that part, but how you could use Microsoft.VisualStudio.TestTools.UnitTesting
to code such a validator in a test-driven manner.
Create the Scaffold
The first step is to create an empty stub of a class that will do the actual logic of the CUSIP validation:
Public MustInherit Class SecurityIdentifier
Private _securtityIdentifier As String
Public ReadOnly Property Identifier As String
Get
Return _securtityIdentifier
End Get
End Property
Public Sub New(ByVal securityIdentifier As String)
_securtityIdentifier = securityIdentifier
End Sub
End Class
Public Class CusipIdentifier
Inherits SecurityIdentifier
Implements ISecurityIdentifierValidation
Public Function IsValid() As Boolean Implements ISecurityIdentifierValidation.IsValid
Return False
End Function
Public Sub New(ByVal cusipSecurityIdentifier As String)
MyBase.New(cusipSecurityIdentifier)
End Sub
End Class
You can see at this stage that the validation does not work - it always returns false
.
Now we put a suite of tests up that will test our CUSIP validator once it is written.
Create the Tests
Firstly, we create a new class and decorate it with the TestClass
attribute. This attribute just tells the test runner that there are tests in this new class:
Imports System.Text
Imports Microsoft.VisualStudio.TestTools.UnitTesting
<TestClass()>
Public Class CusipUnitTest
End Class
Now we throw in some tests to check that the class constructor works for a series of different possible values of the cusipSecurityIdentifier
value:-
<TestMethod()>
<TestCategory("CUSIP")>
Public Sub CusipEmptyCreateTestMethod()
Dim cusipTest As New CusipIdentifier(String.Empty)
Assert.IsNotNull(cusipTest)
End Sub
<TestMethod()>
<TestCategory("CUSIP")>
Public Sub CusipBlankCreateTestMethod()
Dim cusipTest As New CusipIdentifier("")
Assert.IsNotNull(cusipTest)
End Sub
<TestMethod()>
<TestCategory("CUSIP")>
Public Sub CusipNothingCreateTestMethod()
Dim cusipTest As New CusipIdentifier(Nothing)
Assert.IsNotNull(cusipTest)
End Sub
Every test ends in an Assert
statement - if that statement comes out true
, the test passes. If the assert
comes out false
, or any exception occurs, the test is considered a fail.
Positive Tests
Then we add some positive tests. These are tests that check the code works for some values for which we do expect it to work. In my case, I did this by taking a set of existing company's CUSIP identifiers which must - we assume - be valid.
'Agilent - 00846U101
<TestMethod()>
<TestCategory("CUSIP")>
Public Sub CusipAgilentValidTestMethod()
Dim cusipTest As New CusipIdentifier("00846U101")
Dim expected As Boolean = True
Dim actual As Boolean = False
actual = cusipTest.IsValid()
Assert.AreEqual(expected, actual)
End Sub
The thing to note here is that we always initialize the "expected" value to be different to the "actual" variable value. This is so that a test can only pass if that variable value is changed by the test. For good coverage, a set of 30 or so correct values should be coded into tests.
Negative Tests
Now we add some tests that we expect should come out as invalid. You need both positive and negative tests to make sure the code both does what it is expected and excludes cases it is expected to exclude.
To make a negative test, I take a valid CUSIP and change any one of its constituent digits. Again, we make sure to force the actual variable to change for our test.
<TestMethod()>
<TestCategory("CUSIP")>
Public Sub CusipSprintInValidTestMethod_9()
Dim cusipTest As New CusipIdentifier("852061109")
Dim expected As Boolean = False
Dim actual As Boolean = True
actual = cusipTest.IsValid()
Assert.AreEqual(expected, actual)
End Sub
The next step is to run the tests - even though we have not yet written the code that implements the validation logic. This is the key difference in test driven development - we put the tests in first and run them to check they fail.
In the case above, only the positive tests are failing because the validation only returns false
. If all your tests pass before you write any business logic code, you need to check your test code and add more tests.
Fill in the Scaffold Code
Finally, the business logic is written and as soon as it is, all the tests are run against it. The final working CUSIP validator is therefore:
Public Class CusipIdentifier
Inherits SecurityIdentifier
Implements ISecurityIdentifierValidation
Public Function IsValid() As Boolean Implements ISecurityIdentifierValidation.IsValid
If (String.IsNullOrWhiteSpace(MyBase.Identifier)) Then
Return False
End If
If (MyBase.Identifier.Length = 9) Then
Dim checkDigitExpected As Integer = _
GetStringValue(MyBase.Identifier.Substring(0, 8))
Dim checkDigitActual As Integer
If Integer.TryParse(MyBase.Identifier.Substring(8), checkDigitActual) Then
Return (checkDigitActual = checkDigitExpected)
Else
Return False
End If
Else
Return False
End If
End Function
Public Sub New(ByVal cusipSecurityIdentifier As String)
MyBase.New(cusipSecurityIdentifier)
End Sub
Private Function GetLetterValue(ByVal letter As Char) As Integer
Select Case letter
Case "0"
Return 0
Case "1"
Return 1
Case "2"
Return 2
Case "3"
Return 3
Case "4"
Return 4
Case "5"
Return 5
Case "6"
Return 6
Case "7"
Return 7
Case "8"
Return 8
Case "9"
Return 9
Case "A", "a"
Return 10
Case "B", "b"
Return 11
Case "C", "c"
Return 12
Case "D", "d"
Return 13
Case "E", "e"
Return 14
Case "F", "f"
Return 15
Case "G", "g"
Return 16
Case "H", "h"
Return 17
Case "I", "i"
Return 18
Case "J", "j"
Return 19
Case "K", "k"
Return 20
Case "L", "l"
Return 21
Case "M", "m"
Return 22
Case "N", "n"
Return 23
Case "O", "o"
Return 24
Case "P", "p"
Return 25
Case "Q", "q"
Return 26
Case "R", "r"
Return 27
Case "S", "s"
Return 28
Case "T", "t"
Return 29
Case "U", "u"
Return 30
Case "V", "v"
Return 31
Case "W", "w"
Return 32
Case "X", "x"
Return 33
Case "Y", "y"
Return 34
Case "Z", "z"
Return 35
Case "*"
Return 36
Case "@"
Return 37
Case "#"
Return 38
Case Else
Throw New ArgumentOutOfRangeException_
("letter", "Specified letter is not part of the CUSIP allowed letter list")
End Select
End Function
Private Function GetStringValue(ByVal cusipString As String) As Integer
If cusipString.Length < 8 Then
Throw New ArgumentOutOfRangeException("cusipString", "CUSIP is too short")
Else
Dim sum As Integer = 0
For letterPos As Integer = 0 To 7 Step 1
Dim v As Integer
If ((letterPos Mod 2) = 1) Then
v = 2 * GetLetterValue(cusipString.Chars(letterPos))
Else
v = GetLetterValue(cusipString.Chars(letterPos))
End If
sum = sum + ((v \ 10) + (v Mod 10))
Next
Return (10 - (sum Mod 10)) Mod 10
End If
End Function
End Class
Running the Tests
If you have the professional or ultimate versions of Visual Studio, you will have a test menu in the IDE:
Under this menu are options to either run the tests or to debug them - typically what is done is to run all the tests, then debug any failing tests.
If you don't have the full version of Visual Studio, you will need to run MSTest.exe from the command line.
History
- 25th February, 2014: Initial version