Click here to Skip to main content
14,265,813 members

Advanced Test-Driven Development In C#

Rate this:
5.00 (17 votes)
Please Sign up or sign in to vote.
5.00 (17 votes)
2 Aug 2019CPOL
This article discusses advanced Test-Driven Development (TDD) in C# through a simple example. Whether you are a new or experienced developer, this article will show you every step of TDD through a very simple example.

Table Of Contents

  1. Introduction
  2. Origin
  3. Purpose
  4. Development
  5. Debugging
  6. Legacy Code
  7. Example
    1. Fake It!
    2. Triangulation
    3. Multiple Translations
    4. Reverse Translation
    5. File Loading
      1. DictionaryDataSourceTest
      2. DIctionaryParserTest
      3. DictionaryLoaderTest
    6. Type Dependencies Diagram
    7. Test Results
    8. Coverage
    9. How to Run the Source Code?
    10. Other Features
  8. Conclusion
  9. History

Introduction

The traditional method of writing unit tests consists of writing the tests in order to check the validity of the code. First of all, the code is written, then the tests are written. This is contrary to Test-Driven Development.

Test-Driven Development (TDD) consists of writing the tests before writing the code as illustrated in the workflow above.

First of all, the test is written and must fail at the beginning. Then, the code is written so that the test passes. Then, the test must be executed and must succeed. Then, the code is refactored. Then, the test must be executed again to ensure that the code is correct.

To summarize, this is done through five steps:

  1. Write a test.
  2. The test must fail at the beginning.
  3. Write the code so that the test passes.
  4. Execute the test and ensure that it passes.
  5. Refactor the code.

We can notice that in the workflow illustrated above, the test is executed after refactoring the code. This ensures that the code is still correct after the refactoring.

This article will show up advanced TDD in C# through a simple example (we will be creating a bilingual dictionary). Whether you are a new or experienced developer, this article will show you every step of TDD through a very simple example.

Origin

TDD is based on one of the principles of Extreme Programing, also called XP. It's a method of computer project management adapted to reduced teams.

Purpose

TDD is a development method in which the writing of the tests is automatic. This is a very effective technique for delivering software with a suite of regression tests.

TDD plays a fundamental role in developing agile software methods that produce fast and frequent software components.

Development

Development is done in an iterative way.

First of all, the test is written. The first test must fail. Then, the code is written so that the test passes. Then, the test is executed again and must pass. Then, the code is refactored. Then, the test is executed to ensure that the refactoring of the code is correct. This is done for one single unit. Then, for each unit, the same process is executed in an iterative way.

Debugging

Debugging is useful for fixing a unit or a bug. TDD is used to correct the bug as follows:

  • Analyze the problem.
  • Fix the bug.
  • Run the tests to verify that the bug has been fixed.

If the bug still appears, you have to continue correcting until all the tests are valid.

Legacy Code

It often happens to use or to continue the development of an existing code. Understanding the existing code can be done in the following way:

  • Write the test of the unit you want to understand.
  • Execute the test and ensure that it fails.
  • Adapt the code of the test until it passes.

Example

The purpose of this example is to describe every step of TDD through a simple example.

The example will be developed in C# and the test framework used is MSTest.

We will be using Moq for mocking and JetBrains dotCover for coverage.

We will be creating a multilingual dictionary through TDD.

We'll try to respect the SOLID principles while writing our code.

We'll also try to reach 100% of code coverage.

Fake It!

The first task to realize when using TDD is important: It must be so simple so that the cycle red-green-refactor is done fast.

We will first start by creating a test class called DictionaryTest:

[TestClass]
public class DictionaryTest
{
}

Then, we will create a first unit test in this class where we initialize an object of type Dictionary having "en-fr" as name and we will check if the name is correct:

[TestClass]
public class DictionaryTest
{
    [TestMethod]
    public void TestDictionaryName()
    {
      var dict = new Dictionary("en-fr");
      Assert.AreEqual(dict.Name, "en-fr");
    }
}

The test will fail and that's what we want.

Now that we reached the red bar, we will write the code in order that the test passes. To do so, there are a lot of methods. We will use the "Fake it" method. In concrete terms, it consists of doing the minimum necessary to pass the test. In our case, it is sufficient to write a Dictionary class with a property Name returning "en-fr":

public class Dictionary
{
    public string Name {get{return "en-fr";}}          
    
    public Dictionary(string name)
    {
    }
}

We will create the code step by step by using at each step a simple and fast method.

Now, if we run our unit test again, it will pass.

But wait, the code is not refactored. There is a duplication. Indeed, "en-fr" is repeated twice.

We will refactor the code:

public class Dictionary
{
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        Name = name;
    }
}

After refactoring the code, we must run the test again to ensure that the code is correct.

Code refactoring is a form of modification of the code that preserves the exection of existing tests, and which makes it possible to obtain a software architecture with a minimum of defects. Some examples:

  • Remove duplicated code/move code
  • Adjust the private/public attributes/methods

We notice that we've done a cycle of the TDD workflow. Now, we can start again a new cycle with a new test.

The unit tests already done to provide some confidence in the code already written, and allow to consider the future changes with serenity.

Triangulation

In TDD, we first write the test that generates a functional need before to code this need. To refine tests, we will apply the triangulation method.

Let's write a test that checks whether a translation has been added to a dictionary or not (AddTranslation).

The verification will be done through the method GetTranslation.

[TestMethod]
public void TestOneTranslation()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  Assert.AreEqual(dict.GetTranslation("against"), "contre");
}

If we run the test, we will notice that it will fail. Good, that's what we were looking for at this step.

First, we will use the "Fake it" method to pass the test:

public class Dictionary
{
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        Name = name;
    }
    
    public void AddTranslation(string word1, string word2)
    {
    }
    
    public string GetTranslation(string word)
    {
        return "contre";
    }
}

After running the test TestOneTranslation, we will notice that it will pass.

But wait, there is code duplication. The keyword "contre" is repeated twice in the code.

We will change the code to remove this duplication:

public class Dictionary
{
    private Dictionary<string, string> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, string>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2){
        _translations.Add(word1, word2);
    }

    public string GetTranslation(string word){
        return _translations[word];
    }
}

After refactoring the code, we must run the test again to ensure that the code is correct.

Let's add a test to check if a dictionary is empty:

[TestMethod]
public void TestIsEmpty1()
{
    var dict = new Dictionary.Dictionary("en-fr");
    Assert.IsTrue(dict.IsEmpty());
}

If we run the test, we will notice that they will fail. Good, that's what we were looking for at this step.

Let's use the "Fake it" method and write some code to pass the test:

public class Dictionary
{
    [...]

    public bool IsEmpty()
    {
        return true;
    }
}

If we run the test, we will notice that it will pass. But wait, there's a duplication in the code. Indeed, let's fix that:

public class Dictionary
{
    [...]

    public bool IsEmpty()
    {
        return _translations.Count == 0;
    }
}

If we run the test again, we'll notice that it will pass.

We can add another unit test to check that IsEmpty is correct:

[TestMethod]
public void TestIsEmpty2()
{
    var dict = new Dictionary.Dictionary("en-fr");
    dict.AddTranslation("against", "contre");
    Assert.IsFalse(dict.IsEmpty());
}

If we run the test, we'll notice that it will pass.

Multiple Translations

One of the specificities of a dictionary is to be able to manipulate multiple translations. This use case is not originally planned in our architecture.

Let's first write the test:

[TestMethod]
public void TestMultipleTranslations()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  dict.AddTranslation("against", "versus");
  CollectionAssert.AreEqual(dict.GetMultipleTranslations("against"), 
                  string[]{"contre", "versus"});
}

If we run the test, we will notice that it will fail. Good, that's what we were looking for at this step.

First, we will use the "Fake it" method to pass the test by modifying the method AddTranslation and adding the method GetMultipleTranslations:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
    }

    public string[] GetMultipleTranslations(string word)
    {
      return new string[]{"contre", "versus"};
    }
    
    [...]
}

After running the test TestMultipleTranslations, we will notice that it will pass.

But wait, there is code duplication. The string array new string[]{"contre", "versus"} is repeated twice in the code.

We will change the code to remove this duplication:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
    }

    public string[] GetMultipleTranslations(string word)
    {
        return _translations[word].Keys.ToArray();
    }

    public string GetTranslation(string word)
    {
        return _translations[word][0];
    }

    public bool IsEmpty(){
        return _translations.Count == 0;
    }
}

If we run the test again, we will notice that it will pass.

Let's do some refactoring. Let's rename GetMultipleTranslations by GetTranslation:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;
    public string Name { get; }          
    
    public Dictionary(string name)
    {
        _translations = new Dictionary<string, Dictionary<string, string>>();
        Name = name;
    }

    public void AddTranslation(string word1, string word2)
    {
        if (!_translations.ContainsKey(word1))
        {
            _translations.Add(word1, new Dictionary<string, string> {{word2, word1}});
        }
        else
        {
            _translations[word1].Add(word2, word1);
        }
    }    

    public string[] GetTranslation(string word)
    {
        return _translations[word].Keys.ToArray();
    }

    public bool IsEmpty(){
        return _translations.Count == 0;
    }
}

We also have to change our tests:

[TestClass]
public class DictionaryTest{
    [TestMethod]
    public void TestDictionaryName()
    {
      var dict = new Dictionary("en-fr");
      Assert.AreEqual(dict.Name, "en-fr");
    }

    [TestMethod]
    public void TestOneTranslation()
    {
        var dict = new Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        Assert.AreEqual(dict.GetTranslation("against")[0], "contre");
    }

    [TestMethod]
    public void TestIsEmpty1()
    {
        var dict = new Dictionary("en-fr");
        Assert.IsTrue(dict.IsEmpty());
    }

    [TestMethod]
    public void TestIsEmpty2()
    {
        var dict = new Dictionary.Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        Assert.IsFalse(dict.IsEmpty());
    }

    [TestMethod]
    public void TestMultipleTranslations(){
        var dict = new Dictionary("en-fr");
        dict.AddTranslation("against", "contre");
        dict.AddTranslation("against", "versus");
        CollectionAssert.AreEqual(dict.GetTranslation("against"), 
                        new string[]{"contre", "versus"});
    }
}

Reverse Translation

Suppose now that we want to take into account translations in both directions, such as for a bilingual dictionary.

Let's first start by creating the test:

[TestMethod]
public void TestReverseTranslation()
{
  var dict = new Dictionary("en-fr");
  dict.AddTranslation("against", "contre");
  Assert.AreEqual(dict.GetTranslation("contre")[0], "against");
}

If we run the test, we will notice that it will fail. Good, that's what we were looking for at this step.

Now, let's write the code in order to pass the test by using the "Fake it" method:

public string[] GetTranslation(string word)
{
  if(_translations.ContainsKey(word))
  {
    return _translations[word];
  }
  else // try reverse translation
  { 
    return "against";
  }
}

The test will pass. But there is a code duplication. Indeed, "against" is repeated twice. So let's refactor the code:

public string[] GetTranslation(string word)
{
    if (_translations.ContainsKey(word)) return _translations[word].Keys.ToArray();

    return (from t in _translations
            from v in t.Value.Values
            where t.Value.ContainsKey(word)
            select v).Distinct().ToArray();
}

If we run the test again, we will notice that it will pass.

File Loading

Now, let's work on loading the translations from a data source (an external text file for example).

Let's concentrate on the external text file for the moment. The input format will be a text file in which the first line contains the name of the dictionary and the other lines contain words separated by " = ".

Below is an example:

en-fr
against = contre
against = versus

Below is the list of tests that we will perform:

  1. Empty file
  2. A file with only a dictionary name
  3. A file with translations
  4. Erroneous file

First of all, we'll be using mocking to write our tests. Then, we'll write the code as we progress. Then, we'll refactor the code. Finally, we'll test the code to ensure that the refactoring is correct and that everything works fine.

We'll be creating three new test classes:

  • DictionaryDataSourceTest: Where we'll be testing the dictionary loaded from an external data source.
  • DictionaryParserTest: Where we'll be testing the parsing of the dictionary data loaded.
  • DictionaryLoaderTest: Where we'll be testing the loading of the dictionary data loaded from an external data source.

DictionaryDataSourceTest

We'll be using Moq to write our tests.

Empty Filename

First, let's start by writing our test:

[TestMethod]
public void TestEmptyFileName()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetName())
        .Returns(string.Empty);

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    Assert.AreEqual(dict.Name, string.Empty);
}

The test will fail. Good, that's what we were looking for at this step.

We'll be using an interface (IDictionaryParser) for parsing the dictionary data loaded from an external data source.

Below is the interface IDictionaryParser:

public interface IDictionaryParser
{
    string GetName();
}

Let's modify the Dictionary class by using the "Fake it" method in order to pass the test:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = string.Empty;
    }
    
    [...]
}

If we run the test again, we'll notice that it will pass. But wait, there's a duplication in the code. Indeed, string.Empty is repeated twice. So, let's do some refactoring:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
    }
    
    [...]
}

If we run the test again, we'll notice that it will pass.

No Translations

First, let's start by writing our test:

[TestMethod]
public void TestEmptyFileTranslations()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Returns(new Dictionary<string, Dictionary<string, string>>());

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    CollectionAssert.AreEqual(dict.GetTranslation("against"), new string[] { });
}

We'll notice that the test will fail. Good, that's what we were looking for at this step.

First, let's modify the interface IDictionaryParser:

public interface IDictionaryParser
{
    string GetName();
    Dictionary<string, Dictionary<string, string>> GetTranslations();
}

Then, let's write some code by using the "Fake it" method to pass the test:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }
    
    [...]
}

If we run the test again, we'll notice that it will pass. But wait, there's a duplicatoin in the code. Indeed, the dictionary initialization is repeated twice. So, let's do some refactoring:

public class Dictionary
{
    private readonly Dictionary<string, Dictionary<string, string>> _translations;

    public Dictionary(string name)
    {
        Name = name;
        _translations = new Dictionary<string, Dictionary<string, string>>();
    }

    public Dictionary(IDictionaryParser dictionaryParser)
    {
        Name = dictionaryParser.GetName();
        _translations = dictionaryParser.GetTranslations();
    }
    
    [...]
}

If we run the test again, we'll notice that it will pass.

A File With Only A Dictionary Name

First, let's start by writing our test:

[TestMethod]
public void TestDictionaryName()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetName())
        .Returns("en-fr");

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    Assert.AreEqual(dict.Name, "en-fr");
}

We'll notice that the test will pass since we've already written our interface IDictionaryParser and changed the Dictionary class. And no refactoring is needed on this unit at the moment.

Multiple Translations

First, let's start by writing our test:

[TestMethod]
public void TestMultipleTranslations()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Returns(new Dictionary<string, Dictionary<string, string>>
        {
            { "against", new Dictionary<string, string>{{ "contre", "against" }, 
                                                        { "versus", "against" } } }
        });

    var dict = new Dictionary.Dictionary(mockDictionaryParser.Object);
    CollectionAssert.AreEqual(dict.GetTranslation("against"), new[] { "contre", "versus" });
}

We'll notice that the test will pass since we've already written our interface IDictionaryParser and changed the Dictionary class. And no refactoring is needed on this unit at the moment.

Erroneous File

First, let's start by writing the test:

[TestMethod]
public void TestErroneousFile()
{
    var mockDictionaryParser = new Mock<IDictionaryParser>();
    mockDictionaryParser
        .Setup(dp => dp.GetTranslations())
        .Throws(new DictionaryException("The file is erroneous."));

    Assert.ThrowsException<DictionaryException>(() => 
              new Dictionary.Dictionary(mockDictionaryParser.Object));
}

We'll notice that the test will pass since we've already written our interface IDictionaryParser and changed the Dictionary class. And no refactoring is needed on this unit at the moment.

DictionaryParserTest

Now let's create a class that parses the dictionary data loaded by IDictionaryLoader which loads the dictionary data from an external data source.

Empty Filename

First, let's start by writing our test:

[TestMethod]
public void TestEmptyFileName()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.AreEqual(dictionaryParser.GetName(), string.Empty);
}

The test will fail. Good, that's what we were looking for at this step.

We'll be using an interface for loading the dictionary data from an external data source (IDictionaryLoader).

Below is the interface IDictionaryLoader:

public interface IDictionaryLoader
{
    string[] GetLines();
}

Let's write some code by using the "Fake it" method to pass the test:

public class DictionaryParser : IDictionaryParser
{
    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
    }

    public string GetName()
    {
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

The test will pass. Let's move to other units.

No Translations

Let's first start by writing our test:

[TestMethod]
public void TestEmptyFileTranslations()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    CollectionAssert.AreEqual(dictionaryParser.GetTranslations(), 
              new Dictionary<string, Dictionary<string, string>>());
}

The test will pass. Let's move to other units.

Dictionary Name

Let's first start by writing our test:

[TestMethod]
public void TestDictionaryName()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new string[] { "en-fr" });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.AreEqual(dictionaryParser.GetName(), "en-fr");
}

The test will fail. Good, that's what we were looking for at this step.

Let's write some code by using the "Fake it" method in order to pass the test:

public class DictionaryParser : IDictionaryParser
{

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
    }

    public string GetName()
    {
        return "en-fr";
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

The test will pass. But wait, there is a duplication in the code and the test TestEmptyFileName fails. So let's fix that:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];

        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>();
    }
}

Now the tests will pass.

Multiple Translations

Let's first start by writing our test:

[TestMethod]
public void TestMultipleTranslations()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new[] { "en-fr", "against = contre", "against = versus" });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    var expected = new Dictionary<string, Dictionary<string, string>>
    {
        {"against", new Dictionary<string, string> {{"contre", "against"},
                                                    {"versus", "against"}}}
    };

    Assert.IsTrue(dictionaryParser.GetTranslations()
        .All(kvp1 =>
            expected.ContainsKey(kvp1.Key)
            && kvp1.Value.All(kvp2 => kvp1.Value.ContainsValue(kvp2.Value))));
}

The test will fail. Good, that's what we were looking for at this step.

Let's write some code using the "Fake it" method in order to pass the test:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        return new Dictionary<string, Dictionary<string, string>>
        {
            {"against", new Dictionary<string, string> {{"contre", "against"}, 
                                                        {"versus", "against"}}}
        };
    }
}

The test will pass. But wait, there's a duplication in the code and the test TestEmptyFileTranslations fails. So let's fix that:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        var dict = new Dictionary<string, Dictionary<string, string>>();

        if (_lines.Length > 1)
        {
            for (var i = 1; i < _lines.Length; i++)
            {
                var l = _lines[i];
                var regex = new Regex(@"^(?<key>\w+) \= (?<val>\w+)$");
                var match = regex.Match(l);

                var key = match.Groups["key"].Value;
                var val = match.Groups["val"].Value;

                if (!dict.ContainsKey(key))
                {
                    dict.Add(key, new Dictionary<string, string> { { val, key } });
                }
                else
                {
                    dict[key].Add(val, key);
                }
            }
        }

        return dict;
    }
}

Now the tests will pass. The method GetTranslations simply parses the lines loaded by IDictionaryLoader.

Erroneous File

One of the features that we didn't implement yet is to handle the loading of erroneous files. This use case is not originally planned in our architecture.

Let's first start by writing our test:

[TestMethod]
public void TestErroneousFile()
{
    var mockDictionaryLoader = new Mock<IDictionaryLoader>();
    mockDictionaryLoader
        .Setup(dl => dl.GetLines())
        .Returns(new[] { "en-fr", "against = ", "against = " });

    var dictionaryParser = new DictionaryParser(mockDictionaryLoader.Object);

    Assert.ThrowsException<DictionaryException>(() => dictionaryParser.GetTranslations());
}

The test will fail. Good, that's what we were looking for at this step.

Let's edit the code to pass the test:

public class DictionaryParser : IDictionaryParser
{
    private readonly string[] _lines;

    public DictionaryParser(IDictionaryLoader dictionaryLoader)
    {
        _lines = dictionaryLoader.GetLines();
    }

    public string GetName()
    {
        if (_lines.Length > 0) return _lines[0];
        return string.Empty;
    }

    public Dictionary<string, Dictionary<string, string>> GetTranslations()
    {
        var dict = new Dictionary<string, Dictionary<string, string>>();

        if (_lines.Length > 1)
        {
            for (var i = 1; i < _lines.Length; i++)
            {
                var l = _lines[i];
                var regex = new Regex(@"^(?<key>\w+) \= (?<val>\w+)$");
                var match = regex.Match(l);

                if (!match.Success)
                {
                    throw new DictionaryException("The file is erroneous.");
                }

                var key = match.Groups["key"].Value;
                var val = match.Groups["val"].Value;

                if (!dict.ContainsKey(key))
                {
                    dict.Add(key, new Dictionary<string, string> { { val, key } });
                }
                else
                {
                    dict[key].Add(val, key);
                }
            }
        }

        return dict;
    }
}

Now, we'll notice that the test will pass.

DictionaryLoaderTest

In this section, we'll be creating a class that loads dictionary data from an external file.

Empty File

Let's start by creating the first test for testing an empty file:

[TestMethod]
public void TestEmptyFile()
{
    var dictionaryLoader = new DictionaryLoader("dict-empty.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new string[] { });
}

The test will fail. Good, that's what we were looking for at this step.

Now, let's write some code by using the "Fake it" method to pass the test:

public class DictionaryLoader : IDictionaryLoader
{
    private readonly string _dictionaryPath;

    public DictionaryLoader(string dictionaryPath)
    {
        _dictionaryPath = dictionaryPath;
    }

    public string[] GetLines()
    {
        return new string[] { };
    }
}

Now the test will pass. But wait, there's a code duplication. Indeed, new string[] { } is duplicated twice in the code. So, let's do some refactoring:

public class DictionaryLoader : IDictionaryLoader
{
    private readonly string _dictionaryPath;

    public DictionaryLoader(string dictionaryPath)
    {
        _dictionaryPath = dictionaryPath;
    }

    public string[] GetLines()
    {
        return File.ReadAllLines(_dictionaryPath);
    }
}

Now if we run the test again, we'll notice that it will pass.

A File With Only A Dictionary Name

Now, let's use the following text file (dict-name.txt):

en-fr

Let's start by writing our test:

[TestMethod]
public void TestDictionaryName()
{
  var dictionaryLoader = new DictionaryLoader("dict-name.txt");
  CollectionAssert.AreEqual(dictionaryLoader.GetLines(), new[] { "en-fr" });
}

The test will pass since we've implemented the class DictionaryLoader in the previous test.

Let's move to other units.

A File With Translations

Now, let's work with the following dictionary file (dict.txt):

en-fr
against = contre
against = versus

First, let's start by writing our test:

[TestMethod]
public void TestMultipleTranslations()
{
    var dictionaryLoader = new DictionaryLoader("dict.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), 
          new[] { "en-fr", "against = contre", "against = versus" });
}

Again, the test will pass since we implemented the class DictionaryLoader in the previous test.

Erroneous File

Now, let's work with the following dictionary file (dict-erroneous.txt):

en-fr
against = 
against = 

Let's first start by writing our test:

[TestMethod]
public void TestErroneousFile()
{
    var dictionaryLoader = new DictionaryLoader("dict-erroneous.txt");
    CollectionAssert.AreEqual(dictionaryLoader.GetLines(), 
                    new[] { "en-fr", "against = ", "against = " });
}

Again, the test will pass since we implemented the class DictionaryLoader in the previous tests.

We finished testing and creating the class DictionaryLoader which is responsible for loading dictionary data from an external file.

Type Dependencies Diagram

Below is the type dependencies diagram:

And below is the class diagram:

We will notice that all SOLID principles were respected and that the code is maintainable, flexible and extensible.

Test Results

If we run all the tests, we'll notice that they all pass:

We will notice that all unit tests were written. This is one of the benefits of TDD.

Coverage

Below is the code coverage by using JetBrains dotCover:

We will notice that we reached 100% of code coverage. This is one of the benefits of TDD.

How to Run the Source Code?

To run the source code, proceed as follows:

  1. Download the source code from CodeProject.
  2. Restore the nuget packages through the following command: nuget restore tdd.sln
  3. Open tdd.sln in Visual Studio 2019.
  4. Build the solution from Visual Studio 2019.
  5. Run all the unit tests in the solution.
  6. To get the code coverage, download and install JetBrains dotCover, then open the solution tdd.sln in Visual Studio 2019, then right click on DictionaryTest project, finally click on "Cover Unit Tests".

Other Features

Another feature would be to modify the method AddTranslation in the Dictionary class in order to update the external text file of the dictionary.

Finally, another feature would be to read the translations from a database and to write the translations in a database.

Conclusion

This article showed up advanced TDD in C# through a simple example.

We can notice that through TDD:

  • Unit tests were written.
  • We've reached 100% of code coverage.
  • We've spent less time in debugging.
  • The code respects SOLID principles.
  • The code is maintainable, flexible and extensible.
  • The code is more coherent.
  • Clarification of the behavior.
  • Strength of the code.
  • No presence of regression.

TDD is very useful when the code is constantly improved.

TDD provides other benefits because the developer think of the software in terms of small units that can be written and tested independently, and integrated together later.

That's it! I hope you enjoyed reading this article about advanced TDD in C#.

History

  • 26th July, 2019: Initial version
  • 29th July, 2019: Added erroneous file management, added mocking, and updated the article and the source code
  • 31st July, 2019: Added the section "Test Results", and updated the sections "Multiple Translations" and "How To Run The Source Code?"
  • 2nd August, 2019: Added new images to different sections

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Akram El Assas
Architect
Morocco Morocco
Akram El Assas graduated from the french engineering school ENSEIRB located in Bordeaux, a city in the south of France, and got his diploma in software engineering in 2010. He worked in France for Mediatvcom, a company specialized in audiovisual, digital television and new technologies. Mediatvcom offers services such as consulting, project management, audit and turnkey solutions adapted to the needs of customers. Akram worked mainly with Microsoft technologies such as C#, ASP.NET and SQL Server but also with JavaScript, jQuery, HTML5 and CSS3. Akram worked on different projects around digital medias such as Media Asset Management systems, Digital Asset Management systems and sometimes on HbbTV apps.

Comments and Discussions

 
QuestionCan I translate this article to Thai language Pin
solidpz8-Aug-19 19:48
membersolidpz8-Aug-19 19:48 
AnswerRe: Can I translate this article to Thai language Pin
Nelek8-Aug-19 20:31
protectorNelek8-Aug-19 20:31 
GeneralRe: Can I translate this article to Thai language Pin
solidpz8-Aug-19 20:56
membersolidpz8-Aug-19 20:56 
GeneralMy vote of 5 Pin
Eddie Chiou31-Jul-19 22:07
memberEddie Chiou31-Jul-19 22:07 
QuestionIsn’t the File Loading Example Incorrect? Pin
George Swan28-Jul-19 5:13
memberGeorge Swan28-Jul-19 5:13 
AnswerRe: Isn’t the File Loading Example Incorrect? Pin
Akram El Assas28-Jul-19 5:41
memberAkram El Assas28-Jul-19 5:41 
AnswerRe: Isn’t the File Loading Example Incorrect? Pin
Akram El Assas28-Jul-19 8:33
memberAkram El Assas28-Jul-19 8:33 
GeneralRe: Isn’t the File Loading Example Incorrect? Pin
George Swan28-Jul-19 20:20
memberGeorge Swan28-Jul-19 20:20 
GeneralRe: Isn’t the File Loading Example Incorrect? Pin
Akram El Assas29-Jul-19 5:15
memberAkram El Assas29-Jul-19 5:15 
Questionvery clear Pin
Southmountain27-Jul-19 13:17
memberSouthmountain27-Jul-19 13:17 
QuestionNice to to see that people are still interested in unit tests. Pin
dougthompson@timberzen.biz27-Jul-19 1:11
memberdougthompson@timberzen.biz27-Jul-19 1:11 
AnswerRe: Nice to to see that people are still interested in unit tests. Pin
John Brett29-Jul-19 21:25
memberJohn Brett29-Jul-19 21:25 
BugImages broken Pin
Ehsan Sajjad26-Jul-19 1:34
mvpEhsan Sajjad26-Jul-19 1:34 
GeneralRe: Images broken Pin
Akram El Assas26-Jul-19 2:03
memberAkram El Assas26-Jul-19 2:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 25 Jul 2019

Tagged as

Stats

22.5K views
166 downloads
35 bookmarked