unit testing c#

 

See the C++ version of this tutorial here

This is a tutorial on unit testing c# using NUnit. My two favorite references on TDD are “Test Driven Development by Example”, by Kent beck, and this short interview transcription from Joel Spolsky. The book covers the topic very well, and is example based, which is why I like it. The interview transcription tempers things down to reality a bit.

test_driven_development_by_example


Here are a couple sentences from the book that best define Test Driven Development for me.

“Clever play on words in the title. Test driven development is development by example. This book is also structured by example”.

The two rules of Test Driven Development

“Test-Driven Development:
• Don’t write a line of new code unless you first have a failing automated test.
• Eliminate duplication”

“The two rules imply an order to the tasks of programming:
• Red — write a little test that doesn’t work, perhaps doesn’t even compile at first
• Green — make the test work quickly, committing whatever sins necessary in the process
• Refactor — eliminate all the duplication created in just getting the test to work

Red/green/refactor. The TDDs mantra “

I have come to really like TDD as an individual programmer ( I have not used it in a team setting ), that is projects in which I am the only programmer, be it for a personal project, for practice when learning a new language, or even as part of a team that doesn’t require unit testing. I like test driven development because it makes me more productive. Using TDD improves the quality of my code, but the real attraction for me is that I simply get more done, and I don’t mean more lines of code ( TDD naturally results in more lines of code, mostly from the testing code itself). This is somewhat paradoxical, I produce more results in less time while writing more code. If I want to write a program that does x and y, using TDD I will finish the project faster. I’ll give four reasons why I think this is so.

1) it avoids wasted effort
2) makes it is easier to get started.
3) enables design by coding
4) lends itself to bottom up programming

This is subjective, and won’t be true for everyone, but it works for me.

1) The number on reason I like it is because it avoids wasted effort, before I adopted TDD I would often write a function or custom data structure, call it done and then a few hours later realize that I made a fundamental design error and now need to go back and refactor it. I have found that by insisting on writing a test case first I get things correct early on. You’ll never completely remove wasted effort, but using test driven development has really reduced it for me.

2) I find that with some problems, it can be a struggle to get started, or choose the right place to start. Using a test driven development approach forces me to produce small testable units which has the effect of very quickly separating the necessary from the unnecessary, and gets me into a productive mode.

3) Before taking up TDD, I would always tend to design a program using pen and paper first, then once I had a rough overview, I would start to code. TDD has changed my approach, now I immediately start by writing test cases, then pick up pen and paper periodically as I progress, I am always writing code this way. This combination of coding and pen and paper again results wasting less effort, and I get the design right sooner than if I had not been doing TDD.

4) The workflow of test driven development embodies a bottom-up style of programming. I write a lot of small programs where a bottom up approach works great all the way through to completion, but I find I like to take a bottom up approach even with object-oriented design. I find that when doing OOP, I end up wasting a lot of effort in creating code that I end up throwing away. If I first take a bottom-up approach write some bit of functionality to wrap my head around the problem space, then I do a much better job when designing classes, and end up keeping the code I wrote at first.

That’s why I like it, now let me show you a project that is both fun, and a great example for learning about Test Driven Development using Google Test or gtest.

This is a simple console program in which you type a word and it displays all anagrams of that word that are in the dictionary. I chose it because I stumbled upon the algorithm to do it, and thought it was really cool.

 

If you look at different programming books they often discuss anagrams when writing code that generates permutations, as that’s all anagram is. The brute force approach would generate all the permutations of the string, and then look for each of those words in a dictionary. This will work but is very inefficient, first of all generating the permutations is of order n * n!, and the required number of dictionary searches is also of order n * n!.

The algorithm used here makes use of the fundamental theorem of arithmetic, which says that every number can be uniquely written as a product primes, so what we can do is assign each character in the alphabet to a prime number, then anagrams of a word will have the same set of characters, and thus they will have the same prime factorization, and the fundamental theorem of arithmetic guarantees that no other set of characters will have this factorization . We can create a dictionary data structure to map prime factorizations to lists of words. To compute anagrams we will need to compute the prime factorization of the word then we just do a dictionary search. This approach will have a little higher up front cost in generating the word look up table but the actual computation will be very fast, it will consist of n multiplications on a n character string, and a single dictionary look up.

You can just follow along here, cutting and pasting code as you go, or you can obtain this code from my github account here.

First you need to download and install NUnit. You can download it at http://www.nunit.org/index.php?p=download.

Open anagramMain.sln, and build it. You might need to make a few changes to get it to build.

1 ) if you have a different version of NUnit than me, remove the reference to nunit.framework from anagramTest, then add it back through the context menu.

2) anagramTest.cs, and anagramMain.cs contain a hard coded path to the text file that contains the possible words, WordLookup.txt. You will need to change this to reflect your environment.

Once the solution is successfully building, you can run the application by setting anagramMain as the start up project. To run the unit tests you need to run the NUnit Test Runner,

then select File->Open->Project and click on anagramTest.dll.

Then Click Run.  I find its convenient to set the test runner to launch as an external program whenever the anagramTest is launched, this keeps everything accessible from within Visual Studio, this feature is not supported with the Visual Studio express editions.

First take a look at the README file which explains how the project is structured.

uses visual studio express 2010 and Nunit test runner.

there are 3 projects within anagram.sln.
    1) anagramMain - think of this as your "application", i.e. no 
                     testing code will be included or referenced.

    2) anagramTest - this project contains your test cases, and 
                     test data. Has refs to anagramLib, and
                     gtest.

    3) anagramLib - this project contains the functions that are
                    tested using gtest, and called from your 
                    main() application.

If you are actually writing code to be released, then you will want to exclude your testing code from the released executable. To simulate this you can create a separate “testing” project which contains all your testing code, in this example, this project is called anagramTest. The actual released executable is anagramMain, which contains no references to testing code ( i.e. does not lhave a reference to nunit.framework, or contain unit tests ). The code that you actually want to be covered under unit tests is placed in anagramLib. This approach makes it easy isolate code that is not covered under your unit tests, for example if you were not testing some of your GUI code (GUI code is challenging to unit test ) into the “Main” program, or place in another project.

One more advantage of TDD that is potentially huge is the ease with which it allows a new developer to come up to speed on your code. By going through the unit tests for a project, you get to see into the authors mind, and more closely follow the original development path. To understand how this project works, let’s start with anagramTest.cpp.

These test cases appear in the order that I wrote them. The individual functions were incorporated later into a DataDictionary class

public static ulong getPrime(char c)

which takes a char and returns the associated prime.

public static ulong GetPrimeFactorization(string str) 

which takes a string and returns an integer.

public static bool LoadDictionaryFile(string fileName, out Dictionary<ulong, List<string>> wordIndex) 

is the function that creates and initializes the dictionary structure, this is done by reading a file that contains a list of words ( named Wordlist.txt, and stored in the project directory), and computing the prime factorization, then populating the Dictionary structure.

public static List<string> GetAnagrams(string word, Dictionary<ulong, List<string>> wordIndex);

returns a vector of anagrams.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using System.IO;

namespace anagramTest
{
    using anagramLib;
    public class anagramTest
    {
        [TestFixture]
        public class anagramFixture
        {
            //private Dictionary<char, ulong> characterMap;
            
            [TestFixtureSetUpAttribute]               // setup once for all tests
            public void testInitializeCharacterMap()  
            {
                DictionaryData.InitializeCharacterMap();
            }

            [Test]
            public void testGetPrime()
            {
                ulong testChar = DictionaryData.getPrime('F');
                Assert.AreEqual(13, testChar);
            }

            [Test]
            public void testGetPrimeFactorization()
            {
                string[] strings = new string[] { "one", "two", "parsec"};
                Assert.AreEqual(22231, DictionaryData.GetPrimeFactorization(strings[0]));
                Assert.AreEqual(276971, DictionaryData.GetPrimeFactorization(strings[1]));
                Assert.AreEqual(23827210, DictionaryData.GetPrimeFactorization(strings[2]));
            }

            [Test]
            public void testLoadDictionaryFile()
            {
                Dictionary<ulong, List<string>> dictionaryIndex = new Dictionary<ulong, List<string>>();
                bool retval = true;
                string nonExistantFilename = "tHiSfIlEnAmEhAdBeTtErNoTeXiSt";
                retval = DictionaryData.LoadDictionaryFile(nonExistantFilename, out dictionaryIndex);
                Assert.AreEqual(retval, false);
                Assert.AreEqual(dictionaryIndex, null);

                retval = false;
                string defaultDictionaryPath = @"C:\Temp\WordLookup.txt";
                retval = DictionaryData.LoadDictionaryFile(defaultDictionaryPath, out dictionaryIndex);

                Assert.AreEqual(retval, true);
                Assert.AreNotEqual(dictionaryIndex, null);

                // test a few values 9409346, stain, saint, satin
                retval = false;
                List<string> testList = new List<string>();
                retval = dictionaryIndex.TryGetValue(9409346, out testList);
                Assert.AreEqual(retval, true);
                Assert.That(testList.Contains("stain")); 
                Assert.That(testList.Contains("saint"));
                Assert.That(testList.Contains("satin"));
            }

            [Test]
            public void testGetAnagrams()
            {
                Dictionary<ulong, List<string>> dictionaryIndex = new Dictionary<ulong, List<string>>();
                string defaultDictionaryPath = @"C:\Temp\WordLookup.txt";
                DictionaryData.LoadDictionaryFile(defaultDictionaryPath, out dictionaryIndex);
                List<string> retList = new List<string>();
                string stain = "stain";
                string parsec = "parsec";
                string player = "player";

                retList = DictionaryData.GetAnagrams(stain, dictionaryIndex);
                foreach (string str in retList) { Console.WriteLine(str); }
                Console.WriteLine();
                Assert.That(retList.Contains("saint"));
                Assert.That(retList.Contains("satin"));

                retList = DictionaryData.GetAnagrams(parsec, dictionaryIndex);
                foreach(string str in retList ) { Console.WriteLine(str);}
                Console.WriteLine();
                Assert.That(retList.Contains("capers"));
                Assert.That(retList.Contains("pacers"));
                Assert.That(retList.Contains("scrape"));
                Assert.That(retList.Contains("spacer"));
                Assert.That(retList.Contains("escarp"));
                Assert.That(retList.Contains("sparce"));

                retList = DictionaryData.GetAnagrams(player, dictionaryIndex);
                foreach (string str in retList) { Console.WriteLine(str); }
                Console.WriteLine();
                Assert.That(retList.Contains("pearly"));
                Assert.That(retList.Contains("replay"));
                Assert.That(retList.Contains("parley"));
            }
        }
    }
}

anagramLib.cs contain the declarations and definitions for the two classes.

 using System;
<pre>using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace anagramLib
{
    public static class DictionaryData
    {
        private static Dictionary&lt;char, ulong&gt; characterMap = new Dictionary&lt;char, ulong&gt;();
        static public void InitializeCharacterMap()
          {
           characterMap.Add('A', 2);  characterMap.Add('B', 3);  characterMap.Add('C', 5);
           characterMap.Add('D', 7);  characterMap.Add('E', 11); characterMap.Add('F', 13);
           characterMap.Add('G', 17); characterMap.Add('H', 19); characterMap.Add('I', 23);
           characterMap.Add('J', 29); characterMap.Add('K', 31); characterMap.Add('L', 37);
           characterMap.Add('M', 41); characterMap.Add('N', 43); characterMap.Add('O', 47);
           characterMap.Add('P', 53); characterMap.Add('Q', 59); characterMap.Add('R', 61);
           characterMap.Add('S', 67); characterMap.Add('T', 71); characterMap.Add('U', 73);
           characterMap.Add('V', 79); characterMap.Add('W', 83); characterMap.Add('X', 89);
           characterMap.Add('Y', 97); characterMap.Add('Z', 101);
          }

          public static ulong getPrime(char c)
          {
           ulong retval = new ulong();
           characterMap.TryGetValue(Char.ToUpper(c), out retval);
           return retval;
          }

          public static ulong GetPrimeFactorization(string str)
          {
              char[] charArray = str.ToCharArray();
              ulong primeFact = 1;
              foreach (char c in str)
              {
                  primeFact *= getPrime(c);
              }
              return primeFact;
          }

        public static bool LoadDictionaryFile(string fileName, out Dictionary&lt;ulong, List&lt;string&gt;&gt; wordIndex)
        {
         // load a dictionary from file into an index in memory
         FileInfo dictFile = new FileInfo(fileName);
         if (dictFile.Exists)
         {
          // read file and populate wordIndex 
          using (FileStream fs = new FileStream(fileName, FileMode.Open))
           {
            using (StreamReader sr = new StreamReader(fs))
             {
              Dictionary&lt;ulong, List&lt;string&gt;&gt; temp = new Dictionary&lt;ulong, List&lt;string&gt;&gt;();
              wordIndex = temp;
              string textstring = null;
              do
               {
                textstring = sr.ReadLine();
                if (textstring != null)
                 {
                  List&lt;string&gt; tempValue;
                  if (wordIndex.TryGetValue(GetPrimeFactorization(textstring), out tempValue))
                  {
                   // this key already exists, we can just add the word to the list (i.e. add to the value) 
                   wordIndex[GetPrimeFactorization(textstring)].Add(textstring);
                  }
                  else
                  {
                   // this key isn't in the index yet, we need to add both key and value
                   List&lt;string&gt; newList = new List&lt;string&gt;();
                   newList.Add(textstring);
                   wordIndex.Add(GetPrimeFactorization(textstring), newList);
                  }
                 }
               }
              while (textstring != null);
             }
          }
         return true;
         }
         else
         {
          // file does not exist
          wordIndex = null;
          return false;
         }           
       }
        
        // given a word and a dictionary, return the anagrams
        public static List&lt;string&gt; GetAnagrams(string word, Dictionary&lt;ulong, List&lt;string&gt;&gt; wordIndex)
        {
            List&lt;string&gt; anagramList = new List&lt;string&gt;();
            List&lt;string&gt; retList = new List&lt;string&gt;();

            if (wordIndex.TryGetValue(GetPrimeFactorization(word), out anagramList))
            {
                retList = anagramList;
                retList.Remove(word);  // remove the word we entered
                return retList;
            }
            else
            {
                List&lt;string&gt; anagramList2 = new List&lt;string&gt;();
                anagramList2.Add(word); // return what was sent
                return anagramList2;
            }
        }
    }     
}


 

anagramMain is the “application”, it contains a loop that prompts the user for a word, then calls getAnagram() on that word and displays the output.


 

</pre>
<pre>using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace anagramMain
{
    using anagramLib;
    class anagramMain
    {
        public static readonly string defaultDictionaryPath = @"C:\Temp\WordLookup.txt";
        static void Main(string[] args)
        {
            Dictionary&lt;ulong, List&lt;string&gt;&gt; dictionaryIndex = new Dictionary&lt;ulong, List&lt;string&gt;&gt;();
            FileInfo dictFileInfo = new FileInfo(defaultDictionaryPath);
            DictionaryData.InitializeCharacterMap();
            try
            {
                if (!dictFileInfo.Exists)
                {
                    throw new FileNotFoundException("The file was not found.", defaultDictionaryPath);
                }
                else
                {
                    Console.WriteLine("Dictionary file found: {0}", dictFileInfo.FullName);
                    Console.WriteLine("Creating Index......");
                    Console.WriteLine();

                    // initialize data       
                    bool retval = true;
                    retval = DictionaryData.LoadDictionaryFile(defaultDictionaryPath, out dictionaryIndex);
                }
            }
            catch
            {
                Console.WriteLine("Dictionary file not found");
            }
                
            // main program loop
            string word;
            do 
            {
                Console.WriteLine("Enter a word or (-1) to quit\n");
                word = Console.ReadLine();
                List&lt;string&gt; anagrams = new List&lt;string&gt;();
                anagrams = DictionaryData.GetAnagrams(word, dictionaryIndex);
                foreach(string anagram in anagrams)
                {
                    Console.WriteLine("{0}", anagram);
                }
                Console.WriteLine();
            } while (word != "-1");

            return; // close the program
        }
    }
}
</pre>
<pre>

 

 Posted by at 1:17 am

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)


Time limit is exhausted. Please reload CAPTCHA.